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:
@@ -32,6 +32,8 @@
|
||||
let search = $state('');
|
||||
let mounted = $state(false);
|
||||
let checkedAt = $state<Date | null>(null);
|
||||
let totalsOpen = $state(true);
|
||||
let fullTable = $state(false);
|
||||
let controller: AbortController | undefined;
|
||||
let requestId = 0;
|
||||
let loadedUrl = '';
|
||||
@@ -63,6 +65,7 @@
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
totalsOpen = !window.matchMedia('(max-width: 640px)').matches;
|
||||
mounted = true;
|
||||
const refreshVisible = () => { if (!document.hidden && !loading) void refresh(apiUrl); };
|
||||
const interval = setInterval(refreshVisible, 15_000);
|
||||
@@ -170,6 +173,7 @@
|
||||
{/if}
|
||||
|
||||
{#if data}
|
||||
<details class="totals" bind:open={totalsOpen}><summary>Fleet totals · {number.format(data.summary.players)} players · Round {number.format(data.summary.highestRound)} record</summary>
|
||||
<div class="overview" aria-label="Totals for the selected game and map">
|
||||
<div class="record"><span>Highest round</span><strong>{number.format(data.summary.highestRound)}</strong><small>Fleet record</small></div>
|
||||
<div><span>Total kills</span><strong>{number.format(data.summary.kills)}</strong><small>Across all players</small></div>
|
||||
@@ -177,13 +181,15 @@
|
||||
<div><span>Games tracked</span><strong>{number.format(data.summary.matches)}</strong><small>Includes active games</small></div>
|
||||
<div><span>Players</span><strong>{number.format(data.summary.players)}</strong><small>Across tracked games</small></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<section class="rankings" aria-labelledby="rankings-title" aria-busy={loading}>
|
||||
<section class="rankings" class:compact={!fullTable} aria-labelledby="rankings-title" aria-busy={loading}>
|
||||
<div class="rankings-header">
|
||||
<div><h2 id="rankings-title">{metricLabel}</h2><p>{descriptions[metric]}</p></div>
|
||||
<div class="refresh-status" role="status">{loading ? 'Refreshing…' : checkedAt ? `Checked ${checkedAt.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', second: '2-digit' })}` : ''}<span>Refreshes every 15 seconds</span></div>
|
||||
</div>
|
||||
{#if data.entries.length}
|
||||
<div class="mobile-view"><button class="text-button" aria-pressed={fullTable} onclick={() => fullTable = !fullTable}>{fullTable ? 'Show selected stat only' : 'Show all stats and player IDs'}</button></div>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Keyboard users need to scroll the table.) -->
|
||||
<div class="table-scroll" role="region" aria-label="Player rankings" tabindex="0">
|
||||
<table>
|
||||
@@ -239,12 +245,14 @@
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.totals summary { cursor: pointer; padding: 10px 0; font-size: var(--text-sm); color: var(--text-secondary); }
|
||||
.mobile-view { display: none; }
|
||||
.page { display: flex; flex-direction: column; gap: var(--space-5); }
|
||||
.filters { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(180px, 1fr) minmax(180px, 1fr) minmax(230px, 1.25fr); gap: var(--space-4); padding: var(--space-5); background: var(--surface); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); }
|
||||
.field, .search { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
|
||||
label { color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
input, select, button { font: inherit; }
|
||||
input, select { min-height: 42px; width: 100%; padding: 9px 12px; background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: var(--radius-md); color: var(--text-primary); font-size: var(--text-sm); }
|
||||
input, select { min-height: 42px; width: 100%; padding: 9px 12px; background: var(--surface-sunken); border: 1px solid var(--border-control); border-radius: var(--radius-md); color: var(--text-primary); font-size: var(--text-sm); }
|
||||
input::placeholder { color: var(--text-tertiary); }
|
||||
.search > div { display: flex; gap: var(--space-2); }
|
||||
.search input { flex: 1; min-width: 0; width: 0; }
|
||||
@@ -298,6 +306,14 @@
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
||||
@media (max-width: 1000px) { .filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } .overview > div { padding: var(--space-4); } .overview small { display: none; } }
|
||||
@media (max-width: 640px) {
|
||||
.mobile-view { display: block; padding: 0 16px 12px; }
|
||||
.compact .stat:not(.selected) { display: none; }
|
||||
.compact .player { min-width: 110px; max-width: 160px; position: static; }
|
||||
.compact .identity :global(.copy) { display: none; }
|
||||
.compact .stat { white-space: normal; }
|
||||
.compact .rank { width: 44px; }
|
||||
.compact th, .compact td { padding: 10px 8px; }
|
||||
.compact thead button { white-space: normal; text-align: right; }
|
||||
.page { gap: var(--space-4); }
|
||||
.filters { padding: var(--space-4); gap: var(--space-3); }
|
||||
.search { grid-column: 1 / -1; }
|
||||
|
||||
Reference in New Issue
Block a user