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
@@ -153,7 +153,7 @@
/* Loading */
.is-loading .label,
.is-loading .slot { visibility: hidden; }
.is-loading .slot { opacity: 0; }
.spinner {
position: absolute;
width: 14px; height: 14px;
+17 -3
View File
@@ -18,14 +18,28 @@
get messageId() { return error || hint ? `${id}-message` : undefined; },
get invalid() { return !!error; }
});
function connectLabel(node: HTMLDivElement) {
const connect = () => {
const label = node.querySelector<HTMLLabelElement>('label.label');
const control = node.querySelector<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>('input:not([type="hidden"]), select, textarea');
if (!label || !control || forId) return;
if (!control.id) control.id = `${id}-control`;
label.htmlFor = control.id;
};
connect();
const observer = new MutationObserver(connect);
observer.observe(node, { childList: true, subtree: true });
return { destroy: () => observer.disconnect() };
}
</script>
<div class="field">
<div class="field" use:connectLabel>
{#if label}
<svelte:element this={forId ? 'label' : 'div'} class="label" id={`${id}-label`} for={forId}>
<label class="label" id={`${id}-label`} for={forId ?? `${id}-control`}>
{label}
{#if required}<span class="req" aria-hidden="true">*</span>{/if}
</svelte:element>
</label>
{/if}
<div class="control" class:has-error={!!error}>
{@render children?.()}
@@ -63,7 +63,7 @@
width: 100%;
padding: 9px 12px;
background: var(--surface-sunken);
border: 1px solid var(--border-subtle);
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
color: var(--text-primary);
font-family: var(--font-body);
@@ -77,7 +77,7 @@
.input.mono { font-family: var(--font-mono); }
.input::placeholder { color: var(--text-quaternary); }
.input:hover { border-color: var(--border-strong); }
.input:hover { border-color: var(--border-control-hover); }
.input:focus {
outline: none;
border-color: var(--accent-primary);
@@ -1,8 +1,8 @@
<script lang="ts">
import Button from './Button.svelte';
let { total, limit = $bindable(25), step = 25, label = 'items' }: { total: number; limit?: number; step?: number; label?: string } = $props();
let { total, limit = $bindable(25), step = 25, label = 'items', onchange }: { total: number; limit?: number; step?: number; label?: string; onchange?: (limit: number) => void } = $props();
</script>
{#if total > step}
<div class="list-footer"><span role="status">Showing {Math.min(limit, total)} of {total} {label}</span>{#if limit < total}<Button size="sm" onclick={() => limit += step}>Show {Math.min(step, total - limit)} more</Button>{/if}</div>
<div class="list-footer"><span role="status">Showing {Math.min(limit, total)} of {total} {label}</span>{#if limit < total}<Button size="sm" onclick={() => { limit += step; onchange?.(limit); }}>Show {Math.min(step, total - limit)} more</Button>{/if}</div>
{/if}
<style>.list-footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding-block: var(--space-3); color: var(--text-secondary); font-size: var(--text-sm); }</style>
@@ -69,7 +69,7 @@
appearance: none;
padding: 9px 32px 9px 12px;
background: var(--surface-sunken);
border: 1px solid var(--border-subtle);
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
color: var(--text-primary);
font-family: var(--font-body);
@@ -81,7 +81,7 @@
background-color var(--dur-fast) var(--ease-standard),
box-shadow var(--dur-fast) var(--ease-standard);
}
.select:hover { border-color: var(--border-strong); }
.select:hover { border-color: var(--border-control-hover); }
.select:focus {
outline: none;
border-color: var(--accent-primary);
+28 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { tick } from 'svelte';
interface Tab {
id: string;
label: string;
@@ -18,6 +19,32 @@
const uid = $props.id();
let { tabs, value = $bindable(), onchange, ariaLabel, size = 'md', panelId, idPrefix = uid }: Props = $props();
let tablist = $state<HTMLDivElement | null>(null);
// Keep a deep-linked or keyboard-selected tab visible without scrolling the page.
$effect(() => {
const selected = value;
const node = tablist;
if (!node) return;
let cancelled = false;
let observer: ResizeObserver | undefined;
void tick().then(() => {
if (cancelled) return;
const tab = document.getElementById(`${idPrefix}-${selected}`);
if (!tab) return;
const reveal = () => {
const bounds = node.getBoundingClientRect();
const target = tab.getBoundingClientRect();
if (target.left < bounds.left + 4) node.scrollLeft -= bounds.left + 4 - target.left;
else if (target.right > bounds.right - 4) node.scrollLeft += target.right - bounds.right + 4;
};
reveal();
observer = new ResizeObserver(reveal);
observer.observe(node);
observer.observe(tab);
});
return () => { cancelled = true; observer?.disconnect(); };
});
function onKey(event: KeyboardEvent) {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
@@ -38,7 +65,7 @@
}
</script>
<div class="tabs s-{size}" role="tablist" aria-label={ariaLabel}>
<div class="tabs s-{size}" role="tablist" aria-label={ariaLabel} bind:this={tablist}>
{#each tabs as t (t.id)}
<button
type="button"
@@ -54,7 +54,7 @@
width: 100%;
padding: var(--space-3) var(--space-3);
background: var(--surface-sunken);
border: 1px solid var(--border-subtle);
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
color: var(--text-primary);
font-family: var(--font-body);
@@ -68,7 +68,7 @@
}
.textarea.mono { font-family: var(--font-mono); font-size: var(--text-xs); line-height: var(--leading-mono); }
.textarea::placeholder { color: var(--text-quaternary); }
.textarea:hover { border-color: var(--border-strong); }
.textarea:hover { border-color: var(--border-control-hover); }
.textarea:focus {
outline: none;
border-color: var(--accent-primary);