Improve operational UX, recovery flows, and action safety
Implement the approved UX review across Fleet, Betas, Library, deployments, and Settings. Add reviewed promotion and reconciliation plans, atomic queued-removal cancellation, recoverable beta drafts and launch inputs, accessible controls, and mobile improvements. Validated with 473 tests, a clean typecheck, production build, and isolated desktop/mobile browser walkthroughs. Include the review and implementation coverage notes.
This commit is contained in:
@@ -3,13 +3,14 @@
|
||||
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 { afterNavigate, goto } from '$app/navigation';
|
||||
import SectionHeader from './primitives/SectionHeader.svelte';
|
||||
import Button from './primitives/Button.svelte';
|
||||
import Badge from './primitives/Badge.svelte';
|
||||
import EmptyState from './primitives/EmptyState.svelte';
|
||||
import ConfigTextEditor from './ConfigTextEditor.svelte';
|
||||
import ConfigInspector from './ConfigInspector.svelte';
|
||||
import { libraryLimit, saveLibraryContext } from '$ui/library-context.js';
|
||||
|
||||
interface Version { id: number; version: number; state: 'untested' | 'tested' | 'test-failed'; uploaded_at: string; }
|
||||
interface Artifact {
|
||||
@@ -27,7 +28,11 @@
|
||||
let status = $state('');
|
||||
let view = $state('library');
|
||||
let limit = $state(25);
|
||||
$effect(() => { query; game; scope; status; limit = 25; });
|
||||
let mounted = $state(false);
|
||||
afterNavigate(() => { mounted = true; });
|
||||
let lastFilters = '';
|
||||
$effect(() => { const key = JSON.stringify([query, game, scope, status]); if (!mounted) return; if (lastFilters && lastFilters !== key) limit = 25; lastFilters = key; });
|
||||
$effect(() => { if (mounted) saveLibraryContext({ q: query, game, scope, status, view, limit }, { view: 'library', limit: 25 }); });
|
||||
let dialog: HTMLDialogElement;
|
||||
let kind = $state<'baseline' | 'config'>('config');
|
||||
let name = $state('');
|
||||
@@ -67,7 +72,7 @@
|
||||
} catch (err) { error = (err as Error).message; }
|
||||
finally { loading = false; }
|
||||
}
|
||||
onMount(load);
|
||||
onMount(() => { const params = new URLSearchParams(window.location.search); query = params.get('q') ?? ''; game = params.get('game') ?? ''; scope = params.get('scope') ?? ''; status = params.get('status') ?? ''; view = params.get('view') === 'inspect' ? 'inspect' : 'library'; limit = libraryLimit(params); void load(); });
|
||||
function openCreate(type: 'config' | 'baseline' = 'config') { kind = type; createError = ''; dialog.showModal(); }
|
||||
function resetFilters() { query = ''; game = ''; scope = ''; status = ''; }
|
||||
async function importFile(event: Event) {
|
||||
@@ -102,20 +107,20 @@
|
||||
<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="ui-summary"><span><strong>{baselines.length}</strong> baselines</span><span><strong>{overlays.length}</strong> overlays</span><span><strong>{pending}</strong> awaiting promotion</span></div>
|
||||
{#if artifacts.length}<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>{/if}
|
||||
<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 />
|
||||
{:else}
|
||||
<div class="filters">
|
||||
{#if loading || artifacts.length || error}<div class="filters">
|
||||
<input class="search" aria-label="Search configs" bind:value={query} placeholder="Search configs, maps, or servers…" />
|
||||
<select aria-label="Filter by game" bind:value={game}><option value="">All games</option>{#each gameOptions as option}<option value={option}>{option.toUpperCase()}</option>{/each}</select>
|
||||
<select aria-label="Filter by scope" bind:value={scope}><option value="">All scopes</option><option value="game">Game</option><option value="map">Map</option></select>
|
||||
<select aria-label="Filter by status" bind:value={status}><option value="">All statuses</option><option value="pending">Awaiting promotion</option><option value="promoted">Has promoted version</option><option value="beta">Active beta</option><option value="failed">Test failed</option></select>
|
||||
{#if query || game || scope || status}<Button size="sm" variant="ghost" onclick={resetFilters}>Clear</Button>{/if}
|
||||
</div>
|
||||
</div>{/if}
|
||||
{#if loading}<div class="loading" role="status">Loading your config library…</div>
|
||||
{:else if !artifacts.length && !error}
|
||||
<div class="welcome"><span class="eyebrow">Start with a foundation</span><h2>Your fleet, configured your way.</h2><p>Add a baseline for each game, then layer on small, focused changes for a game or map. Every change gets its own version.</p><Button variant="primary" onclick={() => openCreate('baseline')}>Create your first baseline</Button></div>
|
||||
@@ -218,4 +223,5 @@
|
||||
.card-footer { padding-top:12px; margin-top:12px; }
|
||||
.welcome { padding:24px; }
|
||||
.loading { padding:24px 0; }
|
||||
input:not([type='checkbox']):not([type='file']), select { border-color: var(--border-control); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user