Unify UI workflows and streamline fleet management
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Tabs from './primitives/Tabs.svelte';
|
||||
import ListFooter from './primitives/ListFooter.svelte';
|
||||
import { mapLabel } from '$lib/script-library.js';
|
||||
import { goto } from '$app/navigation';
|
||||
import SectionHeader from './primitives/SectionHeader.svelte';
|
||||
import Button from './primitives/Button.svelte';
|
||||
@@ -22,7 +25,9 @@
|
||||
let game = $state('');
|
||||
let scope = $state('');
|
||||
let status = $state('');
|
||||
let view = $state<'library' | 'inspect'>('library');
|
||||
let view = $state('library');
|
||||
let limit = $state(25);
|
||||
$effect(() => { query; game; scope; status; limit = 25; });
|
||||
let dialog: HTMLDialogElement;
|
||||
let kind = $state<'baseline' | 'config'>('config');
|
||||
let name = $state('');
|
||||
@@ -94,15 +99,12 @@
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<SectionHeader title="Configs" eyebrow="Fleet configuration" subtitle="Build on a baseline. Fine-tune with overlays. Know where every value comes from.">
|
||||
<SectionHeader title="Configs" subtitle="Manage game baselines and scoped changes. Inspect the settings applied to a server.">
|
||||
{#snippet actions()}<Button variant="primary" onclick={() => openCreate()}>+ New config</Button>{/snippet}
|
||||
</SectionHeader>
|
||||
<div class="overview">
|
||||
<div><span class="metric">{baselines.length}</span><span>Game baselines<small>The starting point for your servers</small></span></div>
|
||||
<div><span class="metric">{overlays.length}</span><span>Scoped overlays<small>Changes for a game or map</small></span></div>
|
||||
<div><span class="metric" class:pending={pending > 0}>{pending}</span><span>Awaiting promotion<small>Latest versions outside the promoted set</small></span></div>
|
||||
</div>
|
||||
<div class="navigation"><div class="view-switch" aria-label="Configs view"><button class:active={view === 'library'} aria-pressed={view === 'library'} onclick={() => view = 'library'}>Library</button><button class:active={view === 'inspect'} aria-pressed={view === 'inspect'} onclick={() => view = 'inspect'}>Inspect a server <span aria-hidden="true">↗</span></button></div><Button variant="ghost" size="sm" onclick={load} loading={loading}>Refresh library</Button></div>
|
||||
<div class="ui-summary"><span><strong>{baselines.length}</strong> baselines</span><span><strong>{overlays.length}</strong> overlays</span><span><strong>{pending}</strong> awaiting promotion</span></div>
|
||||
<div class="navigation"><Tabs tabs={[{id:'library',label:'Library'},{id:'inspect',label:'Inspect a server'}]} bind:value={view} ariaLabel="Config views" idPrefix="config-tabs" panelId="config-panel" /><Button variant="ghost" size="sm" onclick={load} loading={loading}>Refresh</Button></div>
|
||||
<div id="config-panel" role="tabpanel" aria-labelledby={'config-tabs-' + view} class="config-panel">
|
||||
{#if error}<div class="error" role="alert">{error} <Button size="sm" onclick={load}>Retry</Button></div>{/if}
|
||||
{#if view === 'inspect'}
|
||||
<ConfigInspector />
|
||||
@@ -128,14 +130,16 @@
|
||||
{/if}
|
||||
{#if filteredOverlays.length}
|
||||
<section class="section"><div class="section-heading"><h2>Overlays <span>{filteredOverlays.length}</span></h2><p>Small changes with a clear scope</p></div>
|
||||
<div class="overlay-list">{#each filteredOverlays as artifact (artifact.id)}
|
||||
<a class="overlay-row" href="/configs/{artifact.id}"><div class="overlay-main"><span class="overlay-icon" aria-hidden="true">≋</span><div><h3>{artifact.name}</h3><span class="scope-label">{artifact.scope_type ?? 'Unscoped'}{artifact.scope_value ? ` · ${artifact.scope_value}` : ''}</span></div></div><div class="versions">{@render versionBadges(artifact)}</div><span class="servers">{artifact.servers.length} server{artifact.servers.length === 1 ? '' : 's'}</span><span class="open-arrow" aria-hidden="true">↗</span></a>
|
||||
<div class="overlay-list">{#each filteredOverlays.slice(0,limit) as artifact (artifact.id)}
|
||||
<a class="overlay-row" href="/configs/{artifact.id}"><div class="overlay-main"><span class="overlay-icon" aria-hidden="true">≋</span><div><h3>{artifact.name}</h3><span class="scope-label">{artifact.scope_type ?? 'Unscoped'}{artifact.scope_value ? ` · ${artifact.scope_type === 'map' ? mapLabel(artifact.scope_value) : artifact.scope_value.toUpperCase()}` : ''}</span></div></div><div class="versions">{@render versionBadges(artifact)}</div><span class="servers">{artifact.servers.length} server{artifact.servers.length === 1 ? '' : 's'}</span><span class="open-arrow" aria-hidden="true">↗</span></a>
|
||||
{/each}</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
<ListFooter total={filteredOverlays.length} bind:limit label="overlays" />
|
||||
{#if artifacts.length}<p class="footnote">Choose a config to edit and preview a draft, or inspect a server to trace its promoted settings.</p>{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#snippet versionBadges(artifact: Artifact)}
|
||||
@@ -163,14 +167,11 @@
|
||||
|
||||
<style>
|
||||
.page { display: flex; flex-direction: column; gap: var(--space-6); }
|
||||
.overview { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); background: var(--surface); }
|
||||
.overview > div { display: flex; align-items: center; gap: 18px; padding: 24px; } .overview > div + div { border-left: 1px solid var(--border-subtle); }
|
||||
.metric { font-size: 32px; font-weight: 600; letter-spacing: -.04em; } .metric.pending { color: var(--state-warning); }
|
||||
.overview > div > span:last-child { font-size: var(--text-sm); } small { display: block; color: var(--text-secondary); font-size: 11px; margin-top: 5px; }
|
||||
.navigation, .view-switch, .section-heading, .card-top, .card-footer, .overlay-row, .overlay-main, .versions { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
.navigation, .section-heading, .card-top, .card-footer, .overlay-row, .overlay-main, .versions { display: flex; align-items: center; gap: 12px; }
|
||||
.navigation, .section-heading, .card-top, .card-footer { justify-content: space-between; }
|
||||
.navigation { border-bottom: 1px solid var(--border-subtle); padding-bottom: 12px; }
|
||||
.view-switch { gap: 4px; }
|
||||
|
||||
button { background: transparent; color: var(--text-secondary); font: var(--text-sm) var(--font-body); border: 1px solid transparent; border-radius: var(--radius-sm); padding: 10px 14px; cursor: pointer; }
|
||||
button.active { color: var(--text-primary); background: var(--surface-raised); } button span { margin-left: 8px; }
|
||||
button:disabled { opacity: .5; cursor: default; }
|
||||
@@ -190,7 +191,7 @@
|
||||
.overlay-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.overlay-row { text-decoration: none; color: var(--text-primary); background: var(--surface); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 18px 20px; }
|
||||
.overlay-main { flex: 1; min-width: 0; } .overlay-main h3 { margin: 0 0 5px; font-size: var(--text-sm); }
|
||||
.overlay-icon { display: grid; place-items: center; width: 36px; height: 36px; flex-shrink: 0; border-radius: 8px; background: var(--surface-raised); color: var(--accent-primary-hover); font-size: 24px; }
|
||||
.overlay-icon { display: grid; place-items: center; width: 36px; height: 36px; flex-shrink: 0; border-radius: var(--radius-md); background: var(--surface-raised); color: var(--accent-primary-hover); font-size: 24px; }
|
||||
.scope-label, .servers { color: var(--text-secondary); font-size: var(--text-xs); } .servers { min-width: 80px; text-align: right; }
|
||||
.footnote { margin: 0; color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
.loading { padding: 60px 0; text-align: center; color: var(--text-secondary); }
|
||||
@@ -210,6 +211,11 @@
|
||||
label { display: grid; gap: 8px; font-size: var(--text-xs); color: var(--text-secondary); }
|
||||
.import { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; } .import input { flex: 1; }
|
||||
.dialog-actions { flex-wrap: wrap; } .dialog-actions p { flex: 1 1 240px; font-size: var(--text-xs); color: var(--text-secondary); }
|
||||
@media (max-width: 900px) { .overview > div { padding: 16px; gap: 12px; } .overview small { display: none; } .overlay-row { flex-wrap: wrap; } .overlay-main { flex-basis: 50%; } }
|
||||
@media (max-width: 600px) { .overview { grid-template-columns: 1fr; } .overview > div + div { border-left: 0; border-top: 1px solid var(--border-subtle); } .overview > div { padding: 12px 16px; } .metric { font-size: 24px; min-width: 30px; } .overview small { display: block; } .filters select { flex: 1; } .section-heading p { display: none; } .overlay-row > .versions { order: 3; width: 100%; } .servers { min-width: auto; } .overlay-row > .open-arrow { display: none; } .form-grid { grid-template-columns: 1fr; } form { padding: 18px; } .navigation { gap: 4px; flex-wrap: wrap; } }
|
||||
@media (max-width: 900px) { .overlay-row { flex-wrap: wrap; } .overlay-main { flex-basis: 50%; } }
|
||||
@media (max-width: 600px) { .filters select { flex: 1; } .section-heading p { display: none; } .overlay-row > .versions { order: 3; width: 100%; } .servers { min-width: auto; } .overlay-row > .open-arrow { display: none; } .form-grid { grid-template-columns: 1fr; } form { padding: 18px; } .navigation { gap: 4px; flex-wrap: wrap; } }
|
||||
.config-panel { display:grid; gap:20px; }
|
||||
.baseline-card { padding:16px; border-radius:var(--radius-md); background:var(--surface); }
|
||||
.card-footer { padding-top:12px; margin-top:12px; }
|
||||
.welcome { padding:24px; }
|
||||
.loading { padding:24px 0; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user