Files
parking_solution/apps/web/src/index.css
T
julian 040c0ff4ca feat: tabbed setup, user metadata, light theme, scoped shift history
Consolidate the config screens under a single /setup hub with permission-
gated tabs (Devices/Tariff/Subscriptions/Site/Users/Roles/Shifts), collapsing
the top nav to Booth·Shift·Setup; old top-level paths redirect.

Users: add optional profile metadata (full name, phone, email, address) on
create/edit. Theme: a light palette saved to the user's profile (users.theme),
toggled in the header beside the language switch and applied on load like the
language preference. Both ride on a single additive migration (0008).

Shift history: a new GET /api/shifts folds the signed shift_z_report chain into
completed shifts, SCOPED server-side — operators see only their own; holders of
shift:cash see all with an operator + date-range filter. Surfaced as the Shifts
tab; an operator cannot read another operator's takings (param spoofing is
ignored).

These three features share the router, api client and i18n catalogs, so they
land together. Verified live: theme persists across reload, metadata round-
trips to the DB, and shift scoping holds (operator self-only, admin all+filter).

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-19 10:09:18 +02:00

412 lines
13 KiB
CSS

@import "tailwindcss";
/* Bloomberg-terminal aesthetic: dense, dark, monospace, keyboard-first.
Tailwind v4 — design tokens live here in @theme (no tailwind.config.js).
The booth runs on a fixed appliance display; we optimise for a dark room,
glanceable status colour, and high information density over whitespace.
Token vocabulary adopted from the "TRM" design system (Tracking & Race
Management) — TOKENS ONLY: colours, type scale, spacing, radii, shadows.
None of TRM's race-timing components are used. The existing `term-*` accents
are aligned onto TRM's exact values so the whole booth UI inherits the TRM
palette without renaming a single class. TRM's full vocabulary is also
exposed as utilities (night-*, ink-*, paper-*, flag/amber/green/blue, the
spacing/type/shadow scales) for new work.
Offline appliance: NO webfont @import (no network at runtime). Goldplay (the
TRM display face) is not self-hosted yet — display/heading text falls back to
a clean sans stack; wire local Goldplay @font-face here if it's wanted. */
@theme {
/* ============================================================
TERMINAL ACCENTS — aligned onto TRM's exact values.
These keep their `term-*` names (used across every screen),
but now resolve to TRM colours so the palette is unified.
============================================================ */
/* Surfaces — TRM "night" (trackside dark) scale. */
--color-term-bg: #0b0d10; /* TRM --night */
--color-term-panel: #14171c; /* TRM --night-2 */
--color-term-panel-2: #1e222a; /* TRM --night-3 */
--color-term-border: #2a2f38; /* TRM --night-line */
--color-term-muted: #8a8a82; /* TRM --night-fg-3 / --ink-4 */
--color-term-text: #f2f2ee; /* TRM --night-fg */
/* Status accents — TRM semantic colours. */
--color-term-amber: #f2a516; /* TRM --amber (caution / accent / focus) */
--color-term-green: #2e8c4a; /* TRM --green (entry / ok / free) */
--color-term-red: #e8412b; /* TRM --flag (exit / fault / full) */
--color-term-cyan: #2563c8; /* TRM --blue (payment / info / live) */
/* ============================================================
TRM FULL VOCABULARY — exposed as Tailwind utilities for new work.
============================================================ */
/* Ink & paper (light surfaces — for any light-on-dark inversions). */
--color-paper: #fafaf7;
--color-paper-2: #f2f2ee;
--color-paper-3: #e8e8e2;
--color-ink: #0e0e0c;
--color-ink-2: #2a2a26;
--color-ink-3: #5a5a53;
--color-ink-4: #8a8a82;
--color-ink-5: #b8b8b0;
--color-ink-6: #dcdcd4;
/* Night scale (the booth's working surfaces). */
--color-night: #0b0d10;
--color-night-2: #14171c;
--color-night-3: #1e222a;
--color-night-line: #2a2f38;
--color-night-fg: #f2f2ee;
--color-night-fg-2: #b8b8b0;
--color-night-fg-3: #8a8a82;
/* Race accents + semantic. */
--color-flag: #e8412b;
--color-flag-2: #c8331f;
--color-flag-tint: #fbe3de;
--color-amber: #f2a516;
--color-amber-2: #c88500;
--color-amber-tint: #fbefd0;
--color-green: #2e8c4a;
--color-green-2: #1f6a36;
--color-green-tint: #ddefe2;
--color-blue: #2563c8;
--color-blue-2: #1a4fa8;
--color-blue-tint: #dce6f8;
--color-violet: #6b46c1;
--color-magenta: #c9296f;
--color-teal: #188c8a;
--color-ok: #2e8c4a;
--color-warn: #f2a516;
--color-danger: #e8412b;
--color-info: #2563c8;
/* Data-viz categorical (8). */
--color-viz-1: #e8412b;
--color-viz-2: #2563c8;
--color-viz-3: #2e8c4a;
--color-viz-4: #f2a516;
--color-viz-5: #6b46c1;
--color-viz-6: #188c8a;
--color-viz-7: #c9296f;
--color-viz-8: #5a5a53;
/* ---------- TYPE — families ---------- */
/* Mono is the booth's primary face (data-dense, tabular). Display/UI fall
back to a clean sans (Goldplay not self-hosted — see header note). */
--font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SFMono-Regular",
"Menlo", "Consolas", monospace;
--font-display: "Goldplay", "Helvetica Neue", Arial, sans-serif;
--font-ui: "Goldplay", "Helvetica Neue", Arial, sans-serif;
--font-body: "Inter", "Helvetica Neue", Arial, sans-serif;
/* ---------- TYPE — scale (TRM, optimised for data density) ---------- */
--text-overline: 11px;
--text-micro: 12px;
--text-small: 13px;
--text-body: 15px;
--text-lead: 17px;
--text-h6: 14px;
--text-h5: 16px;
--text-h4: 20px;
--text-h3: 26px;
--text-h2: 34px;
--text-h1: 48px;
--text-display: 72px;
--text-jumbo: 120px;
/* ---------- SPACING (TRM 4px base) ---------- */
--spacing-s0: 0;
--spacing-s1: 2px;
--spacing-s2: 4px;
--spacing-s3: 8px;
--spacing-s4: 12px;
--spacing-s5: 16px;
--spacing-s6: 20px;
--spacing-s7: 24px;
--spacing-s8: 32px;
--spacing-s9: 40px;
--spacing-s10: 48px;
--spacing-s11: 64px;
--spacing-s12: 80px;
--spacing-s13: 96px;
/* ---------- RADIUS — TRM is square-edged ---------- */
--radius-term: 2px; /* existing alias, kept */
--radius-r0: 0;
--radius-r1: 2px;
--radius-r2: 4px;
--radius-r3: 6px;
--radius-r4: 10px;
/* ---------- ELEVATION — TRM sharp "printed" offset shadows ---------- */
--shadow-term-1: 0 1px 0 0 #0e0e0c;
--shadow-term-2: 2px 2px 0 0 #0e0e0c;
--shadow-term-3: 4px 4px 0 0 #0e0e0c;
--shadow-soft: 0 1px 2px rgba(14, 14, 12, 0.06), 0 4px 12px rgba(14, 14, 12, 0.04);
--shadow-pop: 0 8px 24px rgba(14, 14, 12, 0.12);
/* ---------- MOTION (TRM) ---------- */
--ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
/* ---------- COMPONENT TOKENS (TRM control heights, table rows) ---------- */
--control-h-sm: 28px;
--control-h-md: 36px;
--control-h-lg: 44px;
--table-row-h: 36px;
--table-row-h-dense: 28px;
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
background: var(--color-term-bg);
color: var(--color-term-text);
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.4;
-webkit-font-smoothing: antialiased;
/* Crisp text and no rubber-banding on the fixed appliance display. */
overscroll-behavior: none;
}
/* Tabular numerics everywhere — counts, money, clocks must not jitter. */
.num,
.tabular {
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
/* Terminal scrollbars — thin, dark, unobtrusive. */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-term-border) transparent;
}
/* A visible keyboard-focus ring in the amber accent (keyboard-first UI). */
:focus-visible {
outline: 1px solid var(--color-term-amber);
outline-offset: 1px;
}
/* ============================================================
COMPONENT LAYER
The TRM tokens are good, but every screen hand-rolled inputs and
buttons as bare outlines on near-black panels, so a field, a card,
and a button were visually indistinguishable. These classes give
each control a real identity:
- inputs read as RECESSED slots (lighter fill + inset shadow)
- the primary button is FILLED (accent body, dark text) — the
one obvious action; neutrals are filled grey, not bare outlines
- explicit hover / active / focus / disabled states everywhere
Use @apply so the classes compose with Tailwind utilities.
============================================================ */
@layer components {
/* ---- Form fields: a recessed slot, clearly an input ---- */
.input,
.select,
.textarea {
@apply w-full rounded-term border bg-term-bg px-2.5 text-term-text
placeholder:text-term-muted;
border-color: #3a414c; /* lighter than panel borders */
height: var(--control-h-md);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
transition: border-color 120ms var(--ease-snap), box-shadow 120ms var(--ease-snap);
}
.textarea {
height: auto;
@apply py-2 leading-snug;
}
.input::placeholder,
.textarea::placeholder {
@apply text-term-muted;
}
.input:hover,
.select:hover,
.textarea:hover {
border-color: #4a525f;
}
.input:focus,
.select:focus,
.textarea:focus {
outline: none;
border-color: var(--color-term-amber);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45), 0 0 0 1px var(--color-term-amber);
}
.input:disabled,
.select:disabled,
.textarea:disabled {
@apply cursor-not-allowed opacity-50;
}
/* Small / dense variant for inline table cells */
.input-sm {
height: var(--control-h-sm);
@apply px-2 text-[12px];
}
.field {
@apply flex flex-col gap-1;
}
.label {
@apply text-[11px] uppercase tracking-wider text-term-muted;
}
.hint {
@apply text-[11px] leading-snug text-term-muted;
}
/* ---- Buttons: a button must look pressable, never like a field ---- */
.btn {
@apply inline-flex items-center justify-center gap-1.5 rounded-term border
px-3 text-[12px] font-semibold uppercase tracking-wider
transition-colors select-none;
height: var(--control-h-md);
/* Neutral default: a filled grey body, not a bare outline. */
background: var(--color-term-panel-2);
border-color: #3a414c;
color: var(--color-term-text);
}
.btn:hover:not(:disabled) {
background: #2a313b;
border-color: #4a525f;
}
.btn:active:not(:disabled) {
transform: translateY(1px);
}
.btn:disabled {
@apply cursor-not-allowed opacity-40;
}
.btn-sm {
height: var(--control-h-sm);
@apply px-2.5 text-[11px];
}
.btn-lg {
height: var(--control-h-lg);
@apply px-5 text-[13px];
}
/* Primary: FILLED amber, dark text — the unmistakable main action. */
.btn-primary {
background: var(--color-term-amber);
border-color: var(--color-term-amber);
color: #0b0d10;
}
.btn-primary:hover:not(:disabled) {
background: #ffb733;
border-color: #ffb733;
}
/* Semantic filled variants (entry / payment / destructive). */
.btn-go {
background: var(--color-term-green);
border-color: var(--color-term-green);
color: #f2f2ee;
}
.btn-go:hover:not(:disabled) {
background: #38a85a;
border-color: #38a85a;
}
.btn-pay {
background: var(--color-term-cyan);
border-color: var(--color-term-cyan);
color: #f2f2ee;
}
.btn-pay:hover:not(:disabled) {
background: #2f74e0;
border-color: #2f74e0;
}
.btn-danger {
background: transparent;
border-color: var(--color-term-red);
color: var(--color-term-red);
}
.btn-danger:hover:not(:disabled) {
background: color-mix(in srgb, var(--color-term-red) 14%, transparent);
}
/* Ghost: lowest-emphasis (cancel, secondary nav) — text + hover only. */
.btn-ghost {
background: transparent;
border-color: transparent;
color: var(--color-term-muted);
}
.btn-ghost:hover:not(:disabled) {
background: var(--color-term-panel-2);
color: var(--color-term-text);
border-color: transparent;
}
/* ---- Card: a panel that is clearly a container, not a field ---- */
.card {
@apply rounded-term border border-term-border bg-term-panel;
}
.card-head {
@apply flex items-center justify-between border-b border-term-border
bg-term-panel-2 px-4 py-2 text-[12px] uppercase tracking-wider text-term-muted;
}
.card-body {
@apply p-4;
}
}
/* ============================================================
LIGHT THEME
The booth defaults to dark (dark room), but a user may prefer light; the
choice is saved to their profile (users.theme) and applied on <html> as
`.theme-light`. Every screen reads colour through the --color-term-* tokens,
so re-pointing them here re-skins the whole app. The TRM "paper/ink" scale
supplies the surfaces; accents are tuned a shade darker for contrast on white.
A few component-layer values are literal hex (input borders, the inset
"recessed" shadow, primary-button text) — those are overridden too so fields
and buttons keep their affordance on a light background.
============================================================ */
html.theme-light {
/* Surfaces — TRM paper scale (light → slightly darker for layering). */
--color-term-bg: #fafaf7; /* paper */
--color-term-panel: #f2f2ee; /* paper-2 */
--color-term-panel-2: #e8e8e2; /* paper-3 */
--color-term-border: #d2d2c8;
--color-term-muted: #5a5a53; /* ink-3 — readable secondary text */
--color-term-text: #14171c; /* near-black ink */
/* Accents — a step darker than the dark-theme values for white-bg contrast. */
--color-term-amber: #b8740a;
--color-term-green: #1f6a36;
--color-term-red: #c8331f;
--color-term-cyan: #1a4fa8;
}
/* Component-layer literals that must flip for light (the rest read tokens). */
html.theme-light .input,
html.theme-light .select,
html.theme-light .textarea {
border-color: #c2c2b8;
box-shadow: inset 0 1px 2px rgba(20, 23, 28, 0.08);
}
html.theme-light .input:hover,
html.theme-light .select:hover,
html.theme-light .textarea:hover {
border-color: #a8a89e;
}
html.theme-light .input:focus,
html.theme-light .select:focus,
html.theme-light .textarea:focus {
box-shadow: inset 0 1px 2px rgba(20, 23, 28, 0.08), 0 0 0 1px var(--color-term-amber);
}
html.theme-light .btn {
border-color: #c2c2b8;
}
html.theme-light .btn:hover:not(:disabled) {
background: #dcdcd4;
border-color: #a8a89e;
}
/* Filled buttons keep light text; primary uses dark-on-amber, kept legible. */
html.theme-light .btn-primary {
color: #fafaf7;
}