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:
@@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type Padding = 'none' | 'sm' | 'md' | 'lg';
|
||||
type Elevation = 'flat' | 'raised' | 'interactive';
|
||||
|
||||
interface Props {
|
||||
padding?: Padding;
|
||||
elevation?: Elevation;
|
||||
href?: string | null;
|
||||
as?: 'div' | 'section' | 'article' | 'a';
|
||||
ariaLabel?: string;
|
||||
class?: string;
|
||||
style?: string;
|
||||
children?: Snippet;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
padding = 'md',
|
||||
elevation = 'flat',
|
||||
href = null,
|
||||
as = 'div',
|
||||
ariaLabel,
|
||||
class: klass = '',
|
||||
style,
|
||||
children,
|
||||
onclick
|
||||
}: Props = $props();
|
||||
|
||||
let isLink = $derived(href !== null && href !== undefined);
|
||||
let tag = $derived(isLink ? 'a' : as);
|
||||
// When the container isn't a link but does have a click handler, assign
|
||||
// a role so screen readers announce it as interactive (a11y_no_static).
|
||||
let role = $derived(!isLink && onclick ? 'button' : undefined);
|
||||
let tabindex = $derived(!isLink && onclick ? 0 : undefined);
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={tag}
|
||||
class="card p-{padding} e-{elevation} {klass}"
|
||||
href={isLink ? href : undefined}
|
||||
aria-label={ariaLabel}
|
||||
{role}
|
||||
{tabindex}
|
||||
{style}
|
||||
{onclick}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--radius-lg);
|
||||
background: color-mix(in oklab, var(--surface) 90%, transparent);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform var(--dur-fast) var(--ease-standard),
|
||||
box-shadow var(--dur-base) var(--ease-standard),
|
||||
background-color var(--dur-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
/* Inner highlight — a single hairline of light at the top edge. */
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Backdrop blur only where the browser supports it — some rendering
|
||||
backends downgrade poorly, and we don't want blurred noise. */
|
||||
@supports (backdrop-filter: blur(4px)) {
|
||||
.card {
|
||||
backdrop-filter: blur(10px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Padding scale */
|
||||
.p-none { padding: 0; }
|
||||
.p-sm { padding: var(--space-4); }
|
||||
.p-md { padding: var(--space-5); }
|
||||
.p-lg { padding: var(--space-6); }
|
||||
|
||||
/* Elevation */
|
||||
.e-flat { box-shadow: var(--shadow-ambient); }
|
||||
.e-raised { box-shadow: var(--shadow-raised); }
|
||||
|
||||
.e-interactive {
|
||||
box-shadow: var(--shadow-ambient);
|
||||
cursor: pointer;
|
||||
}
|
||||
.e-interactive:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-raised);
|
||||
background: color-mix(in oklab, var(--surface-raised) 92%, transparent);
|
||||
}
|
||||
.e-interactive:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring), var(--shadow-raised);
|
||||
}
|
||||
.e-interactive:active { transform: translateY(-1px); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user