feat: shared visual layer + redesigned topbar (#4)

Introduce new design token set (paper/ink/line/accent + radius/shadow)
with backward-compat aliases for legacy --surface/--navy/--text names.
Swap DM Sans for Plus Jakarta Sans, add JetBrains Mono with tabular
numerics. Replace .app-header with sticky .topbar partial (brand +
segmented nav + Next update / Last refresh meta + Refresh all button).

Add POST /api/refresh-all that runs refreshAllPlayersInDB() with an
in-memory mutex and returns the rendered topbar so HTMX can swap it
in. "Next update" is computed as first Tuesday of next month
(approximation of PDGA's monthly cycle). "Last refresh" derives
from MAX(players.last_updated).
This commit is contained in:
Samuel Enocsson
2026-05-21 12:37:31 +02:00
parent 6e05d3014d
commit 8c977d6624
7 changed files with 359 additions and 102 deletions
+225 -88
View File
@@ -2,52 +2,75 @@
PDGA Ratings — Shared Design System
═══════════════════════════════════════════════════ */
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap');
/* Fonts loaded via <link> in layout.ejs for parallel loading */
:root {
/* Color system */
--surface-0: #f0f2f5;
--surface-1: #ffffff;
--surface-2: #f8f9fb;
--surface-3: #eef0f4;
/* ── New design tokens ─────────────────────────── */
--bg: oklch(0.985 0.005 250);
--paper: #ffffff;
--paper-2: oklch(0.975 0.006 250);
--ink: oklch(0.22 0.02 260);
--ink-2: oklch(0.42 0.015 260);
--ink-3: oklch(0.60 0.012 260);
--line: oklch(0.93 0.008 260);
--line-2: oklch(0.88 0.010 260);
--hover: oklch(0.965 0.012 260);
--navy-900: #0f172a;
--navy-800: #1e293b;
--navy-700: #334155;
--navy-600: #475569;
--accent: #4f5fd8;
--accent-soft: color-mix(in oklab, var(--accent) 12%, white);
--accent-text: color-mix(in oklab, var(--accent) 92%, black);
--text-primary: #0f172a;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--text-inverse: #f8fafc;
--up: oklch(0.55 0.15 150);
--up-soft: oklch(0.94 0.04 150);
--down: oklch(0.58 0.18 25);
--down-soft: oklch(0.95 0.04 25);
--accent: #3b82f6;
--accent-hover: #2563eb;
--accent-subtle: rgba(59, 130, 246, 0.08);
--accent-border: rgba(59, 130, 246, 0.2);
--radius: 14px;
--radius-sm: 10px;
--green: #10b981;
--green-subtle: rgba(16, 185, 129, 0.1);
--red: #ef4444;
--red-subtle: rgba(239, 68, 68, 0.1);
--amber: #f59e0b;
--shadow-card: 0 1px 0 oklch(0.92 0.01 260), 0 1px 2px oklch(0.85 0.01 260 / 0.30);
--shadow-pop: 0 1px 2px oklch(0.80 0.01 260 / 0.18), 0 14px 40px -10px oklch(0.50 0.02 260 / 0.18);
--border: #e2e8f0;
--border-light: #f1f5f9;
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
/* legacy token aliases — remove as components migrate */
--surface-0: var(--bg);
--surface-1: var(--paper);
--surface-2: var(--paper-2);
--surface-3: var(--paper-2);
--text-primary: var(--ink);
--text-secondary: var(--ink-2);
--text-muted: var(--ink-3);
--text-inverse: var(--paper);
--navy-900: var(--ink);
--navy-800: var(--ink);
--navy-700: var(--ink-2);
--navy-600: var(--ink-2);
--border: var(--line);
--border-light: var(--line-2);
--green: var(--up);
--green-subtle: var(--up-soft);
--red: var(--down);
--red-subtle: var(--down-soft);
--accent-hover: color-mix(in oklab, var(--accent) 85%, black);
--accent-subtle: var(--accent-soft);
--accent-border: color-mix(in oklab, var(--accent) 40%, transparent);
--radius-md: var(--radius-sm);
--radius-lg: var(--radius);
--radius-xl: 20px;
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.04);
--shadow-lg: 0 8px 30px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04);
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: var(--shadow-card);
--shadow-lg: var(--shadow-pop);
--shadow-overlay: 0 20px 60px rgba(15, 23, 42, 0.15), 0 4px 12px rgba(15, 23, 42, 0.08);
--font-sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
--transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -59,81 +82,208 @@
body {
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.45;
font-feature-settings: "ss01", "cv11";
margin: 0;
padding: 0;
background-color: var(--surface-0);
color: var(--text-primary);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
}
/* ── App Shell ────────────────────────────────── */
.mono {
font-family: var(--font-mono);
font-feature-settings: "tnum", "zero";
}
.app-header {
background: var(--navy-900);
color: var(--text-inverse);
padding: 0 24px;
/* ── Topbar ───────────────────────────────────── */
.topbar {
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
z-index: 50;
background: color-mix(in oklab, var(--paper) 92%, transparent);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-bottom: 1px solid var(--line);
}
.header-inner {
max-width: 960px;
margin: 0 auto;
.topbar__inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
gap: 16px;
padding: 14px 32px;
max-width: 1400px;
margin: 0 auto;
}
.app-logo {
font-size: 17px;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--text-inverse);
text-decoration: none;
.topbar__brand {
display: flex;
align-items: center;
gap: 8px;
gap: 12px;
text-decoration: none;
}
.app-logo .logo-icon {
width: 28px;
height: 28px;
background: var(--accent);
border-radius: var(--radius-sm);
.topbar__brand-mark {
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 70%, black));
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #fff;
flex-shrink: 0;
}
.app-nav {
.topbar__brand-mark svg {
width: 24px;
height: 24px;
}
.topbar__brand-text {
display: flex;
flex-direction: column;
line-height: 1.1;
}
.topbar__brand-title {
font-size: 15px;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--ink);
}
.topbar__brand-sub {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ink-3);
}
.topbar__nav {
display: flex;
background: var(--paper-2);
border: 1px solid var(--line);
border-radius: 10px;
padding: 4px;
gap: 2px;
}
.app-nav a {
padding: 6px 14px;
color: var(--text-muted);
.topbar__nav a {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 14px;
border-radius: 7px;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
text-decoration: none;
font-size: 14px;
transition: color 120ms ease, background 120ms ease;
}
.topbar__nav a:hover {
color: var(--ink);
}
.topbar__nav a.active {
background: var(--paper);
color: var(--ink);
box-shadow: var(--shadow-card);
}
.topbar__meta {
display: flex;
align-items: center;
gap: 14px;
}
.topbar__meta-item {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.topbar__meta-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ink-3);
}
.topbar__meta-value {
font-family: var(--font-mono);
font-feature-settings: "tnum", "zero";
font-size: 12px;
font-weight: 500;
border-radius: var(--radius-sm);
transition: color var(--transition), background var(--transition);
color: var(--ink);
}
.app-nav a:hover {
color: var(--text-inverse);
background: rgba(255, 255, 255, 0.08);
.topbar__divider {
width: 1px;
height: 22px;
background: var(--line);
}
.app-nav a.active {
color: var(--text-inverse);
background: rgba(255, 255, 255, 0.12);
.topbar__refresh {
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--line);
background: var(--paper);
color: var(--ink);
font-size: 12px;
font-weight: 600;
font-family: var(--font-sans);
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
}
.topbar__refresh:hover:not(:disabled) {
background: var(--hover);
border-color: color-mix(in oklab, var(--line) 60%, var(--ink-3));
}
.topbar__refresh:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.topbar__refresh-spinner {
display: none;
width: 12px;
height: 12px;
border: 2px solid var(--line);
border-top-color: var(--accent);
border-radius: 50%;
animation: topbar-spin 0.7s linear infinite;
}
.topbar__refresh.htmx-request .topbar__refresh-spinner {
display: inline-block;
}
.topbar__refresh.htmx-request .topbar__refresh-icon {
display: none;
}
@keyframes topbar-spin {
to { transform: rotate(360deg); }
}
@media (max-width: 880px) {
.topbar__inner { padding: 10px 16px; gap: 10px; }
.topbar__meta-item, .topbar__divider { display: none; }
.topbar__refresh-label { display: none; }
.topbar__nav a { padding: 0 10px; font-size: 12px; }
.topbar__brand-sub { display: none; }
}
/* ── Container ────────────────────────────────── */
@@ -380,19 +530,6 @@ a:hover {
font-size: 22px;
}
.header-inner {
height: 48px;
}
.app-logo {
font-size: 15px;
}
.app-nav a {
padding: 5px 10px;
font-size: 13px;
}
table {
font-size: 13px;
}
@@ -410,7 +547,7 @@ a:hover {
}
thead {
top: 48px;
top: 56px;
}
}