From a2d4a38925c1507828b819727f447db820b07190 Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 14 Sep 2026 19:48:48 -0400 Subject: [PATCH] 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. --- src/lib/ui/components/ArtifactDetail.svelte | 633 ++++---- src/lib/ui/components/BrandMark.svelte | 4 + .../ui/components/DeployServerDialog.svelte | 308 ++-- src/lib/ui/components/ServerCard.svelte | 328 ++-- src/lib/ui/components/Toaster.svelte | 83 + src/lib/ui/components/primitives/Badge.svelte | 89 + .../components/primitives/Breadcrumbs.svelte | 55 + .../ui/components/primitives/Button.svelte | 162 ++ src/lib/ui/components/primitives/Card.svelte | 110 ++ .../components/primitives/EmptyState.svelte | 62 + src/lib/ui/components/primitives/Field.svelte | 54 + src/lib/ui/components/primitives/Input.svelte | 102 ++ .../primitives/SectionHeader.svelte | 86 + .../ui/components/primitives/Select.svelte | 90 ++ .../ui/components/primitives/Skeleton.svelte | 39 + src/lib/ui/components/primitives/Stat.svelte | 102 ++ .../ui/components/primitives/StatusPip.svelte | 64 + src/lib/ui/components/primitives/Tabs.svelte | 101 ++ .../ui/components/primitives/Textarea.svelte | 70 + src/lib/ui/motion/tweenNumber.ts | 23 + src/lib/ui/motion/viewTransition.ts | 27 + src/lib/ui/styles/fonts.ts | 8 +- src/lib/ui/styles/tokens.css | 317 +++- src/lib/ui/toast.ts | 41 + src/routes/+layout.svelte | 203 ++- src/routes/+page.svelte | 198 ++- src/routes/betas/+page.svelte | 537 +++--- src/routes/betas/[id]/+page.svelte | 436 +++-- src/routes/configs/+page.svelte | 493 +++--- src/routes/deployments/+page.svelte | 690 +++++--- src/routes/deployments/[id]/+page.svelte | 448 ++--- src/routes/plugins/+page.svelte | 319 ++-- src/routes/scripts/+page.svelte | 519 +++--- src/routes/servers/[name]/+page.svelte | 1436 +++++++---------- 34 files changed, 5093 insertions(+), 3144 deletions(-) create mode 100644 src/lib/ui/components/Toaster.svelte create mode 100644 src/lib/ui/components/primitives/Badge.svelte create mode 100644 src/lib/ui/components/primitives/Breadcrumbs.svelte create mode 100644 src/lib/ui/components/primitives/Button.svelte create mode 100644 src/lib/ui/components/primitives/Card.svelte create mode 100644 src/lib/ui/components/primitives/EmptyState.svelte create mode 100644 src/lib/ui/components/primitives/Field.svelte create mode 100644 src/lib/ui/components/primitives/Input.svelte create mode 100644 src/lib/ui/components/primitives/SectionHeader.svelte create mode 100644 src/lib/ui/components/primitives/Select.svelte create mode 100644 src/lib/ui/components/primitives/Skeleton.svelte create mode 100644 src/lib/ui/components/primitives/Stat.svelte create mode 100644 src/lib/ui/components/primitives/StatusPip.svelte create mode 100644 src/lib/ui/components/primitives/Tabs.svelte create mode 100644 src/lib/ui/components/primitives/Textarea.svelte create mode 100644 src/lib/ui/motion/tweenNumber.ts create mode 100644 src/lib/ui/motion/viewTransition.ts create mode 100644 src/lib/ui/toast.ts diff --git a/src/lib/ui/components/ArtifactDetail.svelte b/src/lib/ui/components/ArtifactDetail.svelte index 5f16a9e..1509d54 100644 --- a/src/lib/ui/components/ArtifactDetail.svelte +++ b/src/lib/ui/components/ArtifactDetail.svelte @@ -1,39 +1,38 @@ {#if loading} -

loading…

+
Loading…
{:else if !artifact} -

{family} not found

+ {:else} - {backLabel} -

{artifact.name}

-
{artifact.target_rel_path}
+ {@const a = artifact} +
+ + {#snippet crumbs()} + + {/snippet} + - {#if runningBetaId} - - {/if} - -
-

Details

-
-
-
target file
-
{artifact.target_rel_path}
-
-
-
scope
-
{scopeLabel(artifact)}
-
-
-
applies to
-
- {#if !artifact.scope_type} - unscoped — deploy from a beta - {:else if matchedServers.length === 0} - no matching servers - {:else} - {matchedServers.join(', ')} - {/if} -
-
-
-
current version
-
- {#if currentVersion} - v{currentVersion.version} · {currentVersion.sha256.slice(0, 8)} promoted - {:else} - — never promoted (open a beta from /betas) - {/if} -
-
- {#if showLatestSeparately && latestVersion} -
-
latest version
-
- v{latestVersion.version} · {latestVersion.sha256.slice(0, 8)} - {latestVersion.state} -
-
- {/if} -
-
- - -
-
- -
-

Version history

- {#if versions.length === 0} -

none

- {:else} -
- {#each versions as v (v.id)} -
- v{v.version} - {v.sha256.slice(0, 12)} - {v.size}B - {v.notes ?? ''} - {v.uploaded_at} - {#if artifact.current_version_id === v.id} - promoted - {:else if v.state === 'tested'} - tested - {:else if v.state === 'test-failed'} - test-failed - {:else} - untested - {/if} -
- {/each}
{/if} -
-
-

Danger

- -
+ +
+

Details

+
+ + +
+
- {#if flash}

{flash}

{/if} - - onDialogClick(scopeDialog, e)}> -
-

Edit scope

-
- - {#if scopeType === 'game'} - - {:else if scopeType === 'map'} - - +
+
+ Target file + {a.target_rel_path} +
+
+ Scope + + + {scopeLabel(a)} + + +
+
+ Applies to + + {#if !a.scope_type} + Unscoped — deploy from a beta + {:else if matchedServers.length === 0} + No matching servers + {:else} + {matchedServers.join(', ')} + {/if} + +
+
+ Current version + + {#if currentVersion} + v{currentVersion.version} · {currentVersion.sha256.slice(0, 8)} + Promoted + {:else} + Never promoted — open a beta + {/if} + +
+ {#if showLatestSeparately && latestVersion} +
+ Latest version + + v{latestVersion.version} · {latestVersion.sha256.slice(0, 8)} + {latestVersion.state} + +
{/if}
-
- - -
- {#if flash}

{flash}

{/if} - -
+
- onDialogClick(uploadDialog, e)}> -
-

Upload new version

-
- - + +
+

Version history

+ {versions.length}
-
- - -
- {#if flash}

{flash}

{/if} - -
+ {#if versions.length === 0} + + {:else} +
+ {#each versions as v (v.id)} +
+ v{v.version} + {v.sha256.slice(0, 12)} + {v.size} B + {v.notes ?? ''} + {v.uploaded_at} + {#if a.current_version_id === v.id} + Promoted + {:else} + {v.state.replace('-', ' ')} + {/if} +
+ {/each} +
+ {/if} + + + +

Danger zone

+ +
+ + {#if flash}
{flash}
{/if} + + + onDialogClick(scopeDialog, e)}> +
+
+

Edit scope

+ +
+ +
+ + {/if} + {#if scopeType === 'map'} + + + + + +
+ + +
+ {#if flash}
{flash}
{/if} + +
+
{/if} diff --git a/src/lib/ui/components/BrandMark.svelte b/src/lib/ui/components/BrandMark.svelte index ad01a15..7acf67c 100644 --- a/src/lib/ui/components/BrandMark.svelte +++ b/src/lib/ui/components/BrandMark.svelte @@ -1,4 +1,8 @@ - + + {#if restarting} + + {/if} +
- + {name}
- {#if health !== 'offline'} - 0}> - {players ?? '—'} - + {#if health === 'offline'} + offline {:else} - offline +
+ 0 ? 'online' : 'primary'} + tween + /> +
{/if}
{mapLabel} {#if gtLabel} - · + {gtLabel} {/if}
@@ -102,21 +121,21 @@ {#if showTiming}
{#if activeElapsed} - - Active - {activeElapsed} - +
+ Active + {activeElapsed} +
{:else if inactiveElapsed} - - Inactive - {inactiveElapsed} - +
+ Inactive + {inactiveElapsed} +
{/if} {#if gameElapsed} - - Game - {gameElapsed} - +
+ Match + {gameElapsed} +
{/if}
{/if} @@ -132,15 +151,15 @@ {/each} - {:else if health !== 'offline'} -
lobby is empty
+ {:else} +
Lobby is empty
{/if} {/if} {#if held.length > 0}
{#each held as h (h.target)} - β {h.target} + β {h.target} {/each}
{/if} @@ -149,203 +168,160 @@ {#if health === 'offline' && lastError} {lastError} {:else if lastSeenAt} - last seen {ago(lastSeenAt)} + Last seen {ago(lastSeenAt)} {:else if lastRestartedAt} - restart {ago(lastRestartedAt)} + Restart {ago(lastRestartedAt)} {:else} - no data + No data {/if} - - {#if restarting} -
- {/if} -
+ diff --git a/src/lib/ui/components/Toaster.svelte b/src/lib/ui/components/Toaster.svelte new file mode 100644 index 0000000..e5c1ea1 --- /dev/null +++ b/src/lib/ui/components/Toaster.svelte @@ -0,0 +1,83 @@ + + +
+ {#each $toasts as t (t.id)} +
+ + {t.message} + +
+ {/each} +
+ + diff --git a/src/lib/ui/components/primitives/Badge.svelte b/src/lib/ui/components/primitives/Badge.svelte new file mode 100644 index 0000000..1649196 --- /dev/null +++ b/src/lib/ui/components/primitives/Badge.svelte @@ -0,0 +1,89 @@ + + + + {@render children?.()} + + + diff --git a/src/lib/ui/components/primitives/Breadcrumbs.svelte b/src/lib/ui/components/primitives/Breadcrumbs.svelte new file mode 100644 index 0000000..889e3f5 --- /dev/null +++ b/src/lib/ui/components/primitives/Breadcrumbs.svelte @@ -0,0 +1,55 @@ + + + + + diff --git a/src/lib/ui/components/primitives/Button.svelte b/src/lib/ui/components/primitives/Button.svelte new file mode 100644 index 0000000..f8cfbef --- /dev/null +++ b/src/lib/ui/components/primitives/Button.svelte @@ -0,0 +1,162 @@ + + +{#if isLink} + + {#if leading}{@render leading()}{/if} + {@render children?.()} + {#if trailing}{@render trailing()}{/if} + {#if loading}{/if} + +{:else} + +{/if} + + diff --git a/src/lib/ui/components/primitives/Card.svelte b/src/lib/ui/components/primitives/Card.svelte new file mode 100644 index 0000000..c33f29d --- /dev/null +++ b/src/lib/ui/components/primitives/Card.svelte @@ -0,0 +1,110 @@ + + + + {@render children?.()} + + + diff --git a/src/lib/ui/components/primitives/EmptyState.svelte b/src/lib/ui/components/primitives/EmptyState.svelte new file mode 100644 index 0000000..58ea3ec --- /dev/null +++ b/src/lib/ui/components/primitives/EmptyState.svelte @@ -0,0 +1,62 @@ + + +
+ {#if icon} +
{@render icon()}
+ {:else} +
+ +
+ {/if} +

{title}

+ {#if description}

{description}

{/if} + {#if actions}
{@render actions()}
{/if} +
+ + diff --git a/src/lib/ui/components/primitives/Field.svelte b/src/lib/ui/components/primitives/Field.svelte new file mode 100644 index 0000000..62b8354 --- /dev/null +++ b/src/lib/ui/components/primitives/Field.svelte @@ -0,0 +1,54 @@ + + +
+ {#if label} + + {/if} +
+ {@render children?.()} +
+ {#if error} + + {:else if hint} +
{hint}
+ {/if} +
+ + diff --git a/src/lib/ui/components/primitives/Input.svelte b/src/lib/ui/components/primitives/Input.svelte new file mode 100644 index 0000000..d535a04 --- /dev/null +++ b/src/lib/ui/components/primitives/Input.svelte @@ -0,0 +1,102 @@ + + + + + diff --git a/src/lib/ui/components/primitives/SectionHeader.svelte b/src/lib/ui/components/primitives/SectionHeader.svelte new file mode 100644 index 0000000..9f1d5de --- /dev/null +++ b/src/lib/ui/components/primitives/SectionHeader.svelte @@ -0,0 +1,86 @@ + + +
+ {#if crumbs}
{@render crumbs()}
{/if} +
+
+ {#if eyebrow}
{eyebrow}
{/if} +

{title}

+ {#if subtitle}

{subtitle}

{/if} +
+ {#if actions}
{@render actions()}
{/if} +
+
+ + diff --git a/src/lib/ui/components/primitives/Select.svelte b/src/lib/ui/components/primitives/Select.svelte new file mode 100644 index 0000000..c5e52f2 --- /dev/null +++ b/src/lib/ui/components/primitives/Select.svelte @@ -0,0 +1,90 @@ + + +
+ + +
+ + diff --git a/src/lib/ui/components/primitives/Skeleton.svelte b/src/lib/ui/components/primitives/Skeleton.svelte new file mode 100644 index 0000000..3e79b32 --- /dev/null +++ b/src/lib/ui/components/primitives/Skeleton.svelte @@ -0,0 +1,39 @@ + + + + + diff --git a/src/lib/ui/components/primitives/Stat.svelte b/src/lib/ui/components/primitives/Stat.svelte new file mode 100644 index 0000000..c489e3d --- /dev/null +++ b/src/lib/ui/components/primitives/Stat.svelte @@ -0,0 +1,102 @@ + + +
+ {#if label} +
{label}
+ {/if} +
+ {displayValue} + {#if unit}{unit}{/if} + {#if trailing}{@render trailing()}{/if} +
+
+ + diff --git a/src/lib/ui/components/primitives/StatusPip.svelte b/src/lib/ui/components/primitives/StatusPip.svelte new file mode 100644 index 0000000..c0e484d --- /dev/null +++ b/src/lib/ui/components/primitives/StatusPip.svelte @@ -0,0 +1,64 @@ + + + + + + + + diff --git a/src/lib/ui/components/primitives/Tabs.svelte b/src/lib/ui/components/primitives/Tabs.svelte new file mode 100644 index 0000000..555b581 --- /dev/null +++ b/src/lib/ui/components/primitives/Tabs.svelte @@ -0,0 +1,101 @@ + + +
+ {#each tabs as t (t.id)} + + {/each} +
+ + diff --git a/src/lib/ui/components/primitives/Textarea.svelte b/src/lib/ui/components/primitives/Textarea.svelte new file mode 100644 index 0000000..5e02b03 --- /dev/null +++ b/src/lib/ui/components/primitives/Textarea.svelte @@ -0,0 +1,70 @@ + + + + + diff --git a/src/lib/ui/motion/tweenNumber.ts b/src/lib/ui/motion/tweenNumber.ts new file mode 100644 index 0000000..20cc3a9 --- /dev/null +++ b/src/lib/ui/motion/tweenNumber.ts @@ -0,0 +1,23 @@ +import { tweened, type Tweened } from 'svelte/motion'; +import { cubicOut } from 'svelte/easing'; + +// Svelte's `tweened` store, pre-configured for our design system so number +// changes feel like Apple's counter animations: fast, expressive out-quart, +// tabular-numeric-friendly. +// +// Respects prefers-reduced-motion at construction time — reduced-motion users +// get instant updates instead of interpolation. + +const REDUCED_MOTION = + typeof window !== 'undefined' && + window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; + +export function tweenedNumber( + initial = 0, + opts: { duration?: number } = {} +): Tweened { + return tweened(initial, { + duration: REDUCED_MOTION ? 0 : (opts.duration ?? 360), + easing: cubicOut + }); +} diff --git a/src/lib/ui/motion/viewTransition.ts b/src/lib/ui/motion/viewTransition.ts new file mode 100644 index 0000000..bfa8916 --- /dev/null +++ b/src/lib/ui/motion/viewTransition.ts @@ -0,0 +1,27 @@ +// SvelteKit `onNavigate` helper — wraps client-side navigation in the +// browser's View Transitions API so route changes cross-fade cleanly and +// shared-element transitions (Fleet card → Server detail hero) morph. +// +// Chromium ships the API today; Firefox and Safari fall through to a plain +// navigation — same behavior as before, just without the transition. + +// Structural type — accepts whatever SvelteKit passes to onNavigate without +// coupling to a specific @sveltejs/kit version's type name. +type NavigationLike = { complete: Promise }; + +export function enableViewTransitions(navigation: NavigationLike): Promise | void { + // Feature-detect without a custom Document extension — the DOM lib type + // varies between TS versions, and any narrowing has to survive both. + const start = (document as unknown as { + startViewTransition?: (cb: () => void | Promise) => unknown; + }).startViewTransition; + + if (typeof start !== 'function') return; + + return new Promise((resolve) => { + start.call(document, async () => { + resolve(); + await navigation.complete; + }); + }); +} diff --git a/src/lib/ui/styles/fonts.ts b/src/lib/ui/styles/fonts.ts index 3ec7df6..badae00 100644 --- a/src/lib/ui/styles/fonts.ts +++ b/src/lib/ui/styles/fonts.ts @@ -6,8 +6,14 @@ // assets. Importing the CSS from JS puts it on the module graph, which // Vite *does* walk, so the font binaries end up in // build/client/_app/immutable/assets/files/. +// +// Global set: +// - Inter (400/500/600/700) — everything. +// - JetBrains Mono (400/500) — data cells (timestamps, scores, IDs). +// +// Alfa Slab One is NOT loaded here — it's used only in BrandMark, which +// imports its own weight so the rest of the product doesn't pay for it. -import '@fontsource/alfa-slab-one/400.css'; import '@fontsource/inter/400.css'; import '@fontsource/inter/500.css'; import '@fontsource/inter/600.css'; diff --git a/src/lib/ui/styles/tokens.css b/src/lib/ui/styles/tokens.css index 883cb51..08141e9 100644 --- a/src/lib/ui/styles/tokens.css +++ b/src/lib/ui/styles/tokens.css @@ -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; + } } diff --git a/src/lib/ui/toast.ts b/src/lib/ui/toast.ts new file mode 100644 index 0000000..da6ee9f --- /dev/null +++ b/src/lib/ui/toast.ts @@ -0,0 +1,41 @@ +import { writable } from 'svelte/store'; + +// Small global toast queue. +// +// Any part of the UI can drop a message and it appears in the corner for a +// couple of seconds, then quietly dismisses. Replaces the ad-hoc "flash" +// strings scattered across pages when the message doesn't need to live inline +// with a form. + +export type ToastVariant = 'info' | 'success' | 'warning' | 'danger'; + +export interface Toast { + id: number; + message: string; + variant: ToastVariant; + ttl: number; +} + +export const toasts = writable([]); + +let nextId = 1; + +export function toast( + message: string, + opts: { variant?: ToastVariant; ttl?: number } = {} +): void { + const t: Toast = { + id: nextId++, + message, + variant: opts.variant ?? 'info', + ttl: opts.ttl ?? 3500 + }; + toasts.update((arr) => [...arr, t]); + if (t.ttl > 0) { + setTimeout(() => dismiss(t.id), t.ttl); + } +} + +export function dismiss(id: number): void { + toasts.update((arr) => arr.filter((t) => t.id !== id)); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fe16598..9116b01 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,12 @@
-
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2c0580d..263199e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,9 +1,17 @@ -
-
-
-

Fleet

- {servers.length} server{servers.length === 1 ? '' : 's'} -
-
- -
-
+
+ +
+ +
+
+ 0 ? 'online' : 'primary'} + tween + /> +
+
+ + +
+ +
+
+
+ + + {#snippet actions()} + + {/snippet} + {#if servers.length === 0} -

no servers registered yet — check SERVERS_ROOT

+ {:else}
- {#each servers as s (s.name)} + {#each servers as s, i (s.name)} {@const live = liveFor(s.name, s)} +
+
{/each}
{/if}
diff --git a/src/routes/betas/+page.svelte b/src/routes/betas/+page.svelte index ace7ddc..2ec800b 100644 --- a/src/routes/betas/+page.svelte +++ b/src/routes/betas/+page.svelte @@ -1,6 +1,12 @@ -

Betas

+
+ -{#if loading} -

loading…

-{:else} -
-

Open a beta

- {#if eligibleScripts.length === 0} -

no artifacts with untested versions — upload a new version from a scripts / configs / plugins page

- {:else} -
-
- {#each picks as row, idx (row.key)} -
- - - -
- {/each} -
- - - - -
- - -
- - -
- {#if flash}

{flash}

{/if} - {/if} -
- - {#if betas.length === 0} -

no betas yet — open one above

+ {#if loading} +
Loading…
{:else} - {#each grouped as g (g.state)} - {#if g.rows.length > 0} -
-

{g.state.replace('-', ' ')} · {g.rows.length}

-
- {#each g.rows as b (b.id)} - - #{b.id} - {summarizeScripts(b)} - {b.state} - {b.servers.length} server{b.servers.length === 1 ? '' : 's'} - - {/each} + +

Open a beta

+ + {#if eligibleScripts.length === 0} + + {:else} +
+ +
+ {#each picks as row, idx (row.key)} +
+
+ + ▾ +
+
+ + ▾ +
+ +
+ {/each} +
+ +
+ + +
+ {#if candidateServers.length === 0} + No server satisfies all selected scopes — pick different scripts. + {:else} + {#each candidateServers as name (name)} + + {/each} + {/if} +
+
+ + +
+ + +
+
+ +
+ + {#if flash}{flash}{/if}
-
+ {/if} - {/each} + + + {#if betas.length === 0} + + {:else} + {#each grouped as g (g.state)} + {#if g.rows.length > 0} +
+
+

{g.state.replace('-', ' ')}

+ {g.rows.length} +
+ +
+ {/if} + {/each} + {/if} {/if} -{/if} +
diff --git a/src/routes/betas/[id]/+page.svelte b/src/routes/betas/[id]/+page.svelte index e5981e0..60dc5fb 100644 --- a/src/routes/betas/[id]/+page.svelte +++ b/src/routes/betas/[id]/+page.svelte @@ -1,37 +1,29 @@ {#if loading} -

loading…

+
Loading…
{:else if !beta} -

beta not found

+ {:else} - ← all betas -
-

Beta #{beta.id}

- {beta.state} -
+ {@const b = beta} +
+ + {#snippet crumbs()} + + {/snippet} + {#snippet actions()} + {b.state} + {/snippet} + -
-

Artifacts · {scripts.length}

- {#if scripts.length === 0} -

no artifacts bundled in this beta

- {:else} -
- {#each scripts as s (s.id)} -
- [{s.kind}] - {s.name} - {s.target_rel_path} - v{s.version?.version ?? '?'} - {s.version?.sha256.slice(0, 8) ?? ''} + + +
+

Artifacts

+ {scripts.length} +
+ {#if scripts.length === 0} + + {:else} +
+ {#each scripts as s (s.id)} +
+ {s.kind} + {s.name} + {s.target_rel_path} + v{s.version?.version ?? '?'} + {s.version?.sha256.slice(0, 8) ?? ''} +
+ {/each} +
+ {/if} +
+ + + +

Options

+
+
+ Wait for health + {b.wait_for_health ? 'Yes' : 'No'} +
+
+ Wait for match + {b.wait_for_match ? 'Yes' : 'No'} +
+
+
+ + + +
+

Server progress

+ {servers.length} +
+
+ {#each servers as bs (bs.server)} +
+ {bs.server} + {bs.stage} +
+ Deploy + Empty + Restart + Healthy + {#if b.wait_for_match} + Match + {/if} +
+ {#if bs.error} + {bs.error} + {/if} + {#if bs.stage === 'failed'} + + {/if}
{/each}
- {/if} -
+ -
-

Options

-
- wait for health: {beta.wait_for_health ? 'yes' : 'no'} - wait for match: {beta.wait_for_match ? 'yes' : 'no'} -
-
- -
-

Server progress

-
- {#each servers as bs (bs.server)} -
- {bs.server} - {bs.stage} -
- deploy - empty - restart - healthy - {#if beta.wait_for_match} - match - {/if} -
- {#if bs.error} - {bs.error} - {/if} - {#if bs.stage === 'failed'} - - {/if} -
- {/each} -
-
- -
-

Actions

-
- - -
-
- - {#if flash}

{flash}

{/if} + + +

Actions

+
+ + +
+ {#if flash}
{flash}
{/if} +
+
{/if} diff --git a/src/routes/configs/+page.svelte b/src/routes/configs/+page.svelte index 6a54044..6c9ef26 100644 --- a/src/routes/configs/+page.svelte +++ b/src/routes/configs/+page.svelte @@ -1,35 +1,30 @@ -

Configs

+
+ -
-

Upload a baseline (one per game)

{#if successBanner} -
-
-

Create a config overlay

-
- -
+ + -
-

Baselines · {baselines.length}

- {#if loading} -

loading…

- {:else if baselines.length === 0} -

no baseline yet — upload one above

- {:else} - - {/if} -
+ + +

Create a config overlay

+
+ + + + +
+ + {#if overlayScopeType === 'map'} +