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,162 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type Variant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
type Size = 'sm' | 'md' | 'lg';
|
||||
|
||||
interface Props {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
href?: string | null;
|
||||
title?: string;
|
||||
ariaLabel?: string;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
children?: Snippet;
|
||||
leading?: Snippet;
|
||||
trailing?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'secondary',
|
||||
size = 'md',
|
||||
type = 'button',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
href = null,
|
||||
title,
|
||||
ariaLabel,
|
||||
onclick,
|
||||
children,
|
||||
leading,
|
||||
trailing
|
||||
}: Props = $props();
|
||||
|
||||
let isLink = $derived(href !== null && href !== undefined);
|
||||
</script>
|
||||
|
||||
{#if isLink}
|
||||
<a
|
||||
class="btn v-{variant} s-{size}"
|
||||
class:is-loading={loading}
|
||||
aria-disabled={disabled || loading}
|
||||
aria-label={ariaLabel}
|
||||
{title}
|
||||
href={disabled || loading ? undefined : href}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
{#if leading}<span class="slot slot-leading">{@render leading()}</span>{/if}
|
||||
<span class="label">{@render children?.()}</span>
|
||||
{#if trailing}<span class="slot slot-trailing">{@render trailing()}</span>{/if}
|
||||
{#if loading}<span class="spinner" aria-hidden="true"></span>{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
class="btn v-{variant} s-{size}"
|
||||
class:is-loading={loading}
|
||||
{type}
|
||||
{title}
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled || loading}
|
||||
{onclick}
|
||||
>
|
||||
{#if leading}<span class="slot slot-leading">{@render leading()}</span>{/if}
|
||||
<span class="label">{@render children?.()}</span>
|
||||
{#if trailing}<span class="slot slot-trailing">{@render trailing()}</span>{/if}
|
||||
{#if loading}<span class="spinner" aria-hidden="true"></span>{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
border-radius: var(--radius-md);
|
||||
font-family: var(--font-body);
|
||||
font-weight: var(--weight-semibold);
|
||||
letter-spacing: var(--tracking-normal);
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
background-color var(--dur-fast) var(--ease-standard),
|
||||
border-color var(--dur-fast) var(--ease-standard),
|
||||
color var(--dur-fast) var(--ease-standard),
|
||||
transform var(--dur-fast) var(--ease-standard),
|
||||
box-shadow var(--dur-fast) var(--ease-standard);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
.btn:focus { outline: none; }
|
||||
.btn:focus-visible { box-shadow: var(--focus-ring); }
|
||||
.btn:disabled,
|
||||
.btn[aria-disabled='true'] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn:active:not(:disabled):not([aria-disabled='true']) { transform: translateY(0.5px); }
|
||||
|
||||
/* Sizes */
|
||||
.s-sm { padding: 6px 12px; font-size: var(--text-xs); border-radius: var(--radius-sm); }
|
||||
.s-md { padding: 9px 16px; font-size: var(--text-sm); }
|
||||
.s-lg { padding: 12px 22px; font-size: var(--text-base); }
|
||||
|
||||
/* Variants */
|
||||
.v-primary {
|
||||
background: var(--accent-primary);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
.v-primary:hover:not(:disabled):not([aria-disabled='true']) {
|
||||
background: var(--accent-primary-hover);
|
||||
color: var(--gray-950);
|
||||
}
|
||||
|
||||
.v-secondary {
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-subtle);
|
||||
}
|
||||
.v-secondary:hover:not(:disabled):not([aria-disabled='true']) {
|
||||
background: var(--surface-overlay);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.v-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.v-ghost:hover:not(:disabled):not([aria-disabled='true']) {
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.v-danger {
|
||||
background: transparent;
|
||||
color: var(--state-danger);
|
||||
border-color: rgba(255, 86, 112, 0.32);
|
||||
}
|
||||
.v-danger:hover:not(:disabled):not([aria-disabled='true']) {
|
||||
background: rgba(255, 86, 112, 0.12);
|
||||
border-color: var(--state-danger);
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.is-loading .label,
|
||||
.is-loading .slot { visibility: hidden; }
|
||||
.spinner {
|
||||
position: absolute;
|
||||
width: 14px; height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user