040c0ff4ca
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
412 lines
13 KiB
CSS
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;
|
|
}
|