114 lines
3.6 KiB
Svelte
114 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import type { RosterPlayer, Health } from '$ui/stores/servers.js';
|
|
|
|
interface Props {
|
|
roster: RosterPlayer[] | null;
|
|
health: Health;
|
|
compact?: boolean; // hide guid column, cap rows
|
|
limit?: number;
|
|
}
|
|
let { roster, health, compact = false, limit }: Props = $props();
|
|
|
|
let sorted = $derived(
|
|
(roster ?? [])
|
|
.slice()
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, limit ?? Infinity)
|
|
);
|
|
|
|
function pingClass(ping: number): string {
|
|
if (ping < 60) return 'ping-good';
|
|
if (ping < 150) return 'ping-ok';
|
|
return 'ping-bad';
|
|
}
|
|
</script>
|
|
|
|
{#if health === 'starting'}
|
|
<div class="state empty">Server starting</div>
|
|
{:else if health === 'offline'}
|
|
<div class="state offline">server unreachable</div>
|
|
{:else if !roster || roster.length === 0}
|
|
<div class="state empty">lobby is empty</div>
|
|
{:else}
|
|
<table class="roster" class:compact>
|
|
<thead>
|
|
<tr>
|
|
{#if !compact}<th class="col-num">#</th>{/if}
|
|
<th class="col-name">player</th>
|
|
<th class="col-score">score</th>
|
|
<th class="col-ping">ping</th>
|
|
{#if !compact}<th class="col-guid">guid</th>{/if}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each sorted as p (p.guid ?? p.name + '-' + p.num)}
|
|
<tr>
|
|
{#if !compact}<td class="col-num mono">{p.num ?? ''}</td>{/if}
|
|
<td class="col-name">{p.name || '—'}</td>
|
|
<td class="col-score mono">{p.score}</td>
|
|
<td class="col-ping mono {pingClass(p.ping)}">{p.ping}</td>
|
|
{#if !compact}<td class="col-guid mono">{p.guid ? (p.guid.length > 12 ? p.guid.slice(0, 10) + '…' : p.guid) : '—'}</td>{/if}
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
{/if}
|
|
|
|
<style>
|
|
.state {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-mono);
|
|
padding: 14px 16px;
|
|
border-radius: var(--r-sm);
|
|
}
|
|
.state.offline {
|
|
color: var(--danger-500);
|
|
background: rgba(255, 48, 80, 0.08);
|
|
border: 1px solid rgba(255, 48, 80, 0.25);
|
|
}
|
|
.state.empty {
|
|
color: var(--ink-500);
|
|
background: var(--bg-nav);
|
|
border: 1px dashed var(--border);
|
|
}
|
|
|
|
.roster {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
font-family: var(--font-body);
|
|
font-size: var(--text-base);
|
|
color: var(--ink-100);
|
|
}
|
|
.roster thead th {
|
|
text-align: left;
|
|
font-family: var(--font-body);
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1.2px;
|
|
color: var(--ink-500);
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.roster tbody td {
|
|
padding: 12px;
|
|
border-bottom: 1px solid rgba(30, 50, 96, 0.5);
|
|
}
|
|
.roster tbody tr:hover td {
|
|
background: rgba(124, 192, 245, 0.06);
|
|
}
|
|
.roster.compact tbody td { padding: 6px 10px; font-size: var(--text-sm); }
|
|
|
|
.mono { font-family: var(--font-mono); }
|
|
.col-num { width: 48px; color: var(--ink-500); }
|
|
.col-name { color: var(--cream); font-weight: 500; }
|
|
.col-score { width: 100px; color: var(--spark-500); }
|
|
.col-ping { width: 90px; }
|
|
.col-guid { width: 120px; color: var(--ink-500); }
|
|
|
|
.ping-good { color: var(--spark-400); }
|
|
.ping-ok { color: var(--cream-dim); }
|
|
.ping-bad { color: var(--amber-500); }
|
|
</style>
|