feat(redesign): design-system tokens, 23 Vue components, game-aware shell + Fleet/Solo dashboard
All checks were successful
Test Asgard Runner / test (push) Successful in 4s

Tokens ported 1:1 from the Claude Design bundle (colors/game-themes/type/spacing/elevation/motion/fonts) with the data-theme/data-game theming contract via useThemeGame (+ cc-skin-swap repaint guard). 23 design-system components reimplemented as Vue SFCs (core/forms/data/navigation/feedback/brand). DashboardLayout rebuilt as the game-aware shell (GameSwitcher, grouped nav with permission gating preserved, agent-health footer, topbar). DashboardView: Fleet + Solo with per-game GAME_FIELDS rows and the themed ECharts PlayersChart; Solo wired to the real server store, Fleet on representative data pending the multi-instance backend. All four game skins (Rust/Dune/Conan/Soulmask). vue-tsc + vite build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 02:12:35 -04:00
parent ef128b47d2
commit f91ef84832
42 changed files with 3577 additions and 299 deletions

View File

@@ -0,0 +1,166 @@
<script setup lang="ts">
/**
* ServerCard — server instance summary card.
* Sets :data-game so per-game accent token re-skins apply via the global [data-game] selector.
* Status drives the dot + left rail color.
* `offline` dims the card and swaps the power IconButton to a Start action.
* Pending state shows when status==='online' && cpu==null && ram==null.
*/
import { computed } from 'vue'
import Badge from '@/components/ds/core/Badge.vue'
import Icon from '@/components/ds/core/Icon.vue'
import IconButton from '@/components/ds/core/IconButton.vue'
import ResourceMeter from './ResourceMeter.vue'
export interface StatItem {
label: string
value: string | number
}
const props = withDefaults(
defineProps<{
game?: string
gameIcon?: string
name: string
region?: string
map?: string
version?: string
status?: 'online' | 'offline' | 'starting' | 'wiping' | 'updating'
players?: { cur: number; max: number }
cpu?: number
ram?: number
ramSub?: string
ip?: string
stats?: StatItem[]
}>(),
{
game: 'rust',
gameIcon: 'box',
status: 'online',
players: () => ({ cur: 0, max: 0 }),
},
)
const emit = defineEmits<{
console: []
settings: []
power: []
}>()
interface StatusEntry {
tone: 'online' | 'offline' | 'warn' | 'info' | 'starting' | 'wiping' | 'neutral' | 'accent'
label: string
pulse: boolean
}
const DEFAULT_STATUS: StatusEntry = { tone: 'online', label: 'Online', pulse: true }
const STATUS_MAP: Record<string, StatusEntry> = {
online: { tone: 'online', label: 'Online', pulse: true },
offline: { tone: 'offline', label: 'Offline', pulse: false },
starting: { tone: 'starting', label: 'Booting', pulse: true },
wiping: { tone: 'wiping', label: 'Wiping', pulse: true },
updating: { tone: 'starting', label: 'Updating', pulse: true },
}
const st = computed<StatusEntry>(() => STATUS_MAP[props.status ?? 'online'] ?? DEFAULT_STATUS)
const offline = computed(() => props.status === 'offline')
const statList = computed<StatItem[]>(() => {
if (props.stats) return props.stats
const items: StatItem[] = [
{ label: 'Players', value: `${props.players?.cur ?? 0} / ${props.players?.max ?? 0}` },
]
if (props.version) {
items.push({ label: 'Build', value: props.version })
}
return items
})
const pending = computed(
() => props.status === 'online' && props.cpu == null && props.ram == null,
)
const showMeters = computed(
() => !offline.value && (props.cpu != null || props.ram != null),
)
</script>
<template>
<div
:data-game="game"
:class="['cc-server', offline && 'cc-server--offline']"
>
<!-- Head -->
<div class="cc-server__head">
<div class="cc-server__game">
<Icon :name="gameIcon" :size="18" :stroke-width="2" />
</div>
<div class="cc-server__id">
<div class="cc-server__name">
{{ name }}
<Badge :tone="st.tone" dot :pulse="st.pulse">{{ st.label }}</Badge>
</div>
<div class="cc-server__meta">
<span v-if="region">{{ region }}</span>
<span v-if="map">{{ map }}</span>
<span v-if="ip">{{ ip }}</span>
</div>
</div>
<div class="cc-server__actions">
<IconButton icon="terminal" variant="ghost" size="sm" label="Console" @click="emit('console')" />
<IconButton icon="settings" variant="ghost" size="sm" label="Settings" @click="emit('settings')" />
<IconButton
:icon="offline ? 'play' : 'power'"
:variant="offline ? 'accent' : 'ghost'"
size="sm"
:label="offline ? 'Start' : 'Power'"
@click="emit('power')"
/>
</div>
</div>
<!-- Body -->
<div class="cc-server__body">
<div class="cc-server__stats">
<div v-for="(s, i) in statList" :key="i" class="cc-server__stat">
<b>{{ s.value }}</b>
<span>{{ s.label }}</span>
</div>
</div>
<div v-if="showMeters" class="cc-server__meters">
<ResourceMeter v-if="cpu != null" label="CPU" :value="cpu" />
<ResourceMeter v-if="ram != null" label="RAM" :value="ram" :sub="ramSub" />
</div>
<div v-if="pending" class="cc-server__pending">
<Icon name="loader" :size="13" :stroke-width="2.5" />
Telemetry pending · agent monitoring
</div>
</div>
</div>
</template>
<style>
.cc-server { position: relative; background: var(--surface-base); border-radius: var(--radius-lg); box-shadow: var(--ring-default); overflow: hidden; transition: var(--transition-colors); }
.cc-server::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--accent); opacity: .9; }
.cc-server:hover { box-shadow: inset 0 0 0 1px var(--border-strong); }
.cc-server--offline::before { background: var(--status-offline); }
.cc-server--offline { opacity: .82; }
.cc-server__head { display: flex; align-items: center; gap: 12px; padding: 14px 14px 12px 17px; }
.cc-server__game { width: 34px; height: 34px; border-radius: var(--radius-md); flex: none; display: flex; align-items: center; justify-content: center; color: var(--accent); background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-border); }
.cc-server__id { flex: 1; min-width: 0; }
.cc-server__name { font-size: var(--text-base); font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; gap: 8px; }
.cc-server__meta { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; display: flex; gap: 10px; }
.cc-server__body { padding: 0 14px 13px 17px; display: flex; flex-direction: column; gap: 11px; }
.cc-server__stats { display: flex; gap: 18px; }
.cc-server__stat { display: flex; flex-direction: column; gap: 1px; }
.cc-server__stat b { font-family: var(--font-mono); font-weight: 600; font-size: var(--text-sm); color: var(--text-primary); font-variant-numeric: tabular-nums; }
.cc-server__stat span { font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; }
.cc-server__meters { display: flex; gap: 14px; }
.cc-server__meters > * { flex: 1; }
.cc-server__pending { display: flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); }
.cc-server__pending .cc-icon { color: var(--status-starting); }
.cc-server__actions { display: flex; gap: 5px; }
</style>