Redesign UI with new token system, primitives, and motion
Restraint-first design language: neutral gray surfaces + one blue accent (cyan/amber/red reserved for state), 8pt spacing, softer radii, one ambient + one raised shadow, standardized motion tokens. Alfa Slab One demoted to BrandMark only; Inter carries all page copy. New primitives (Button, Card, Badge, Stat, SectionHeader, Skeleton, EmptyState, Tabs, Breadcrumbs, StatusPip, Field, Input, Select, Textarea) plus motion utilities (tweenNumber, viewTransition) and a global toast system. All eight route surfaces rewritten on top: Fleet gains an overview hero, Server detail restructures into tabs, Deployments gets a collapsible drift summary + per-server pipeline visual, and the scripts/configs/plugins/betas flows adopt the new form primitives. Sticky translucent nav with backdrop blur, view-transition cross-fades between routes, staggered card entrance on Fleet, breathing status pip halo, tabular-numeric player counts. Gmail-style g-key nav shortcuts. Mobile stacks below 720px; prefers-reduced-motion neutralizes durations. Legacy tokens preserved as aliases so any un-migrated code still renders.
This commit is contained in:
+229
-88
@@ -1,121 +1,262 @@
|
||||
/* Electric Cherry — design tokens.
|
||||
Blue-primary theme: bright saturated mid-blue from the brand mark,
|
||||
with cherry red demoted to accent + danger. Cool-tinted dark surfaces,
|
||||
larger typography, more generous spacing.
|
||||
|
||||
Font @import chains live in src/lib/ui/styles/fonts.ts and are pulled
|
||||
in by +layout.svelte via JS import — Vite's CSS `@import` inlines the
|
||||
text but doesn't emit the url()-referenced woff files as assets, so
|
||||
we need the JS module graph to see them. */
|
||||
This file is the single source of truth for color, type, spacing, motion,
|
||||
and elevation. It's organized in three layers:
|
||||
|
||||
1. RAW RAMPS — the actual color/type/space values. Never referenced
|
||||
directly by components.
|
||||
2. SEMANTIC ALIASES — what components should reference (--surface,
|
||||
--text-primary, --state-online, --space-4, etc.). Swapping the palette
|
||||
or spacing rhythm later is a single-file edit.
|
||||
3. LEGACY ALIASES — old names (--bg-page, --accent, --ink-300, --pad-card…)
|
||||
mapped onto new semantic values. Kept alive so existing components render
|
||||
during the phased redesign; each page rewrite removes its usage, and
|
||||
this block shrinks over time.
|
||||
|
||||
Design principles this file encodes:
|
||||
· One accent (blue). Cyan/amber/red/green mean state only.
|
||||
· Depth by light, not lines. One ambient shadow + subtle translucency.
|
||||
· One typeface (Inter). Mono for data. Display face lives inside BrandMark.
|
||||
· Motion tokens standardize easings + durations across the product. */
|
||||
|
||||
:root {
|
||||
/* Blue — surfaces + primary (matches logo circle) */
|
||||
--blue-975: #060b16;
|
||||
--blue-925: #0a1428;
|
||||
--blue-875: #12203a;
|
||||
--blue-825: #182b4d;
|
||||
--blue-700: #1e3260;
|
||||
--blue-600: #2560a8;
|
||||
/* ---------- 1. RAW RAMPS ---------- */
|
||||
|
||||
/* Blue — one hue, five stops. Brand primary + focus/link. */
|
||||
--blue-950: #060b16;
|
||||
--blue-900: #0d1826;
|
||||
--blue-700: #1c3d6b;
|
||||
--blue-500: #2e86d1;
|
||||
--blue-400: #4ea3e8;
|
||||
--blue-300: #7cc0f5;
|
||||
|
||||
/* Spark — soft cyan accent */
|
||||
--spark-500: #7cd4ff;
|
||||
--spark-400: #a8e5ff;
|
||||
--spark-glow: 0 0 8px rgba(124, 212, 255, 0.55);
|
||||
--spark-glow-lg: 0 0 14px rgba(124, 212, 255, 0.45);
|
||||
/* Neutral gray — cool-tinted, distinct from the blue ramp so surfaces
|
||||
stop being tinted-blue on tinted-blue. */
|
||||
--gray-975: #08090c;
|
||||
--gray-950: #0d0f14;
|
||||
--gray-925: #12151c;
|
||||
--gray-900: #171b24;
|
||||
--gray-850: #1f242f;
|
||||
--gray-800: #262c39;
|
||||
--gray-700: #363d4d;
|
||||
--gray-500: #6b7488;
|
||||
--gray-400: #8892a5;
|
||||
--gray-300: #a8b2c4;
|
||||
--gray-200: #c5cddc;
|
||||
--gray-100: #dfe4ee;
|
||||
--gray-050: #eef2f8;
|
||||
|
||||
/* Cherry — retained for danger + brand-cherry accent only */
|
||||
--cherry-500: #c8102e;
|
||||
--cherry-400: #e93656;
|
||||
--cherry-300: #ff7a92;
|
||||
/* State — reserved for actual state, never decoration. */
|
||||
--state-online: #7cd4ff;
|
||||
--state-warning: #ffb020;
|
||||
--state-danger: #ff5670;
|
||||
--state-restarting: #7cc0f5;
|
||||
|
||||
/* Neutrals — cool blue-white */
|
||||
--cream: #eef4fb;
|
||||
--cream-dim: #b8c8dc;
|
||||
--ink-100: #eaf1fb;
|
||||
--ink-300: #94a8c4;
|
||||
--ink-500: #5a6d8a;
|
||||
--ink-700: #2a3a55;
|
||||
/* ---------- 2. SEMANTIC ALIASES ---------- */
|
||||
|
||||
/* State */
|
||||
--amber-500: #ffb020;
|
||||
--amber-glow: 0 0 8px rgba(255, 176, 32, 0.4);
|
||||
--danger-500: var(--cherry-500);
|
||||
--success-500: #4ade80;
|
||||
/* Surfaces — three tiers of depth, all near-black cool gray. */
|
||||
--surface-sunken: var(--gray-975);
|
||||
--surface: var(--gray-925);
|
||||
--surface-raised: var(--gray-850);
|
||||
--surface-overlay: var(--gray-800);
|
||||
|
||||
/* Semantic aliases — components should reference THESE, not the raw ramps.
|
||||
Swapping palette later is then a single-file edit. */
|
||||
--bg-page: var(--blue-975);
|
||||
--bg-nav: var(--blue-925);
|
||||
--bg-card: var(--blue-875);
|
||||
--bg-card-hi: var(--blue-825);
|
||||
--border: var(--blue-700);
|
||||
--border-hi: var(--blue-600);
|
||||
--accent: var(--blue-500);
|
||||
--accent-hi: var(--blue-400);
|
||||
--accent-glow: 0 0 8px rgba(46, 134, 209, 0.5);
|
||||
--accent-glow-lg: 0 0 14px rgba(46, 134, 209, 0.4);
|
||||
/* Text — four steps of hierarchy on dark. */
|
||||
--text-primary: var(--gray-050);
|
||||
--text-secondary: var(--gray-300);
|
||||
--text-tertiary: var(--gray-500);
|
||||
--text-quaternary: var(--gray-700);
|
||||
--text-on-accent: #ffffff;
|
||||
|
||||
/* Typography */
|
||||
--font-display: 'Alfa Slab One', 'Georgia', serif;
|
||||
/* Border — hairlines, no more. */
|
||||
--border-subtle: rgba(255, 255, 255, 0.06);
|
||||
--border-strong: rgba(255, 255, 255, 0.12);
|
||||
--border-focus: var(--blue-500);
|
||||
|
||||
/* Accent — one color, one voice. */
|
||||
--accent-primary: var(--blue-500);
|
||||
--accent-primary-hover: var(--blue-300);
|
||||
--accent-primary-active: #256cae;
|
||||
|
||||
/* Typography — Inter everywhere, mono for data.
|
||||
Display face lives inside BrandMark; no global --font-display token. */
|
||||
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-numeric: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||
|
||||
/* Type scale — substantial bump */
|
||||
--text-xs: 11px;
|
||||
/* Type scale — cleaner ramp anchored at 15px body. */
|
||||
--text-xs: 12px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 15px;
|
||||
--text-md: 17px;
|
||||
--text-lg: 20px;
|
||||
--text-xl: 24px;
|
||||
--text-2xl: 30px;
|
||||
--text-3xl: 40px;
|
||||
--text-2xl: 32px;
|
||||
--text-3xl: 44px;
|
||||
|
||||
--leading-tight: 1.15;
|
||||
--leading-body: 1.55;
|
||||
--leading-mono: 1.5;
|
||||
|
||||
--tracking-tight: -0.01em;
|
||||
--tracking-normal: 0;
|
||||
--tracking-wide: 0.02em;
|
||||
--tracking-label: 0.08em;
|
||||
|
||||
/* Weight scale — one variable at each rung so intent is legible. */
|
||||
--weight-regular: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-semibold: 600;
|
||||
--weight-bold: 700;
|
||||
|
||||
/* Spacing — strict 8pt grid (with 4pt half-step). */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
--space-24: 96px;
|
||||
|
||||
/* Radii — three steps + pill. Softer than the old 4/8/12. */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 16px;
|
||||
--radius-pill: 999px;
|
||||
|
||||
/* Elevation — two shadows only, ambient + raised. No glow rings. */
|
||||
--shadow-ambient:
|
||||
0 1px 2px rgba(0, 0, 0, 0.35),
|
||||
0 4px 12px rgba(0, 0, 0, 0.22);
|
||||
--shadow-raised:
|
||||
0 2px 4px rgba(0, 0, 0, 0.4),
|
||||
0 12px 28px rgba(0, 0, 0, 0.32);
|
||||
|
||||
/* Motion — standardized easings + durations. Apple-esque out-quart. */
|
||||
--ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-linear: linear;
|
||||
--dur-instant: 80ms;
|
||||
--dur-fast: 160ms;
|
||||
--dur-base: 240ms;
|
||||
--dur-slow: 360ms;
|
||||
--dur-slower: 560ms;
|
||||
|
||||
/* Focus ring — 2px accent, 2px offset, everywhere. */
|
||||
--focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent-primary);
|
||||
|
||||
/* ---------- 3. LEGACY ALIASES ----------
|
||||
Old token names, mapped onto new semantic values so components that
|
||||
haven't been rewritten yet keep rendering. Delete entries as pages
|
||||
migrate; the block should be empty at the end of Phase 8. */
|
||||
|
||||
/* Old blue ramp names */
|
||||
--blue-975: var(--gray-975);
|
||||
--blue-925: var(--gray-950);
|
||||
--blue-875: var(--gray-925);
|
||||
--blue-825: var(--gray-850);
|
||||
--blue-600: #256cae;
|
||||
--blue-400: var(--blue-300);
|
||||
|
||||
/* Old spark/cherry/amber names */
|
||||
--spark-500: var(--state-online);
|
||||
--spark-400: #a8e5ff;
|
||||
--spark-glow: 0 0 8px rgba(124, 212, 255, 0.45);
|
||||
--spark-glow-lg: 0 0 14px rgba(124, 212, 255, 0.35);
|
||||
--cherry-500: var(--state-danger);
|
||||
--cherry-400: #ff7590;
|
||||
--cherry-300: #ff9db0;
|
||||
--amber-500: var(--state-warning);
|
||||
--amber-glow: 0 0 8px rgba(255, 176, 32, 0.32);
|
||||
--danger-500: var(--state-danger);
|
||||
--success-500: #4ade80;
|
||||
|
||||
/* Old neutral names */
|
||||
--cream: var(--text-primary);
|
||||
--cream-dim: var(--gray-200);
|
||||
--ink-100: var(--text-primary);
|
||||
--ink-300: var(--text-secondary);
|
||||
--ink-500: var(--text-tertiary);
|
||||
--ink-700: var(--text-quaternary);
|
||||
|
||||
/* Old surface/border/accent aliases */
|
||||
--bg-page: var(--surface-sunken);
|
||||
--bg-nav: var(--surface);
|
||||
--bg-card: var(--surface);
|
||||
--bg-card-hi: var(--surface-raised);
|
||||
--border: var(--border-subtle);
|
||||
--border-hi: var(--border-strong);
|
||||
--accent: var(--accent-primary);
|
||||
--accent-hi: var(--accent-primary-hover);
|
||||
--accent-glow: 0 0 0 1px rgba(46, 134, 209, 0.35);
|
||||
--accent-glow-lg: 0 0 0 1px rgba(46, 134, 209, 0.28);
|
||||
|
||||
/* Old typography names */
|
||||
--font-display: 'Alfa Slab One', 'Georgia', serif;
|
||||
--text-mono: 13px;
|
||||
--text-mono-sm: 12px;
|
||||
--tracking-display: 0.12em;
|
||||
--tracking-nav: 0.03em;
|
||||
|
||||
--leading-tight: 1.2;
|
||||
--leading-body: 1.6;
|
||||
--leading-mono: 1.55;
|
||||
|
||||
--tracking-display: 2px;
|
||||
--tracking-nav: 0.4px;
|
||||
--tracking-label: 1px;
|
||||
|
||||
/* Spacing */
|
||||
--pad-card: 20px;
|
||||
--pad-panel: 24px;
|
||||
--pad-nav-y: 16px;
|
||||
--pad-nav-x: 32px;
|
||||
--gap-section: 32px;
|
||||
--gap-cards: 18px;
|
||||
|
||||
/* Radii */
|
||||
--r-sm: 4px;
|
||||
--r-md: 8px;
|
||||
--r-lg: 12px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-card: 0 2px 12px rgba(6, 11, 22, 0.5);
|
||||
--shadow-panel: 0 4px 24px rgba(6, 11, 22, 0.4);
|
||||
/* Old spacing/radii names */
|
||||
--pad-card: var(--space-5);
|
||||
--pad-panel: var(--space-6);
|
||||
--pad-nav-y: var(--space-4);
|
||||
--pad-nav-x: var(--space-8);
|
||||
--gap-section: var(--space-8);
|
||||
--gap-cards: var(--space-4);
|
||||
--r-sm: var(--radius-sm);
|
||||
--r-md: var(--radius-md);
|
||||
--r-lg: var(--radius-lg);
|
||||
--shadow-card: var(--shadow-ambient);
|
||||
--shadow-panel: var(--shadow-raised);
|
||||
}
|
||||
|
||||
/* Keyframes — pared down.
|
||||
Legacy names kept so existing components animate; the new StatusPip
|
||||
uses `pip-breathe` (opacity-only, GPU-cheap). */
|
||||
|
||||
@keyframes pip-breathe {
|
||||
0%, 100% { opacity: 0.35; transform: scale(1); }
|
||||
50% { opacity: 0.9; transform: scale(1.4); }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
from { background-position: -200% 0; }
|
||||
to { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* Legacy — retained until ServerCard/BrandMark/StatusDot are replaced. */
|
||||
@keyframes electric-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 0 rgba(124, 212, 255, 0.55), var(--spark-glow);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 6px rgba(124, 212, 255, 0), var(--spark-glow-lg);
|
||||
}
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(124, 212, 255, 0.5); }
|
||||
50% { box-shadow: 0 0 0 5px rgba(124, 212, 255, 0); }
|
||||
}
|
||||
|
||||
@keyframes cherry-fizz {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-1px); }
|
||||
50% { transform: translateY(-1px); }
|
||||
}
|
||||
|
||||
@keyframes spark-slide {
|
||||
from { background-position: -100% 0; }
|
||||
to { background-position: 200% 0; }
|
||||
to { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* Reduced motion — neutralize all animation. Components that respect this
|
||||
token get calm behavior for free; the media query below handles the
|
||||
ones that don't. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--dur-instant: 0.01ms;
|
||||
--dur-fast: 0.01ms;
|
||||
--dur-base: 0.01ms;
|
||||
--dur-slow: 0.01ms;
|
||||
--dur-slower: 0.01ms;
|
||||
}
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user