Standardize UI typography, charts and empty states

This commit is contained in:
2026-10-07 19:40:02 -04:00
parent 05bdeaf624
commit 0ed3cd6b47
57 changed files with 417 additions and 382 deletions
+18 -18
View File
@@ -218,9 +218,9 @@
<style>
.builder { display: grid; gap: 16px; }
h4, h5, p, dl, dd { margin: 0; }
h4 { font-size: 19px; font-weight: 600; }
h5 { font-size: 15px; font-weight: 550; line-height: 1.5; }
p { color: var(--text-secondary); font-size: 13px; line-height: 1.6; }
h4 { font-size: var(--text-lg); font-weight: 600; }
h5 { font-size: var(--text-base); font-weight: 550; line-height: 1.5; }
p { color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; }
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.eyebrow { display: block; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .1em; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
.pill { padding: 5px 9px; border-radius: 99px; font-size: var(--text-xs); color: var(--text-secondary); border: 1px solid var(--border-strong); white-space: nowrap; }
@@ -229,32 +229,32 @@
.preset:hover { border-color: var(--accent-primary-hover); }
.preset.selected { border-color: var(--accent-primary); background: color-mix(in oklab, var(--accent-primary) 10%, var(--surface-sunken)); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent-primary) 30%, transparent); }
.preset-top { display: flex; align-items: center; justify-content: space-between; }
.preset-number, .step-number { font-family: var(--font-mono); color: var(--text-secondary); font-size: 12px; }
.choice { width: 19px; height: 19px; border: 1px solid var(--border-strong); border-radius: 50%; display: grid; place-items: center; font-size: 12px; }
.preset-number, .step-number { font-family: var(--font-numeric); color: var(--text-secondary); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
.choice { width: 19px; height: 19px; border: 1px solid var(--border-strong); border-radius: 50%; display: grid; place-items: center; font-size: var(--text-xs); }
.selected .choice { background: var(--accent-primary); color: var(--text-on-accent); border-color: var(--accent-primary); }
.preset > strong { font-size: 15px; }
.preset-description { font-size: 12px; line-height: 1.6; color: var(--text-secondary); }
.preset ul { padding-left: 16px; margin: 2px 0 10px; font-size: 12px; line-height: 1.65; flex: 1; }
.preset > strong { font-size: var(--text-base); }
.preset-description { font-size: var(--text-xs); line-height: 1.6; color: var(--text-secondary); }
.preset ul { padding-left: 16px; margin: 2px 0 10px; font-size: var(--text-xs); line-height: 1.65; flex: 1; }
.preset li + li { margin-top: 5px; }
.preset li::marker { color: var(--accent-primary-hover); }
.preset-footer { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; border-top: 1px solid var(--border-subtle); padding-top: 12px; font-size: var(--text-xs); color: var(--text-secondary); }
.preset-footer > span { color: var(--accent-primary-hover); }
.feedback { color: var(--accent-primary-hover); font-size: 12px; min-height: 18px; }
.feedback { color: var(--accent-primary-hover); font-size: var(--text-xs); min-height: 18px; }
.error { color: var(--state-danger); }
.active-plan { display: grid; gap: 18px; padding: 22px; background: color-mix(in oklab, var(--surface-raised) 25%, var(--surface)); border: 1px solid var(--border-strong); border-radius: var(--radius-md); }
.plan-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; padding: 16px 0; border-top: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle); }
dt { color: var(--text-secondary); font-size: var(--text-xs); margin-bottom: 6px; }
dd { font-size: 13px; font-weight: 550; line-height: 1.5; }
dd { font-size: var(--text-sm); font-weight: 550; line-height: 1.5; }
dd small { font-size: var(--text-xs); font-weight: 400; color: var(--text-secondary); }
.plan-caption { font-size: 12px; }
.telemetry-note { border-left: 2px solid var(--accent-primary); background: color-mix(in oklab, var(--accent-primary) 6%, transparent); padding: 10px 12px; font-size: 12px; }
.plan-caption { font-size: var(--text-xs); }
.telemetry-note { border-left: 2px solid var(--accent-primary); background: color-mix(in oklab, var(--accent-primary) 6%, transparent); padding: 10px 12px; font-size: var(--text-xs); }
.telemetry-note span { color: var(--accent-primary-hover); margin-right: 5px; }
.requirements { display: grid; gap: 10px; }
.requirement { border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--surface-sunken); min-width: 0; overflow: hidden; }
.requirement-heading { display: flex; align-items: center; gap: 14px; padding: 16px; }
.requirement-copy { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.requirement-copy .eyebrow { font-size: var(--text-xs); margin-bottom: 4px; }
.remove { padding: 3px 8px; font-size: 21px; background: transparent; border: 0; color: var(--text-secondary); }
.remove { padding: 3px 8px; font-size: var(--text-lg); background: transparent; border: 0; color: var(--text-secondary); }
.remove:hover { color: var(--state-danger); }
.editor { border-top: 1px solid var(--border-subtle); }
.editor > summary { padding: 10px 16px; font-size: var(--text-xs); color: var(--accent-primary-hover); }
@@ -262,24 +262,24 @@
.editor-fields { display: grid; gap: 12px; padding: 6px 16px 16px; }
.fields, .add-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: end; }
.add-row label { flex: 1 1 220px; }
label { display: grid; gap: 7px; flex: 1 1 180px; min-width: 0; font-size: 12px; color: var(--text-secondary); }
label { display: grid; gap: 7px; flex: 1 1 180px; min-width: 0; font-size: var(--text-xs); color: var(--text-secondary); }
input, select, button { color: var(--text-primary); background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: 6px; padding: 10px 12px; min-width: 0; font: inherit; }
input, select { width: 100%; font-size: 13px; min-height: 40px; box-sizing: border-box; border-color: var(--border-control); }
input, select { width: 100%; font-size: var(--text-sm); min-height: 40px; box-sizing: border-box; border-color: var(--border-control); }
button { cursor: pointer; }
button:disabled { opacity: .5; cursor: default; }
button:focus-visible, summary:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--accent-primary-hover); outline-offset: 3px; }
.secondary { background: var(--surface-raised); font-size: 12px; min-height: 40px; }
.secondary { background: var(--surface-raised); font-size: var(--text-xs); min-height: 40px; }
.secondary:hover:not(:disabled) { border-color: var(--accent-primary-hover); }
.policy-settings { display: grid; gap: 8px; }
.policy { border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
.policy > summary { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 18px; font-size: 13px; font-weight: 550; }
.policy > summary { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 18px; font-size: var(--text-sm); font-weight: 550; }
.policy small { display: block; font-size: var(--text-xs); color: var(--text-secondary); font-weight: 400; line-height: 1.6; margin-top: 5px; }
.edit-hint { color: var(--accent-primary-hover); font-size: var(--text-xs); }
.policy-body { padding: 0 18px 18px; display: grid; gap: 12px; }
.policy-body.fields { display: flex; }
summary { cursor: pointer; }
.empty-plan { padding: 22px; text-align: center; border: 1px dashed var(--border-strong); border-radius: var(--radius-md); }
.empty-plan strong { display: block; margin-bottom: 8px; font-size: 14px; }
.empty-plan strong { display: block; margin-bottom: 8px; font-size: var(--text-base); }
@media (max-width: 850px) { .presets { grid-template-columns: 1fr; } .preset { padding: 16px; } .plan-facts { grid-template-columns: 1fr; gap: 12px; } }
@media (max-width: 480px) { .active-plan { padding: 14px; } .requirement-heading { padding: 12px; gap: 10px; } .editor > summary > span { display: block; margin: 5px 0 0; } .add-row { align-items: stretch; } .add-row button { width: 100%; } }
@media (prefers-reduced-motion: reduce) { .preset { transition: none; } }
@@ -95,6 +95,6 @@
<style>
.files, label { display:grid; gap:12px; } .files { margin:20px 0; }
.files label { padding:16px; border:1px solid var(--border-subtle); border-radius:var(--radius-md); }
p { line-height:1.6; } code, .hash { overflow-wrap:anywhere; } .hash, label span { color:var(--text-secondary); font-size:13px; }
p { line-height:1.6; } code, .hash { overflow-wrap:anywhere; } .hash, label span { color:var(--text-secondary); font-size: var(--text-sm); }
li { margin:8px 0; } textarea { width:100%; } section { margin-top:24px; }
</style>
@@ -157,16 +157,16 @@
.server-detail { background:var(--surface); }
.deployment-details,.deployment-links { display:flex; flex-wrap:wrap; gap:8px 20px; margin:12px 0; font-size:var(--text-sm); }
@media(max-width:720px) { table { min-width:620px; } }
section { display:grid; gap:16px; } h2,p { margin:0; } h2 { font-size:18px; }
p,span,time { font-size:13px; line-height:1.6; color:var(--text-secondary); } a { color:var(--accent-primary-hover); }
section { display:grid; gap:16px; } h2,p { margin:0; } h2 { font-size: var(--text-md); }
p,span,time { font-size: var(--text-sm); line-height:1.6; color:var(--text-secondary); } a { color:var(--accent-primary-hover); }
.heading,.actions,.filters { display:flex; flex-wrap:wrap; align-items:center; gap:12px; } .heading { justify-content:space-between; }
.server { border:1px solid var(--border-subtle); border-radius:var(--radius-md); padding:16px; }
.server > summary { display:flex; flex-wrap:wrap; gap:8px 16px; } summary { cursor:pointer; font-size:14px; } .server > summary::before { content:'▸'; } .server[open] > summary::before { content:'▾'; }
.server > summary { display:flex; flex-wrap:wrap; gap:8px 16px; } summary { cursor:pointer; font-size: var(--text-base); } .server > summary::before { content:'▸'; } .server[open] > summary::before { content:'▾'; }
.requirement { display:grid; gap:8px; margin-top:20px; border-top:1px solid var(--border-subtle); padding-top:16px; } .result { font-weight:600; margin-right:6px; color:inherit; }
progress { width:100%; height:8px; accent-color:var(--state-online); } .failed { color:var(--state-danger); }
.observation,.guidance { margin-top:12px; } .evidence-link { justify-self:start; padding:6px 0; color:var(--accent-primary-hover); border:0; background:transparent; cursor:pointer; font-size:12px; }
.observation,.guidance { margin-top:12px; } .evidence-link { justify-self:start; padding:6px 0; color:var(--accent-primary-hover); border:0; background:transparent; cursor:pointer; font-size: var(--text-xs); }
.signoff { display:flex; gap:12px; align-items:end; flex-wrap:wrap; } .signoff label { flex:1 1 240px; }
label { display:grid; gap:6px; font-size:13px; color:var(--text-secondary); } input,select,textarea { min-height:40px; padding:8px 10px; border:1px solid var(--border-control); border-radius:6px; background:var(--surface-sunken); color:var(--text-primary); font:inherit; max-width:100%; } textarea { resize:vertical; width:100%; box-sizing:border-box; }
label { display:grid; gap:6px; font-size: var(--text-sm); color:var(--text-secondary); } input,select,textarea { min-height:40px; padding:8px 10px; border:1px solid var(--border-control); border-radius:6px; background:var(--surface-sunken); color:var(--text-primary); font:inherit; max-width:100%; } textarea { resize:vertical; width:100%; box-sizing:border-box; }
.filters { margin:16px 0; align-items:end; } .filters label { flex:1 1 180px; min-width:0; }
.evidence { display:grid; gap:8px; padding:16px 0; border-bottom:1px solid var(--border-subtle); } .evidence-section { scroll-margin-top:80px; } pre { white-space:pre-wrap; overflow-wrap:anywhere; font-size:12px; } .evidence-section > p { margin-top:12px; }
.evidence { display:grid; gap:8px; padding:16px 0; border-bottom:1px solid var(--border-subtle); } .evidence-section { scroll-margin-top:80px; } pre { white-space:pre-wrap; overflow-wrap:anywhere; font-size: var(--text-xs); } .evidence-section > p { margin-top:12px; }
</style>
@@ -98,20 +98,20 @@
</dialog>
<style>
.validation { padding: 12px; border-left: 3px solid var(--state-warning); color: var(--text-primary); font-size: 14px; line-height: 1.6; }
.validation { padding: 12px; border-left: 3px solid var(--state-warning); color: var(--text-primary); font-size: var(--text-base); line-height: 1.6; }
dialog { width: min(820px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); padding: 0; background: var(--surface); color: var(--text-primary); border: 1px solid var(--border-strong); border-radius: var(--radius-lg); }
dialog::backdrop { background: rgb(0 0 0 / .7); backdrop-filter: blur(5px); }.builder { padding: 28px; display: grid; gap: 20px; }
header, .section-title, .actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }.actions { justify-content: flex-end; }
.eyebrow { display: block; color: var(--text-secondary); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .12em; margin-bottom: 8px; } h2, h3, p { margin: 0; } h2 { font-size: 25px; letter-spacing: -.03em; } h3 { font-size: 16px; }.muted { color: var(--text-secondary); font-size: 13px; line-height: 1.6; }
form, .lineup { display: grid; gap: 18px; }.identity { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } label { display: grid; gap: 7px; font-size: 12px; min-width: 0; } input, select { width: 100%; min-width: 0; box-sizing: border-box; padding: 11px; border: 1px solid var(--border-control); border-radius: var(--radius-md); background: var(--surface-sunken); color: inherit; font: inherit; }
.lineup { border-block: 1px solid var(--border-subtle); padding: 20px 0; }.map-row { display: grid; grid-template-columns: 24px 1fr 1.5fr 80px 24px; gap: 12px; align-items: end; padding: 14px; background: var(--surface-sunken); border-radius: var(--radius-md); }.number { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-secondary); align-self: center; }.count { font-size: 12px; padding: 6px 10px; border: 1px solid var(--border-strong); border-radius: 99px; }.close, .remove { background: none; border: 0; color: var(--text-secondary); font-size: 25px; cursor: pointer; }.remove { padding: 7px 0; }.error { color: var(--state-danger); font-size: 13px; }button:disabled { opacity: .5; cursor: default; }
.eyebrow { display: block; color: var(--text-secondary); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .12em; margin-bottom: 8px; } h2, h3, p { margin: 0; } h2 { font-size: var(--text-xl); letter-spacing: -.03em; } h3 { font-size: var(--text-md); }.muted { color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; }
form, .lineup { display: grid; gap: 18px; }.identity { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } label { display: grid; gap: 7px; font-size: var(--text-xs); min-width: 0; } input, select { width: 100%; min-width: 0; box-sizing: border-box; padding: 11px; border: 1px solid var(--border-control); border-radius: var(--radius-md); background: var(--surface-sunken); color: inherit; font: inherit; }
.lineup { border-block: 1px solid var(--border-subtle); padding: 20px 0; }.map-row { display: grid; grid-template-columns: 24px 1fr 1.5fr 80px 24px; gap: 12px; align-items: end; padding: 14px; background: var(--surface-sunken); border-radius: var(--radius-md); }.number { font-family: var(--font-numeric); font-size: var(--text-xs); color: var(--text-secondary); align-self: center; font-variant-numeric: tabular-nums; }.count { font-size: var(--text-xs); padding: 6px 10px; border: 1px solid var(--border-strong); border-radius: 99px; }.close, .remove { background: none; border: 0; color: var(--text-secondary); font-size: var(--text-xl); cursor: pointer; }.remove { padding: 7px 0; }.error { color: var(--state-danger); font-size: var(--text-sm); }button:disabled { opacity: .5; cursor: default; }
@media (max-width: 620px) {
.builder { padding: 20px; }
header { display: grid; grid-template-columns: minmax(0, 1fr) 44px; align-items: start; }
.close, .remove { min-width: 44px; }
.identity { grid-template-columns: 1fr; }
.map-row { grid-template-columns: minmax(0, 1fr) 44px; }
.number { display: none; }
.number { display: none; font-variant-numeric: tabular-nums; }
.map-row label:first-of-type, .map-row label:nth-of-type(2) { grid-column: 1 / -1; }
.actions { align-items: stretch; flex-direction: column; }
}
+2 -2
View File
@@ -162,7 +162,7 @@
.messages:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent-primary); border-radius: var(--radius-sm); }
ol { list-style: none; padding: 0; margin: 0; }
.message { display: grid; grid-template-columns: 6rem minmax(0, 1fr); align-items: baseline; column-gap: var(--space-4); padding: var(--space-3) 0; }
.message-clock, .number { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.message-clock, .number { font-family: var(--font-numeric); font-variant-numeric: tabular-nums; }
.message-clock { font-size: var(--text-xs); color: var(--text-secondary); white-space: nowrap; }
.message-content { min-width: 0; }
.message-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-1) var(--space-3); }
@@ -170,7 +170,7 @@
.context { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-1) var(--space-3); margin: 0; color: var(--text-secondary); font-size: var(--text-xs); font-weight: var(--weight-regular); }
.context > div { display: flex; align-items: baseline; gap: var(--space-1); white-space: nowrap; }
.context > div + div::before { content: '·'; margin-right: var(--space-1); color: var(--text-tertiary); }
.context dd { margin: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.context dd { margin: 0; font-family: var(--font-numeric); font-variant-numeric: tabular-nums; }
.message p { margin: var(--space-1) 0 0; font-size: var(--text-base); line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
.match-events { padding: var(--space-1) 0; }
.match-divider { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; color: var(--text-secondary); font-size: var(--text-xs); }
+1 -1
View File
@@ -144,7 +144,7 @@
.setting summary::-webkit-details-marker { display: none; }
.setting-main { display: flex; gap: 12px; justify-content: space-between; flex-wrap: wrap; }
code, strong { font: var(--text-xs)/1.6 var(--font-mono); overflow-wrap: anywhere; white-space: pre-wrap; }
strong { color: var(--accent-primary-hover); }
strong { color: var(--text-primary); }
.source-label { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: var(--text-xs); color: var(--text-secondary); margin-top: 6px; }
.source-label::before { content: '▸'; } .setting[open] .source-label::before { content: '▾'; }
b { font-weight: 400; color: var(--state-warning); }
+8 -7
View File
@@ -106,7 +106,7 @@
<div class="page">
<SectionHeader title="Configs" subtitle="Manage game baselines and scoped changes.">
{#snippet actions()}<Button variant="primary" onclick={() => openCreate()}>New config</Button>{/snippet}
{#snippet actions()}{#if view === 'inspect' || loading || artifacts.length || error}<Button variant="primary" onclick={() => openCreate()}>New config</Button>{/if}{/snippet}
</SectionHeader>
<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">
@@ -119,12 +119,16 @@
<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}
{#if filtered.length && (query || game || scope || status)}<Button size="sm" variant="ghost" onclick={resetFilters}>Clear</Button>{/if}
</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>
{:else if !filtered.length && !error}<EmptyState title="No matching configs" description="Try a different search or clear your filters." />
<EmptyState title="No configs yet" description="Configs store game baselines and scoped changes. Create a baseline to start managing configuration versions.">
{#snippet actions()}<Button variant="primary" onclick={() => openCreate('baseline')}>Create baseline</Button>{/snippet}
</EmptyState>
{:else if !filtered.length && !error}<EmptyState title="No matching configs" description="Try a different search or clear your filters.">
{#snippet actions()}<Button onclick={resetFilters}>Clear filters</Button>{/snippet}
</EmptyState>
{:else}
{#if filteredBaselines.length}
<section class="section"><h2>Baselines <span>{filteredBaselines.length}</span></h2>{@render configTable(filteredBaselines)}</section>
@@ -181,8 +185,6 @@
.section { display: flex; flex-direction: column; gap: 14px; }
h2 { font-size: var(--text-md); margin: 0; letter-spacing: var(--tracking-tight); } h2 span { font-size: var(--text-xs); color: var(--text-secondary); font-weight: 400; margin-left: 8px; }
.loading { padding: 60px 0; text-align: center; color: var(--text-secondary); }
.welcome { padding: 56px 32px; border-radius: var(--radius-lg); background: var(--surface); border: 1px solid var(--border-strong); text-align: center; }
.welcome h2 { margin-top: 12px; font-size: var(--text-2xl); } .welcome p { color: var(--text-secondary); max-width: 540px; margin: 16px auto 24px; }
.eyebrow { color: var(--accent-primary-hover); font-size: var(--text-xs); }
.error { color: var(--state-danger); padding: 12px; background: var(--surface-raised); border-radius: var(--radius-sm); font-size: var(--text-sm); }
.warning { color: var(--state-warning); font-size: var(--text-sm); }
@@ -199,7 +201,6 @@
.dialog-actions { flex-wrap: wrap; } .dialog-actions p { flex: 1 1 240px; font-size: var(--text-xs); color: var(--text-secondary); }
@media (max-width:600px) { .form-grid { grid-template-columns:1fr; } form { padding:18px; } .navigation { gap:4px; flex-wrap:wrap; } }
.config-panel { display:grid; gap:20px; }
.welcome { padding:24px; }
.loading { padding:24px 0; }
input:not([type='checkbox']):not([type='file']), select { border-color: var(--border-control); }
</style>
@@ -286,7 +286,7 @@
fieldset label.selected { background: var(--surface-raised); border-color: var(--accent-primary); }
fieldset input { margin: 0; }
.change-summary { display: grid; gap: 8px; padding: 14px 16px; background: var(--surface-sunken); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
.change-summary > strong { font-size: 17px; line-height: 1.4; }
.change-summary > strong { font-size: var(--text-md); line-height: 1.4; }
.readiness { display: flex; flex-wrap: wrap; gap: 6px 16px; color: var(--text-secondary); font-size: var(--text-xs); }
.readiness:empty { display: none; }
p { margin: 0; font-size: var(--text-sm); line-height: 1.5; }
@@ -220,7 +220,7 @@
label { display: grid; gap: 8px; color: var(--text-secondary); font-size: var(--text-sm); }
select { width: 100%; }
.summary { display: grid; gap: 8px; padding: 16px; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--surface-sunken); }
.summary > strong { font-size: 17px; line-height: 1.4; }
.summary > strong { font-size: var(--text-md); line-height: 1.4; }
p { margin: 0; font-size: var(--text-sm); line-height: 1.5; }
.muted { color: var(--text-secondary); font-size: var(--text-xs); }
.readiness { display: flex; flex-wrap: wrap; gap: 8px 14px; font-size: var(--text-xs); color: var(--text-secondary); }
@@ -46,5 +46,5 @@
p { margin: 0; line-height: 1.6; color: var(--text-secondary); }
.notice { padding: 16px; background: var(--surface-raised); border-radius: var(--radius-md); line-height: 1.6; }
.notice { color: var(--text-primary); }
.irreversible { font-size: 13px; } .error { color: var(--state-danger); }
.irreversible { font-size: var(--text-sm); } .error { color: var(--state-danger); }
</style>
+14 -14
View File
@@ -51,9 +51,9 @@
<style>
.diff {
background: var(--bg-page);
border: 1px solid var(--border);
border-radius: var(--r-sm);
background: var(--surface-sunken);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
margin: 10px 0;
overflow: hidden;
}
@@ -61,14 +61,14 @@
display: flex;
gap: 12px;
padding: 10px 14px;
background: var(--bg-nav);
border-bottom: 1px solid var(--border);
background: var(--surface);
border-bottom: 1px solid var(--border-subtle);
font-size: var(--text-sm);
align-items: center;
}
.verb {
padding: 3px 10px;
border-radius: var(--r-sm);
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: var(--text-xs);
text-transform: uppercase;
@@ -78,35 +78,35 @@
.verb-added {
background: rgba(124, 212, 255, 0.10);
border-color: rgba(124, 212, 255, 0.3);
color: var(--spark-500);
color: var(--state-online);
}
.verb-updated {
background: rgba(124, 192, 245, 0.10);
border-color: rgba(124, 192, 245, 0.28);
color: var(--cream);
color: var(--text-primary);
}
.verb-removed {
background: rgba(255, 48, 80, 0.1);
border-color: rgba(255, 48, 80, 0.3);
color: var(--danger-500);
color: var(--state-danger);
}
.verb-held {
background: rgba(255, 176, 32, 0.1);
border-color: rgba(255, 176, 32, 0.3);
color: var(--amber-500);
color: var(--state-warning);
}
.path { color: var(--ink-100); font-family: var(--font-mono); font-size: var(--text-mono); }
.path { color: var(--text-primary); font-family: var(--font-mono); font-size: var(--text-sm); }
pre {
margin: 0;
padding: 14px 16px;
font-family: var(--font-mono);
font-size: var(--text-mono);
font-size: var(--text-sm);
line-height: var(--leading-mono);
color: var(--ink-300);
color: var(--text-secondary);
max-height: 540px;
overflow: auto;
}
.ln { display: block; }
.ln-add { background: rgba(124, 212, 255, 0.08); color: var(--spark-400); }
.ln-add { background: color-mix(in srgb, var(--state-online) 8%, transparent); color: var(--state-online); }
.ln-del { background: rgba(255, 48, 80, 0.08); color: #ff8a9a; }
</style>
@@ -68,9 +68,9 @@
<style>
dialog { width: min(620px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); padding: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface); color: var(--text-primary); }
dialog::backdrop { background: rgb(0 0 0 / .7); backdrop-filter: blur(5px); }
.content { display: grid; gap: 20px; padding: 28px; } h2, p { margin: 0; } h2 { font-size: 25px; }
p { color: var(--text-secondary); font-size: 13px; line-height: 1.6; } .error { color: var(--state-danger); } .caption { font-size: 12px; }
.content { display: grid; gap: 20px; padding: 28px; } h2, p { margin: 0; } h2 { font-size: var(--text-xl); }
p { color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; } .error { color: var(--state-danger); } .caption { font-size: var(--text-xs); }
ul { margin: 0; padding: 12px 12px 12px 30px; max-height: 220px; overflow: auto; background: var(--surface-sunken); border-radius: var(--radius-md); }
li { padding: 5px 0; font-family: var(--font-body); font-size: 12px; overflow-wrap: anywhere; }
li { padding: 5px 0; font-family: var(--font-body); font-size: var(--text-xs); overflow-wrap: anywhere; }
.actions { display: flex; gap: 12px; justify-content: flex-end; flex-wrap: wrap; }
</style>
+6 -6
View File
@@ -23,12 +23,12 @@
.recipe { display: grid; gap: 16px; min-width: 0; }
.recipe-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.eyebrow { display: block; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .12em; color: var(--text-secondary); margin-bottom: 6px; }
h3 { margin: 0; font-size: 22px; letter-spacing: -.03em; }
.game { border: 1px solid var(--border-strong); border-radius: 99px; padding: 5px 10px; font-size: 12px; white-space: nowrap; }
dl { display: grid; gap: 12px; margin: 0; } dt { color: var(--text-secondary); font-size: 12px; } dd { margin: 4px 0 0; font-size: 13px; overflow-wrap: anywhere; line-height: 1.5; }
h3 { margin: 0; font-size: var(--text-xl); letter-spacing: -.03em; }
.game { border: 1px solid var(--border-strong); border-radius: 99px; padding: 5px 10px; font-size: var(--text-xs); white-space: nowrap; }
dl { display: grid; gap: 12px; margin: 0; } dt { color: var(--text-secondary); font-size: var(--text-xs); } dd { margin: 4px 0 0; font-size: var(--text-sm); overflow-wrap: anywhere; line-height: 1.5; }
.mono, code, .fingerprint { font-family: var(--font-mono); }
details { border-top: 1px solid var(--border-subtle); padding-top: 14px; } summary { cursor: pointer; font-size: 13px; }
details { border-top: 1px solid var(--border-subtle); padding-top: 14px; } summary { cursor: pointer; font-size: var(--text-sm); }
ul { list-style: none; padding: 0; margin: 12px 0; } li { display: grid; gap: 5px; padding: 12px 0; border-bottom: 1px solid var(--border-subtle); overflow-wrap: anywhere; }
li > div { display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap; } strong { font-size: 13px; font-weight: 550; } span, code, small { font-size: var(--text-xs); } li span, small { color: var(--text-secondary); }
p { color: var(--text-secondary); font-size: 12px; line-height: 1.6; }
li > div { display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap; } strong { font-size: var(--text-sm); font-weight: 550; } span, code, small { font-size: var(--text-xs); } li span, small { color: var(--text-secondary); }
p { color: var(--text-secondary); font-size: var(--text-xs); line-height: 1.6; }
</style>
+31 -31
View File
@@ -185,7 +185,7 @@
flex-wrap: wrap;
gap: 6px;
align-items: center;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 8px;
}
@@ -196,46 +196,46 @@
gap: 14px;
}
.meta {
color: var(--ink-500);
font-family: var(--font-mono);
font-size: var(--text-mono-sm);
color: var(--text-tertiary);
font-family: var(--font-body);
font-size: var(--text-xs);
}
.auto {
display: flex;
align-items: center;
gap: 6px;
color: var(--ink-500);
font-family: var(--font-mono);
font-size: var(--text-mono-sm);
color: var(--text-tertiary);
font-family: var(--font-body);
font-size: var(--text-xs);
cursor: pointer;
user-select: none;
}
.auto input { accent-color: var(--accent); }
.auto input { accent-color: var(--accent-primary); }
.refresh {
background: var(--bg-card);
color: var(--ink-100);
border: 1px solid var(--border);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--text-primary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 6px 12px;
font-family: var(--font-body);
font-size: var(--text-sm);
cursor: pointer;
}
.refresh:hover:not(:disabled) {
border-color: var(--accent);
box-shadow: var(--accent-glow);
border-color: var(--accent-primary);
box-shadow: var(--focus-ring);
}
.refresh:disabled { opacity: 0.4; cursor: default; }
.body {
margin: 0;
padding: 16px 18px;
background: var(--bg-page);
border: 1px solid var(--border);
border-radius: var(--r-sm);
color: var(--ink-300);
background: var(--surface-sunken);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: var(--text-mono);
font-size: var(--text-sm);
line-height: var(--leading-mono);
max-height: 560px;
overflow: auto;
@@ -245,28 +245,28 @@
.empty, .error {
padding: 28px 16px;
text-align: center;
font-family: var(--font-mono);
font-size: var(--text-mono);
border-radius: var(--r-sm);
font-family: var(--font-body);
font-size: var(--text-sm);
border-radius: var(--radius-sm);
}
.empty {
color: var(--ink-500);
background: var(--bg-nav);
border: 1px dashed var(--border);
color: var(--text-tertiary);
background: var(--surface);
border: 1px dashed var(--border-subtle);
}
.empty code {
color: var(--cream-dim);
font-size: var(--text-mono-sm);
color: var(--text-secondary);
font-size: var(--text-xs);
}
.error {
color: var(--danger-500);
color: var(--state-danger);
background: rgba(255, 48, 80, 0.08);
border: 1px solid rgba(255, 48, 80, 0.25);
}
.foot {
color: var(--ink-500);
font-family: var(--font-mono);
font-size: var(--text-mono-sm);
color: var(--text-tertiary);
font-family: var(--font-body);
font-size: var(--text-xs);
text-align: right;
}
@media (max-width: 640px) {
@@ -122,11 +122,11 @@
{/if}
<style>
.game-files { display:grid; gap:5px; } .game-files strong { font-size:13px; font-weight:600; }
.game-files { display:grid; gap:5px; } .game-files strong { font-size: var(--text-sm); font-weight:600; }
.advanced { border-top:1px solid var(--border-subtle); padding-top:16px; }
.advanced summary { color:var(--text-secondary); }
.advanced-fields { padding-top:2px; }
fieldset { margin:18px 0 0; padding:0; border:0; min-width:0; }
legend { padding:0; font-size:13px; font-weight:600; }
.selection { display:grid; gap:14px; padding:20px; border:1px solid var(--border-subtle); border-radius:var(--radius-md); background:var(--surface-sunken); } .title { display:flex; gap:12px; justify-content:space-between; align-items:center; } h3 { font-size:var(--text-sm); margin:0; } label { display:grid; gap:7px; font-size:13px; } input { width:100%; box-sizing:border-box; padding:11px; color:var(--text-primary); background:var(--surface); border:1px solid var(--border-control); border-radius:var(--radius-sm); font:inherit; } p { color:var(--text-secondary); font-size:12px; line-height:1.6; margin:0; } a,button { color:var(--accent-primary-hover); } button { background:none; border:0; cursor:pointer; font:inherit; font-size:12px; } button:disabled { opacity:.5; cursor:default; } .error { color:var(--state-danger); } summary { cursor:pointer; font-size:13px; } details label { margin-top:14px; } details p { margin-top:12px; }
legend { padding:0; font-size: var(--text-sm); font-weight:600; }
.selection { display:grid; gap:14px; padding:20px; border:1px solid var(--border-subtle); border-radius:var(--radius-md); background:var(--surface-sunken); } .title { display:flex; gap:12px; justify-content:space-between; align-items:center; } h3 { font-size:var(--text-sm); margin:0; } label { display:grid; gap:7px; font-size: var(--text-sm); } input { width:100%; box-sizing:border-box; padding:11px; color:var(--text-primary); background:var(--surface); border:1px solid var(--border-control); border-radius:var(--radius-sm); font:inherit; } p { color:var(--text-secondary); font-size: var(--text-xs); line-height:1.6; margin:0; } a,button { color:var(--accent-primary-hover); } button { background:none; border:0; cursor:pointer; font:inherit; font-size: var(--text-xs); } button:disabled { opacity:.5; cursor:default; } .error { color:var(--state-danger); } summary { cursor:pointer; font-size: var(--text-sm); } details label { margin-top:14px; } details p { margin-top:12px; }
</style>
+1 -1
View File
@@ -14,7 +14,7 @@
const statuses = $fleetServers.map(server => ({ server, state: serverHealth(server, $uiNow).state }));
const servers = statuses.filter(({ server, state }) => !['online', 'starting', 'provisioning'].includes(state)
|| $fleet.operations.some(operation => operation.server === server.name && operation.state === 'attention')).length;
const hosts = new Set(statuses.filter(({ server, state }) => server.host && state === 'host-offline').map(({ server }) => server.host!.id)).size;
const hosts = $fleet.hostAttention ?? new Set(statuses.filter(({ server, state }) => server.host && state === 'host-offline').map(({ server }) => server.host!.id)).size;
const maintenance = $fleet.operations.filter(operation => operation.kind === 'maintenance' && operation.state === 'attention').length;
return { '/': servers, '/hosts': hosts, '/maintenance': maintenance } as Record<string, number>;
});
@@ -47,7 +47,7 @@
<style>
section { display: grid; gap: 16px; }
h2 { font-size: 18px; margin: 0; }
h2 { font-size: var(--text-md); margin: 0; }
details { border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 16px; }
summary { cursor: pointer; font-size: var(--text-sm); font-weight: var(--weight-semibold); }
details[open] summary { margin-bottom: 16px; }
@@ -58,11 +58,11 @@
label { display: grid; gap: 6px; font-size: var(--text-xs); color: var(--text-secondary); }
label:first-child { flex: 1; min-width: 180px; }
input, select { background: var(--surface-sunken); color: var(--text-primary); border: 1px solid var(--border-control); padding: 9px 12px; border-radius: var(--radius-sm); font: inherit; }
time, .muted { color: var(--text-secondary); font-size: 13px; }
time, .muted { color: var(--text-secondary); font-size: var(--text-sm); }
p { margin: 0; line-height: 1.6; }
.flow { display: flex; flex-wrap: wrap; gap: 12px; padding: 0; margin: 0; list-style: none; }
.flow li { display: flex; align-items: center; gap: 10px; background: var(--surface); border-radius: var(--radius-md); padding: 12px; font-size: 13px; border: 1px solid var(--border-subtle); color: var(--text-secondary); }
.flow span { color: var(--accent-primary-hover); font-family: var(--font-mono); }
.flow li { display: flex; align-items: center; gap: 10px; background: var(--surface); border-radius: var(--radius-md); padding: 12px; font-size: var(--text-sm); border: 1px solid var(--border-subtle); color: var(--text-secondary); }
.flow span { color: var(--text-primary); font-family: var(--font-numeric); font-variant-numeric: tabular-nums; }
.flow .done { color: var(--text-primary); }
.flow .current { color: var(--text-primary); border-color: var(--accent-primary); }
.flow .failed { color: var(--state-danger); border-color: var(--state-danger); }
+27 -27
View File
@@ -45,10 +45,10 @@
<style>
.wrapper {
background: var(--bg-nav);
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: var(--pad-card);
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: var(--space-5);
}
header {
display: flex;
@@ -63,66 +63,66 @@
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--ink-300);
color: var(--text-secondary);
}
.empty {
color: var(--ink-500);
font-family: var(--font-mono);
font-size: var(--text-mono);
color: var(--text-tertiary);
font-family: var(--font-body);
font-size: var(--text-sm);
margin: 0;
padding: 14px 0;
}
.rows { display: grid; gap: 10px; }
.row {
display: block;
color: var(--ink-100);
color: var(--text-primary);
text-decoration: none;
padding: 12px 14px;
background: var(--bg-card);
border-radius: var(--r-sm);
border: 1px solid var(--border);
background: var(--surface);
border-radius: var(--radius-sm);
border: 1px solid var(--border-subtle);
transition: border-color 0.15s;
}
.row:hover { border-color: var(--accent); }
.row:hover { border-color: var(--accent-primary); }
.row-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 8px;
}
.action { font-family: var(--font-mono); font-size: var(--text-mono); color: var(--cream); }
.count { color: var(--ink-500); font-size: var(--text-mono-sm); font-family: var(--font-mono); }
.action { font-family: var(--font-body); font-size: var(--text-sm); color: var(--text-primary); }
.count { color: var(--text-tertiary); font-size: var(--text-xs); font-family: var(--font-numeric); font-variant-numeric: tabular-nums; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
padding: 3px 9px;
border-radius: var(--r-sm);
font-size: var(--text-mono-sm);
font-family: var(--font-mono);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
font-family: var(--font-body);
border: 1px solid transparent;
}
.chip-done {
background: rgba(124, 212, 255, 0.10);
border-color: rgba(124, 212, 255, 0.3);
color: var(--spark-500);
background: color-mix(in srgb, var(--state-online) 10%, transparent);
border-color: color-mix(in srgb, var(--state-online) 30%, transparent);
color: var(--state-online);
}
.chip-failed {
background: rgba(255, 48, 80, 0.1);
border-color: rgba(255, 48, 80, 0.3);
color: var(--danger-500);
color: var(--state-danger);
}
.chip-pending {
background: rgba(255, 176, 32, 0.1);
border-color: rgba(255, 176, 32, 0.3);
color: var(--amber-500);
color: var(--state-warning);
}
.chip-held {
background: rgba(124, 192, 245, 0.10);
border-color: rgba(124, 192, 245, 0.28);
color: var(--cream);
color: var(--text-primary);
}
.chip-manual {
background: var(--bg-nav);
border-color: var(--border);
color: var(--ink-500);
background: var(--surface);
border-color: var(--border-subtle);
color: var(--text-tertiary);
}
</style>
@@ -51,7 +51,7 @@
<span class="eyebrow">{report.event === 'game_end' ? 'Final scoreboard' : fresh ? 'Live scoreboard' : 'Last reported scoreboard'}</span>
<div class="match-title">
<h2 title={report.map}>{mapLabel ?? mapTitle(report.map)}</h2>
{#if report.event !== 'game_start'}<Badge variant="accent" size="md">Round {report.round}</Badge>{/if}
{#if report.event !== 'game_start'}<Badge size="md">Round {report.round}</Badge>{/if}
{#if displayElapsed}<span class="match-time">Match {displayElapsed}{fresh ? '' : ' at last report'}</span>{/if}
{#if report.event === 'game_end'}<Badge>Game ended</Badge>
{:else if report.event === 'game_start'}<Badge>Game started</Badge>{/if}
@@ -140,8 +140,8 @@
.timing-note { margin: var(--space-2) 0 0; font-size: var(--text-xs); color: var(--text-tertiary); }
.guid { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-1); color: var(--text-tertiary); font-size: var(--text-xs); }
.stat { text-align: right; white-space: nowrap; }
td.stat { font: var(--text-sm) var(--font-mono); font-variant-numeric: tabular-nums; }
.score { color: var(--accent-primary-hover); font-weight: var(--weight-semibold); }
td.stat { font: var(--text-sm) var(--font-numeric); font-variant-numeric: tabular-nums; }
.score { color: var(--text-primary); font-weight: var(--weight-semibold); }
.unreported { color: var(--text-tertiary); }
tbody tr:last-child > * { border-bottom: none; }
tbody tr:hover > * { background: var(--surface-raised); }
@@ -93,7 +93,7 @@
summary { cursor: pointer; }
.phases summary { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-5); list-style: none; }
.phases summary::-webkit-details-marker { display: none; }
.phases summary::after { content: '+'; color: var(--text-tertiary); font-size: 18px; }
.phases summary::after { content: '+'; color: var(--text-tertiary); font-size: var(--text-md); }
.phases details[open] > summary::after { content: '−'; }
summary:focus-visible { outline: 2px solid var(--border-focus); outline-offset: -3px; border-radius: var(--radius-sm); }
.phase-icon { flex: 0 0 26px; display: grid; place-items: center; width: 26px; height: 26px; border: 1px solid var(--border-strong); border-radius: var(--radius-pill); font-size: var(--text-sm); }
+1 -1
View File
@@ -19,6 +19,6 @@
<style>
.round { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
strong { color: var(--text-primary); font-family: var(--font-mono); }
strong { color: var(--text-primary); font-family: var(--font-numeric); font-variant-numeric: tabular-nums; }
span { color: var(--text-tertiary); font-size: var(--text-xs); }
</style>
+1 -1
View File
@@ -7,6 +7,6 @@
<style>
.identity { display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; min-width: 0; }
.map { overflow-wrap: anywhere; }
.lobby { white-space: nowrap; font-size: .85em; color: var(--text-secondary); font-weight: var(--weight-medium); }
.lobby { white-space: nowrap; font-size: max(var(--text-xs), .85em); color: var(--text-secondary); font-weight: var(--weight-medium); }
small { font-size: var(--text-xs); color: var(--text-secondary); white-space: nowrap; font-weight: var(--weight-regular); }
</style>
-37
View File
@@ -1,37 +0,0 @@
<script lang="ts">
import type { Health } from '$ui/stores/servers.js';
interface Props {
health: Health;
size?: number;
label?: string | null;
}
let { health, size = 10, label = null }: Props = $props();
</script>
<span
class="dot dot-{health}"
style="width:{size}px;height:{size}px"
aria-label={label ?? `server ${health}`}
title={label ?? health}
></span>
<style>
.dot {
display: inline-block;
border-radius: 999px;
vertical-align: middle;
flex-shrink: 0;
}
.dot-online {
background: var(--spark-500);
animation: electric-pulse 1.6s ease-in-out infinite;
}
.dot-stale, .dot-starting, .dot-provisioning {
background: var(--amber-500);
box-shadow: var(--amber-glow);
}
.dot-offline {
background: var(--danger-500);
opacity: 0.6;
}
</style>
@@ -123,16 +123,17 @@
<style>
.panel { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: var(--space-5); min-width: 0; }
h2 { margin: 0; font-size: var(--text-lg); } .heading { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-4); flex-wrap: wrap; }
.heading > div:first-child { flex:1 1 240px; min-width:0; }
.heading p { margin: var(--space-2) 0 0; color: var(--text-secondary); font-size: var(--text-sm); }
.controls { display:flex; align-items:center; flex-wrap:wrap; gap:12px; }
.controls { display:flex; align-items:center; flex-wrap:wrap; gap:12px; min-width:0; max-width:100%; }
.scope-note { margin:8px 0 0; font-size:var(--text-xs); color:var(--text-secondary); }
.mode { display: flex; gap: 3px; padding: 3px; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
.mode button { padding: 7px 14px; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; }
.mode button[aria-pressed='true'] { color: var(--text-primary); background: var(--surface-raised); }
.mode button:focus-visible, .plot:focus-visible, .table-wrap:focus-visible, summary:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 3px; }
.axis-title { margin-top: var(--space-4); color: var(--text-tertiary); font-size: 11px; }
.axis-title { margin-top: var(--space-4); color: var(--text-tertiary); font-size: var(--text-xs); }
.chart-grid { display: grid; grid-template-columns: 42px minmax(0, 1fr); gap: 10px; margin-top: 14px; }
.y-axis { position: relative; height: 176px; margin-top: 8px; color: var(--text-tertiary); font-size: 11px; }
.y-axis { position: relative; height: 176px; margin-top: 8px; color: var(--text-tertiary); font-size: var(--text-xs); }
.y-axis span { position: absolute; right: 0; transform: translateY(-50%); }
.plot { position: relative; height: 192px; border-radius: 3px; cursor: crosshair; touch-action: pan-y; }
svg { display: block; width: 100%; height: 100%; overflow: visible; }
@@ -141,7 +142,7 @@
.trend { fill: none; stroke: var(--accent-primary); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.point, .selected-point { fill: var(--accent-primary-hover); } .selected-point { stroke: var(--surface); stroke-width: 2; }
.cursor { stroke: var(--text-tertiary); stroke-width: 1; stroke-dasharray: 4 5; }
.x-axis { grid-column: 2; display: flex; justify-content: space-between; gap: var(--space-4); color: var(--text-tertiary); font-size: 11px; }
.x-axis { grid-column: 2; display: flex; justify-content: space-between; gap: var(--space-4); color: var(--text-tertiary); font-size: var(--text-xs); }
.x-axis span:last-child { text-align: right; }
.empty-chart { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 10px; text-align: center; padding: var(--space-4); pointer-events: none; }
.empty-chart strong { font-size: var(--text-sm); font-weight: var(--weight-medium); } .empty-chart span { color: var(--text-tertiary); font-size: var(--text-xs); }
@@ -150,8 +151,8 @@
dl { display:flex; flex-wrap:wrap; gap:12px 24px; margin:0; } dl>div { display:flex; gap:6px; } dt { color:var(--text-secondary); } dd { margin:0; font-variant-numeric:tabular-nums; }
.note { color: var(--text-tertiary); font-size: var(--text-xs); line-height: 1.65; margin: var(--space-4) 0; max-width: 115ch; }
summary { cursor: pointer; color: var(--text-secondary); font-size: var(--text-xs); padding: 4px 0; }
.table-wrap { overflow: auto; max-height: 400px; margin-top: var(--space-3); } table { width: 100%; border-collapse: collapse; font-size: var(--text-xs); }
.table-wrap { position: relative; overflow: auto; max-height: 400px; margin-top: var(--space-3); } table { width: 100%; border-collapse: collapse; font-size: var(--text-xs); }
caption { text-align: left; color: var(--text-tertiary); margin-bottom: var(--space-3); } th, td { text-align: right; padding: 12px; white-space: nowrap; border-bottom: 1px solid var(--border-subtle); font-variant-numeric: tabular-nums; }
th:first-child { text-align: left; } tbody th { font-weight: var(--weight-regular); } thead th { color: var(--text-secondary); }
@media (max-width: 640px) { .panel { padding: var(--space-4); } .chart-grid { grid-template-columns: 32px minmax(0, 1fr); gap: 8px; } .readout { gap: var(--space-4); } dl { gap: var(--space-4); } .x-axis { font-size: 10px; } }
@media (max-width: 640px) { .panel { padding: var(--space-4); } .chart-grid { grid-template-columns: 32px minmax(0, 1fr); gap: 8px; } .readout { gap: var(--space-4); } dl { gap: var(--space-4); } .x-axis { font-size: var(--text-xs); } }
</style>
@@ -14,7 +14,8 @@
}
</script>
<div class="table-wrap"><table>
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable tables must be reachable by keyboard.) -->
<div class="table-wrap" role="region" aria-label="Host capacity comparison" tabindex="0"><table>
<thead><tr><th>Host</th><th>Additional instances</th><th>Limited by</th><th>CPU available</th><th>Memory available</th><th>Free ports</th><th><span class="sr-only">Details</span></th></tr></thead>
<tbody>{#each hosts as host (host.id)}
<tr>
@@ -43,7 +44,8 @@
</table></div>
<style>
.table-wrap { overflow-x:auto; border:1px solid var(--border-subtle); border-radius:var(--radius-md); }
.table-wrap { position:relative; overflow-x:auto; border:1px solid var(--border-subtle); border-radius:var(--radius-md); }
.table-wrap:focus-visible { outline:2px solid var(--accent-primary); outline-offset:3px; }
table { width:100%; min-width:800px; border-collapse:collapse; text-align:left; font-size:var(--text-sm); }
th { padding:12px 16px; color:var(--text-secondary); font-size:var(--text-xs); font-weight:500; background:var(--surface-sunken); }
td { padding:14px 16px; border-top:1px solid var(--border-subtle); background:var(--surface); font-variant-numeric:tabular-nums; }
@@ -3,7 +3,7 @@
import { page } from '$app/state';
import { replaceState } from '$app/navigation';
import { createTimeFormatter } from '$lib/time-zone.js';
import { metricDuration, metricNumber, metricPercent } from '$ui/metrics.js';
import { metricDuration, metricNumber, metricPercent, metricPeriodLabels } from '$ui/metrics.js';
import Button from '$ui/components/primitives/Button.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import Select from '$ui/components/primitives/Select.svelte';
@@ -32,7 +32,7 @@
const tick = new Intl.NumberFormat('en-US', { maximumSignificantDigits: 3 });
const ticks = $derived([1, .75, .5, .25, 0].map(fraction => ({ fraction, value: `${tick.format(ceiling * fraction)}${mode === 'percent' ? '%' : ''}` })));
const periodOptions = $derived(points.map(point => ({ value: String(point.at), label: `${format.period(point.startAt)}${point.partial ? ' · partial' : ''}` })));
const middle = $derived(points[Math.floor(points.length / 2)]);
const periodLabels = $derived(metricPeriodLabels(points, days, timeZone));
const captured = $derived(selected ? selected.observed + selected.unknown : 0);
function setMode(value: 'hours' | 'percent') {
@@ -65,7 +65,7 @@
</script>
<section class="panel" aria-labelledby="metrics-trend-title">
<div class="heading"><div><h2 id="metrics-trend-title">Instance activity over time</h2><p>{mode === 'percent' ? 'Share of captured server time, including unknown status.' : `Server-hours per ${days === 1 ? 'hour' : '24-hour period'}.`} Partial periods contain less elapsed time.</p></div>
<div class="heading"><div><h2 id="metrics-trend-title">Server activity over time</h2><p>{mode === 'percent' ? 'Share of captured server time, including unknown status.' : `Server-hours per ${days === 1 ? 'hour' : '24-hour period'}.`} Partial periods contain less elapsed time.</p></div>
<div class="mode" role="group" aria-label="Timeline measure"><button type="button" aria-pressed={mode === 'percent'} onclick={() => setMode('percent')}>Percent</button><button type="button" aria-pressed={mode === 'hours'} onclick={() => setMode('hours')}>Hours</button></div>
</div>
<div class="legend"><span><i class="occupied"></i>With players</span><span><i class="idle"></i>Idle</span><span><i class="unknown"></i>Unknown</span><span class="partial-key">* Partial period</span></div>
@@ -88,8 +88,9 @@
</button>
{/each}
</div>
<div class="x-axis" aria-hidden="true"><span>{points[0] ? format.period(points[0].startAt) : ''}</span>{#if middle}<span class="middle">{format.period(middle.startAt)}</span>{/if}<span>{points.length ? format.period(points[points.length - 1].endAt) : ''}</span></div>
<div class="x-axis" class:with-dates={days === 30} aria-hidden="true">{#each periodLabels as item}<span style:left={`${item.position}%`} title={format.period(points[item.index].startAt)}>{item.label}{#if item.date}<small>{item.date}</small>{/if}</span>{/each}</div>
</div>
<p class="period-note">{days === 1 ? 'Hours' : 'Weekdays'} show when each period starts in {timeZone.replaceAll('_', ' ')}.{days === 1 ? '' : ' Daily periods are 24-hour windows.'}</p>
{#if selected}
<div class="selection-summary" aria-label="Selected period summary"><time>{format.period(selected.startAt)}{selected.partial ? ' · Partial period' : ''}</time><span>{captured ? `${metricDuration(selected.active)} with players · ${metricDuration(selected.idle)} idle · ${metricDuration(selected.unknown)} unknown · ${selected.observed ? metricNumber(selected.playerHours) : '—'} player-hours` : 'No captured time · not evidence of idle instances'}</span></div>
<details class="inspection"><summary>Inspect a period</summary>
@@ -125,6 +126,7 @@
.panel { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: var(--space-5); min-width: 0; }
h2 { margin: 0; font-size: var(--text-lg); }
.heading { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.heading > div:first-child { flex: 1 1 260px; min-width: 0; }
.mode { display: flex; border: 1px solid var(--border-subtle); padding: 3px; border-radius: var(--radius-md); }
.mode button { border: 0; border-radius: var(--radius-sm); padding: 7px 10px; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; }
.mode button[aria-pressed='true'] { background: var(--surface-raised); color: var(--text-primary); }
@@ -134,12 +136,12 @@
.legend span { display: flex; align-items: center; gap: 6px; }
.legend i { width: 10px; height: 10px; border-radius: 2px; }
.occupied { background: var(--chart-active); }
.idle { background: var(--chart-idle); }
.idle { background: repeating-linear-gradient(0deg, transparent 0 5px, color-mix(in srgb, var(--text-secondary) 24%, transparent) 5px 6px), var(--chart-idle); }
.unknown { background: repeating-linear-gradient(135deg, var(--chart-unknown-base) 0 3px, var(--chart-unknown-stripe) 3px 5px); }
.partial-key { color: var(--text-secondary); }
.axis-title { margin-top: var(--space-5); font-size: 11px; color: var(--text-tertiary); }
.axis-title { margin-top: var(--space-5); font-size: var(--text-xs); color: var(--text-tertiary); }
.chart-grid { display: grid; grid-template-columns: 44px minmax(0, 1fr); column-gap: 8px; margin-top: 12px; }
.y-axis { position: relative; text-align: right; font-size: 11px; color: var(--text-tertiary); height: 210px; }
.y-axis { position: relative; text-align: right; font-size: var(--text-xs); color: var(--text-tertiary); height: 210px; }
.y-axis span { position: absolute; right: 0; transform: translateY(-50%); }
.plot { display: flex; position: relative; height: 210px; gap: 4px; isolation: isolate; border-bottom: 1px solid var(--border-strong); }
.grid-lines { position: absolute; inset: 0; pointer-events: none; z-index: -1; }
@@ -149,13 +151,14 @@
.bar:focus-visible { outline: 2px solid var(--accent-primary-hover); outline-offset: 2px; }
.bar:hover { background: color-mix(in oklab, var(--accent-primary) 8%, transparent); }
.segment { display: block; width: 100%; flex-shrink: 0; pointer-events: none; }
.no-time { color: var(--text-tertiary); font-size: 20px; line-height: 1; }
.no-time { color: var(--text-tertiary); font-size: var(--text-lg); line-height: 1; }
.bar.partial { border-inline: 1px dashed var(--border-strong); }
.partial-marker { position: absolute; top: -17px; left: 50%; transform: translateX(-50%); font-size: 15px; color: var(--text-secondary); }
.x-axis { grid-column: 2; display: flex; justify-content: space-between; gap: 16px; color: var(--text-tertiary); font-size: 10px; margin-top: 10px; }
.x-axis span { max-width: 25ch; }
.x-axis span:last-child { text-align: right; }
.x-axis .middle { text-align: center; }
.partial-marker { position: absolute; top: -17px; left: 50%; transform: translateX(-50%); font-size: var(--text-base); color: var(--text-secondary); }
.x-axis { grid-column: 2; position: relative; min-width: 0; height: 1.6em; color: var(--text-secondary); font-size: var(--text-xs); margin-top: 10px; }
.x-axis.with-dates { height: 3em; }
.x-axis > span { position: absolute; transform: translateX(-50%); text-align: center; white-space: nowrap; }
.x-axis small { margin: 0; }
.period-note { color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); margin: var(--space-2) 0 0; }
.period-controls { display: flex; align-items: flex-end; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-5); }
.period-select { flex: 1 1 250px; max-width: 450px; }
.step-buttons { display: flex; gap: var(--space-2); padding-bottom: 3px; }
@@ -165,13 +168,13 @@
dl { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); margin: var(--space-3) 0 0; }
dt { color: var(--text-tertiary); font-size: var(--text-xs); }
dd { margin: var(--space-1) 0 0; font-variant-numeric: tabular-nums; font-size: var(--text-md); }
small { display: block; color: var(--text-tertiary); font-size: 11px; font-weight: normal; line-height: 1.5; }
small { display: block; color: var(--text-tertiary); font-size: var(--text-xs); font-weight: normal; line-height: 1.5; }
.empty { margin: var(--space-3) 0 0; color: var(--text-secondary); font-size: var(--text-sm); }
.denominator { color: var(--text-tertiary); font-size: 11px; line-height: 1.5; margin: var(--space-3) 0 0; }
.denominator { color: var(--text-tertiary); font-size: var(--text-xs); line-height: 1.5; margin: var(--space-3) 0 0; }
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
details { margin-top: var(--space-4); font-size: var(--text-sm); }
summary { cursor: pointer; color: var(--text-secondary); padding: 4px 0; }
.table-wrap { max-width: 100%; overflow-x: auto; margin-top: var(--space-3); }
.table-wrap { position: relative; max-width: 100%; overflow-x: auto; margin-top: var(--space-3); }
.table-wrap:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 3px; }
table { width: 100%; border-collapse: collapse; font-size: var(--text-xs); }
caption { text-align: left; color: var(--text-tertiary); margin-bottom: 8px; }
@@ -179,5 +182,5 @@
thead th { color: var(--text-secondary); }
th:first-child { text-align: left; }
tbody th { font-weight: var(--weight-regular); }
@media (max-width: 640px) { .panel { padding: var(--space-4); } .legend { gap: 10px; } .chart-grid { grid-template-columns: 32px minmax(0, 1fr); gap: 6px; } .plot { gap: 3px; } .x-axis .middle { display: none; } .x-axis { font-size: 11px; } dl { grid-template-columns: repeat(2, minmax(0, 1fr)); } .readout { padding: var(--space-3); } }
@media (max-width: 640px) { .panel { padding: var(--space-4); } .legend { gap: 10px; } .chart-grid { grid-template-columns: 32px minmax(0, 1fr); gap: 6px; } .plot { gap: 3px; } dl { grid-template-columns: repeat(2, minmax(0, 1fr)); } .readout { padding: var(--space-3); } .step-buttons { flex-wrap: wrap; } }
</style>
@@ -8,6 +8,7 @@
type Heatmap = MetricsData['heatmap'];
type Cell = Heatmap['cells'][number];
let { heatmap, disabled = false, onTimeZoneChange }: { heatmap: Heatmap; disabled?: boolean; onTimeZoneChange: (value: string) => void } = $props();
const id = $props.id();
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const hours = Array.from({ length: 24 }, (_, hour) => hour);
const zones = $derived([...new Set([...availableTimeZones(), heatmap.timeZone])].sort().map(zone => ({ value: zone, label: zone.replaceAll('_', ' ') })));
@@ -34,8 +35,8 @@
}
</script>
<div class="heatmap">
<div class="heading"><div><h3>Activity by weekday and hour</h3><p>Time with players as a share of known server time, grouped in {heatmap.timeZone.replaceAll('_', ' ')}.</p></div>
<section class="heatmap" aria-labelledby={`${id}-heatmap-title`}>
<div class="heading"><div><h2 id={`${id}-heatmap-title`}>Activity by weekday and hour</h2><p>Time with players as a share of known server time, grouped in {heatmap.timeZone.replaceAll('_', ' ')}.</p></div>
<div class="zone"><Field label="Report timezone"><Select value={heatmap.timeZone} options={zones} {disabled} onchange={event => onTimeZoneChange((event.currentTarget as HTMLSelectElement).value)} /></Field></div>
</div>
<div class="legend"><span>0% <i class="scale"></i> 100% with players</span><span><i class="unknown-example"></i>Unknown only</span><span><i class="missing-example">·</i>No captured time</span></div>
@@ -65,24 +66,25 @@
</div>
{/if}
<span class="sr-only" role="status" aria-live="polite">{announcement}</span>
</div>
</section>
<style>
.heatmap { min-width: 0; margin-top: var(--space-4); }
.heatmap { min-width: 0; padding: var(--space-5); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); background: var(--surface); }
.heading { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-4); flex-wrap: wrap; }
h3 { margin: 0; font-size: var(--text-md); }
.heading > div:first-child { flex: 1 1 260px; min-width: 0; }
h2 { margin: 0; font-size: var(--text-lg); }
p { font-size: var(--text-xs); color: var(--text-secondary); margin: var(--space-2) 0 0; line-height: 1.6; }
.zone { flex: 0 1 250px; min-width: 190px; }
.legend { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; margin: var(--space-4) 0 var(--space-3); font-size: 11px; color: var(--text-secondary); }
.zone { flex: 0 1 250px; min-width: 0; max-width: 100%; }
.legend { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; margin: var(--space-4) 0 var(--space-3); font-size: var(--text-xs); color: var(--text-secondary); }
.legend span { display: flex; gap: 5px; align-items: center; }
.scale { width: 64px; height: 9px; background: linear-gradient(to right, var(--surface-raised), var(--chart-active)); border-radius: 3px; }
.unknown-example, .missing-example { width: 14px; height: 14px; display: inline-flex; justify-content: center; align-items: center; font-style: normal; }
.unknown-example, .cell.unknown { background: repeating-linear-gradient(135deg, var(--chart-unknown-base) 0 3px, var(--chart-unknown-stripe) 3px 5px); }
.missing-example { border: 1px dashed var(--border-control); }
.table-wrap { overflow-x: auto; max-width: 100%; padding: 3px; }
.table-wrap { position: relative; overflow-x: auto; max-width: 100%; padding: 3px; }
.table-wrap:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
table { width: 100%; min-width: 750px; border-collapse: separate; border-spacing: 3px; table-layout: fixed; }
th { color: var(--text-tertiary); font-weight: normal; font-size: 10px; text-align: center; padding: 0 0 3px; }
th { color: var(--text-tertiary); font-weight: normal; font-size: var(--text-xs); text-align: center; padding: 0 0 3px; }
th:first-child { width: 35px; position: sticky; left: 0; background: var(--surface); z-index: 2; text-align: left; }
td { padding: 0; }
.cell { position: relative; appearance: none; display: block; width: 100%; height: 29px; border: 1px solid var(--border-subtle); border-radius: 3px; background: var(--surface-raised); color: var(--text-secondary); font: inherit; cursor: pointer; overflow: hidden; }
@@ -91,14 +93,14 @@
.cell.mixed::after { content: ''; position: absolute; top: 0; right: 0; width: 7px; height: 7px; background: repeating-linear-gradient(135deg, var(--chart-unknown-base) 0 2px, var(--chart-unknown-stripe) 2px 4px); }
.cell[aria-pressed='true'] { outline: 1px solid var(--text-primary); outline-offset: 1px; }
.cell:focus-visible { outline: 2px solid var(--accent-primary-hover); outline-offset: 1px; }
.note { color: var(--text-tertiary); font-size: 11px; }
.note { color: var(--text-tertiary); font-size: var(--text-xs); }
.selectors { display: grid; grid-template-columns: repeat(2, minmax(0, 190px)); gap: var(--space-3); margin-top: var(--space-4); }
.readout { margin-top: var(--space-3); padding: var(--space-4); background: var(--surface-sunken); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
.readout > strong { font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--text-secondary); }
dl { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-3); margin: var(--space-3) 0 0; }
dt { font-size: 11px; color: var(--text-tertiary); }
dt { font-size: var(--text-xs); color: var(--text-tertiary); }
dd { margin: 5px 0 0; font-size: var(--text-md); font-variant-numeric: tabular-nums; }
small { display: block; margin-top: 4px; font-size: 11px; color: var(--text-tertiary); }
small { display: block; margin-top: 4px; font-size: var(--text-xs); color: var(--text-tertiary); }
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
@media (max-width: 640px) { .zone { flex-basis: 100%; } dl { grid-template-columns: repeat(2, minmax(0, 1fr)); } .readout { padding: var(--space-3); } .selectors { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px) { .heatmap { padding: var(--space-4); } .zone { flex-basis: 100%; } dl { grid-template-columns: repeat(2, minmax(0, 1fr)); } .readout { padding: var(--space-3); } .selectors { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
</style>
@@ -15,7 +15,7 @@
<div class="summary">
<Card><Stat label="Time with players" value={metricPercent(report.summary.active, report.summary.observed)} size="lg" tween={false} />
<p>{occupancyDelta === null ? 'Comparison unavailable' : `${signed(occupancyDelta, ' pp')} vs ${previousLabel}`}</p>
<p>{occupancyDelta === null ? 'Comparison unavailable' : `${signed(occupancyDelta, ' percentage points')} vs ${previousLabel}`}</p>
<small>{metricDuration(report.summary.active)} of {metricDuration(report.summary.observed)} observed instance time</small>
</Card>
<Card><Stat label="Player-hours" value={report.summary.observed > 0 ? metricNumber(report.summary.playerHours) : '—'} size="lg" tween={false} />
@@ -24,7 +24,7 @@
</Card>
<Card><Stat label="Time all lobbies were full" value={metricPercent(pressure.fullMapHours, pressure.knownMapHours)} size="lg" tween={false} />
<p>{pressure.knownMapHours > 0 ? `${pressure.coveredMaps} of ${pressure.totalMaps} maps have capacity data` : 'Capacity data unavailable'}</p>
<small>Pooled map time · not simultaneous fleet saturation</small>
<small>Combined map history · each map measured separately</small>
</Card>
</div>
{#if prior.partialHistory}<p class="notice">Partial previous history. Comparisons use available observations without extrapolation.</p>{/if}
@@ -117,7 +117,7 @@
.heading { padding: 20px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
h2 { font-size: var(--text-lg); margin: 0; }
.search { flex: 0 1 240px; }
.table-wrap { width: 100%; overflow-x: auto; }
.table-wrap { position: relative; width: 100%; overflow-x: auto; }
.table-wrap:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: -3px; }
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
th, td { padding: 12px 16px; border-bottom: 1px solid var(--border-subtle); text-align: right; font-variant-numeric: tabular-nums; vertical-align: top; }
+3 -3
View File
@@ -142,11 +142,11 @@
border-radius: var(--radius-pill);
background: var(--surface-overlay);
color: var(--text-secondary);
font-family: var(--font-mono);
font-family: var(--font-numeric);
font-size: var(--text-xs);
}
font-variant-numeric: tabular-nums; }
.tab.active .badge {
background: var(--accent-primary);
color: var(--text-on-accent);
}
font-variant-numeric: tabular-nums; }
</style>
+14
View File
@@ -1,5 +1,19 @@
const numberFormat = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 });
/** Label the actual bucket start in the chosen timezone; never imply local calendar-day buckets. */
export function metricPeriodLabels(points: readonly { startAt: number }[], days: 1 | 7 | 30, timeZone: string) {
const stride = days === 7 ? 1 : Math.max(1, Math.ceil(points.length / 6));
const weekday = new Intl.DateTimeFormat('en-US', { timeZone, weekday: 'short' });
const date = new Intl.DateTimeFormat('en-US', { timeZone, day: 'numeric' });
const clock = new Intl.DateTimeFormat('en-US', { timeZone, hour: 'numeric' });
return points.flatMap((point, index) => index % stride ? [] : [{
index,
position: (index + 0.5) / points.length * 100,
label: days === 1 ? clock.format(point.startAt) : weekday.format(point.startAt),
date: days === 30 ? date.format(point.startAt) : ''
}]);
}
export function metricNumber(value: number): string {
if (!Number.isFinite(value)) return '—';
if (value > 0 && value < 0.1) return '<0.1';
+5 -4
View File
@@ -8,7 +8,7 @@ import { type FleetServer, type FleetOperation } from '$lib/fleet-overview.js';
// Each server detail view owns a scoped snapshot; it must not replace the shared fleet.
export function createFleetStore(serverName?: string) {
const fleet = writable<{ servers: FleetServer[]; operations: FleetOperation[]; receivedAt: number }>({
const fleet = writable<{ servers: FleetServer[]; operations: FleetOperation[]; receivedAt: number; hostAttention?: number }>({
servers: [], operations: [], receivedAt: 0
});
const fleetError = writable('');
@@ -64,12 +64,13 @@ export function createFleetStore(serverName?: string) {
});
let snapshotServerNow = 0;
function hydrateFleet(snapshot: { servers: FleetServer[]; operations: FleetOperation[]; integrations: NonNullable<ReturnType<typeof getIntegrations>>; serverNow: number }) {
function hydrateFleet(snapshot: { servers: FleetServer[]; operations: FleetOperation[]; integrations: NonNullable<ReturnType<typeof getIntegrations>>; serverNow: number; hostAttention?: number }) {
if (!browser || snapshot.serverNow < snapshotServerNow) return;
snapshotServerNow = snapshot.serverNow;
syncServerTime(snapshot.serverNow);
fleetIntegrations.set(snapshot.integrations);
fleet.set({ servers: snapshot.servers, operations: snapshot.operations, receivedAt: Date.now() - Math.max(0, serverTimeNow() - snapshot.serverNow) });
fleet.set({ servers: snapshot.servers, operations: snapshot.operations, hostAttention: snapshot.hostAttention,
receivedAt: Date.now() - Math.max(0, serverTimeNow() - snapshot.serverNow) });
}
function getIntegrations() { return get(fleetIntegrations); }
@@ -92,7 +93,7 @@ export function createFleetStore(serverName?: string) {
if (servers.serverNow < snapshotServerNow) return;
snapshotServerNow = servers.serverNow;
syncServerTime(servers.serverNow); fleetIntegrations.set(servers.integrations);
fleet.update(value => ({ ...value, servers: servers.servers, receivedAt: requestedAt,
fleet.update(value => ({ ...value, servers: servers.servers, receivedAt: requestedAt, hostAttention: servers.hostAttention,
operations: serverName === undefined ? value.operations : servers.operations }));
})(),
(async () => {
+6 -85
View File
@@ -1,22 +1,18 @@
/* Electric Cherry — design tokens.
This file is the single source of truth for color, type, spacing, motion,
and elevation. It's organized in three layers:
and elevation. It's organized in two layers:
1. RAW RAMPS — the actual color/type/space values. Never referenced
directly by components.
2. SEMANTIC ALIASES — what components should reference (--surface,
--text-primary, --state-online, --space-4, etc.). Swapping the palette
or spacing rhythm later is a single-file edit.
3. LEGACY ALIASES — old names (--bg-page, --accent, --ink-300, --pad-card…)
mapped onto new semantic values. Kept alive so existing components render
during the phased redesign; each page rewrite removes its usage, and
this block shrinks over time.
Design principles this file encodes:
· One accent (blue). Cyan/amber/red/green mean state only.
· Depth by light, not lines. One ambient shadow + subtle translucency.
· One typeface (Inter). Mono for data. Display face lives inside BrandMark.
· One typeface (Inter). Inter tabular figures for metrics; mono for code and logs. Display face lives inside BrandMark.
· Motion tokens standardize easings + durations across the product. */
:root {
@@ -65,7 +61,7 @@
--text-primary: var(--gray-050);
--text-secondary: var(--gray-300);
--text-tertiary: #98a4b8;
--text-quaternary: var(--gray-400);
--text-quaternary: #8c96a9;
--text-on-accent: #08111f;
/* Border — hairlines, no more. */
@@ -82,11 +78,11 @@
/* Metrics: unknown uses a pattern as well as color. */
--chart-active: var(--accent-primary);
--chart-idle: #7b9baf;
--chart-idle: #2b3442;
--chart-unknown-base: #605b64;
--chart-unknown-stripe: #a7a0ad;
/* Typography — Inter everywhere, mono for data.
/* Typography — Inter for text and metrics; mono for code, IDs and logs.
Display face lives inside BrandMark; no global --font-display token. */
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-numeric: 'Inter', system-ui, -apple-system, sans-serif;
@@ -95,6 +91,7 @@
/* Type scale — cleaner ramp anchored at 15px body. */
--text-xs: 12px;
--text-sm: 13px;
--text-input: 16px; /* Prevent mobile browser zoom when a field receives focus. */
--text-base: 15px;
--text-md: 17px;
--text-lg: 20px;
@@ -156,72 +153,6 @@
/* Focus ring — 2px accent, 2px offset, everywhere. */
--focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent-primary);
/* ---------- 3. LEGACY ALIASES ----------
Old token names, mapped onto new semantic values so components that
haven't been rewritten yet keep rendering. Delete entries as pages
migrate; the block should be empty at the end of Phase 8. */
/* Old blue ramp names */
--blue-975: var(--gray-975);
--blue-925: var(--gray-950);
--blue-875: var(--gray-925);
--blue-825: var(--gray-850);
--blue-600: #256cae;
--blue-400: var(--blue-300);
/* Old spark/cherry/amber names */
--spark-500: var(--state-online);
--spark-400: #a8e5ff;
--spark-glow: 0 0 8px rgba(124, 212, 255, 0.45);
--spark-glow-lg: 0 0 14px rgba(124, 212, 255, 0.35);
--cherry-500: var(--state-danger);
--cherry-400: #ff7590;
--cherry-300: #ff9db0;
--amber-500: var(--state-warning);
--amber-glow: 0 0 8px rgba(255, 176, 32, 0.32);
--danger-500: var(--state-danger);
--success-500: #4ade80;
/* Old neutral names */
--cream: var(--text-primary);
--cream-dim: var(--gray-200);
--ink-100: var(--text-primary);
--ink-300: var(--text-secondary);
--ink-500: var(--text-tertiary);
--ink-700: var(--text-quaternary);
/* Old surface/border/accent aliases */
--bg-page: var(--surface-sunken);
--bg-nav: var(--surface);
--bg-card: var(--surface);
--bg-card-hi: var(--surface-raised);
--border: var(--border-subtle);
--border-hi: var(--border-strong);
--accent: var(--accent-primary);
--accent-hi: var(--accent-primary-hover);
--accent-glow: 0 0 0 1px rgba(46, 134, 209, 0.35);
--accent-glow-lg: 0 0 0 1px rgba(46, 134, 209, 0.28);
/* Old typography names */
--font-display: 'Alfa Slab One', 'Georgia', serif;
--text-mono: 13px;
--text-mono-sm: 12px;
--tracking-display: 0.12em;
--tracking-nav: 0.03em;
/* Old spacing/radii names */
--pad-card: var(--space-5);
--pad-panel: var(--space-6);
--pad-nav-y: var(--space-4);
--pad-nav-x: var(--space-8);
--gap-section: var(--space-8);
--gap-cards: var(--space-4);
--r-sm: var(--radius-sm);
--r-md: var(--radius-md);
--r-lg: var(--radius-lg);
--shadow-card: var(--shadow-ambient);
--shadow-panel: var(--shadow-raised);
}
/* Motion is reserved for active work and changes. */
@@ -231,16 +162,6 @@
to { background-position: 200% 0; }
}
/* Legacy — retained until ServerCard/BrandMark/StatusDot are replaced. */
@keyframes electric-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(124, 212, 255, 0.5); }
50% { box-shadow: 0 0 0 5px rgba(124, 212, 255, 0); }
}
@keyframes spark-slide {
from { background-position: -100% 0; }
to { background-position: 200% 0; }
}
/* Reduced motion — neutralize all animation. Components that respect this
token get calm behavior for free; the media query below handles the
ones that don't. */