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:
2026-09-19 21:34:34 -04:00
parent 940f81e6ca
commit 8ac420fe61
83 changed files with 2574 additions and 1190 deletions
+5 -1
View File
@@ -5,6 +5,7 @@
import ConfigTextEditor from './ConfigTextEditor.svelte';
import ConfigInspector from './ConfigInspector.svelte';
import Button from './primitives/Button.svelte';
import { configWarnings } from '$lib/config-workbench.js';
interface Version { id: number; version: number; notes: string | null; }
let { id, kind, versions, currentVersionId, runningBetaId, scopeType, scopeValue, onsaved }: {
id: number; kind: 'baseline' | 'config'; versions: Version[]; currentVersionId: number | null;
@@ -23,6 +24,7 @@
let loadedVersion = $state('');
let recovered = $state<{ version: string; text: string; notes: string } | null>(null);
const dirty = $derived(text !== original || notes.trim().length > 0);
const ignoredWarnings = $derived(configWarnings(text, kind === 'config').filter(warning => warning.includes('ignored') || warning.includes('no value')));
const storageKey = $derived(`electric-cherry.config-draft.${id}`);
onMount(() => {
selected = String(versions[0]?.id ?? ''); ready = true;
@@ -117,7 +119,7 @@
{:else if loadedVersion}
<ConfigTextEditor bind:text overlay={kind === 'config'} disabled={saving || runningBetaId !== null} />
<label class="notes-label">Version notes<input bind:value={notes} placeholder="What changed, and why?" disabled={saving || runningBetaId !== null} /></label>
<div class="actions ui-sticky-actions"><span class="muted">{runningBetaId !== null ? 'Finish the active beta to save another version.' : 'A new version must pass through a beta before promotion.'}</span>
<div class="actions ui-sticky-actions"><span class="muted">{runningBetaId !== null ? 'Finish the active beta to save another version.' : 'A new version must pass through a beta before promotion.'}{#if ignoredWarnings.length}<strong class="save-warning">{ignoredWarnings.length} ignored or incomplete lines — review the open configuration notes before saving.</strong>{/if}</span>
<Button variant="ghost" size="sm" onclick={discard} disabled={!dirty || saving}>Discard</Button>
<Button variant="primary" onclick={save} loading={saving} disabled={!dirty || !text.trim() || runningBetaId !== null}>Save new version</Button>
</div>
@@ -144,8 +146,10 @@
.actions { margin-top: 16px; }
.actions > span { flex: 1 1 200px; }
.muted { color: var(--text-secondary); font-size: var(--text-xs); }
.save-warning { display: block; color: var(--state-warning); font-weight: 500; margin-top: 6px; }
.recovery, .success, .error { font-size: var(--text-sm); padding: 12px; border-radius: var(--radius-sm); background: var(--surface-raised); margin-bottom: 16px; }
.recovery { color: var(--state-warning); } .success { color: var(--state-online); } .error { color: var(--state-danger); }
@media (max-width: 1100px) { .workbench { grid-template-columns: 1fr; } }
@media (max-width: 600px) { .editor-panel { padding: 16px; } }
input:not([type='checkbox']):not([type='file']), select { border-color: var(--border-control); }
</style>