Unify UI workflows and streamline fleet management

This commit is contained in:
2026-09-18 14:00:28 -04:00
parent 9cf0bd87f4
commit f54cac8c50
63 changed files with 1706 additions and 1286 deletions
+34 -16
View File
@@ -7,6 +7,9 @@
import { page } from '$app/stores';
import BrandMark from '$ui/components/BrandMark.svelte';
import Toaster from '$ui/components/Toaster.svelte';
import Confirmation from '$ui/components/Confirmation.svelte';
import { resolveConfirmation } from '$ui/confirm.js';
import '$ui/styles/components.css';
import { enableViewTransitions } from '$ui/motion/viewTransition.js';
// Side-effect import — puts @fontsource CSS on Vite's module graph so
// the referenced woff/woff2 files get emitted into the build.
@@ -17,7 +20,7 @@
let menuOpen = $state(false);
let menuButton: HTMLButtonElement;
afterNavigate(() => { menuOpen = false; });
afterNavigate(() => { menuOpen = false; resolveConfirmation(false); });
function closeMenu(event: KeyboardEvent) {
if (event.key === 'Escape' && menuOpen) {
@@ -27,8 +30,8 @@
}
onMount(() => {
startPipelineStream();
startServerStream();
const stopPipeline = startPipelineStream();
const stopServers = startServerStream();
const stopFleetMonitor = startFleetMonitor();
// Two-key nav shortcuts (Gmail-style): `g f` fleet, `g d` deployments,
@@ -42,7 +45,7 @@
const onKey = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return;
if (target && (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName) || target.isContentEditable || target.closest('dialog'))) return;
if (e.metaKey || e.ctrlKey || e.altKey) return;
if (!armed && e.key === 'g') {
@@ -64,6 +67,8 @@
window.addEventListener('keydown', onKey);
return () => {
stopFleetMonitor();
stopPipeline?.();
stopServers?.();
window.removeEventListener('keydown', onKey);
if (armTimer) clearTimeout(armTimer);
};
@@ -74,20 +79,23 @@
onNavigate(enableViewTransitions);
const tabs = [
{ href: '/', label: 'Fleet', match: (p: string) => p === '/' },
{ href: '/scripts', label: 'Scripts', match: (p: string) => p.startsWith('/scripts') },
{ href: '/configs', label: 'Configs', match: (p: string) => p.startsWith('/configs') },
{ href: '/plugins', label: 'Plugins', match: (p: string) => p.startsWith('/plugins') },
{ href: '/deployments', label: 'Deployments', match: (p: string) => p.startsWith('/deployments') },
{ href: '/campaigns', label: 'Campaigns', match: (p: string) => p.startsWith('/campaigns') },
{ href: '/server-keys', label: 'Keys', match: (p: string) => p.startsWith('/server-keys') },
{ href: '/betas', label: 'Betas', match: (p: string) => p.startsWith('/betas') }
{ href: '/', label: 'Fleet', match: (p: string) => p === '/' || p.startsWith('/servers/') },
{ href: '/scripts', label: 'Library', match: (p: string) => /^\/(scripts|configs|plugins)(\/|$)/.test(p) },
{ href: '/deployments', label: 'Operations', match: (p: string) => /^\/(deployments|campaigns)(\/|$)/.test(p) },
{ href: '/betas', label: 'Betas', match: (p: string) => p.startsWith('/betas') },
{ href: '/server-keys', label: 'Settings', match: (p: string) => p.startsWith('/server-keys') }
];
const secondary = $derived(tabs[1].match($page.url.pathname)
? [{ href: '/scripts', label: 'Scripts' }, { href: '/configs', label: 'Configs' }, { href: '/plugins', label: 'Plugins' }]
: tabs[2].match($page.url.pathname)
? [{ href: '/deployments', label: 'Server deployments' }, { href: '/campaigns', label: 'Rollouts' }]
: tabs[4].match($page.url.pathname) ? [{ href: '/server-keys', label: 'Server keys' }] : []);
</script>
<svelte:window onkeydown={closeMenu} />
<div class="shell">
<a class="skip-link" href="#main-content">Skip to content</a>
<nav aria-label="Primary">
<a class="brand-link" href="/" aria-label="Electric Cherry home">
<BrandMark size="sm" />
@@ -112,10 +120,12 @@
</div>
<div class="strut" aria-hidden="true"></div>
</nav>
<main>
{#if secondary.length}<div class="subnav"><div role="navigation" aria-label="Section">{#each secondary as item}<a href={item.href} aria-current={$page.url.pathname.startsWith(item.href) ? 'page' : undefined}>{item.label}</a>{/each}</div></div>{/if}
<main id="main-content" tabindex="-1">
{@render children()}
</main>
<Toaster />
<Confirmation />
</div>
<style>
@@ -140,7 +150,8 @@
color: var(--text-on-accent);
}
:global(:focus-visible) {
outline: none;
outline: 2px solid var(--accent-primary-hover);
outline-offset: 3px;
}
/* Ambient background — a faint radial gradient hints at depth without
@@ -223,14 +234,14 @@
main {
min-width: 0;
overflow-wrap: anywhere;
padding: var(--space-10) var(--space-10) var(--space-24);
padding: var(--space-6) var(--space-8) var(--space-16);
max-width: 1440px;
margin: 0 auto;
view-transition-name: main;
}
/* The full navigation needs more room than a phone or portrait tablet. */
@media (max-width: 1000px) {
@media (max-width: 800px) {
nav {
flex-wrap: wrap;
justify-content: space-between;
@@ -271,6 +282,13 @@
:global(input), :global(select), :global(textarea) { min-width: 0; max-width: 100%; }
:global(dialog) { max-height: calc(100dvh - 32px); overflow-y: auto; overscroll-behavior: contain; }
.skip-link { position: fixed; left: 16px; top: -80px; z-index: 300; padding: 12px 16px; background: var(--surface-raised); border-radius: var(--radius-md); }
.skip-link:focus { top: 12px; }
.subnav { border-bottom: 1px solid var(--border-subtle); background: var(--surface-sunken); }
.subnav > div { display: flex; gap: var(--space-6); max-width: 1440px; margin: 0 auto; padding: 0 var(--space-8); overflow-x: auto; }
.subnav a { text-decoration: none; white-space: nowrap; font-size: var(--text-sm); color: var(--text-secondary); padding: var(--space-3) 0; border-bottom: 2px solid transparent; }
.subnav a[aria-current] { color: var(--text-primary); border-bottom-color: var(--accent-primary); }
@media (max-width: 720px) { .subnav > div { padding-inline: var(--space-4); } }
/* View-transition tuning — cross-fade routes, ease the shared nav pill. */
:global(::view-transition-old(root)),