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.
111 lines
3.2 KiB
Svelte
111 lines
3.2 KiB
Svelte
<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>
|