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
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:
98
frontend/src/components/ds/data/PlayersChart.vue
Normal file
98
frontend/src/components/ds/data/PlayersChart.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* PlayersChart — themed ECharts area chart of players online.
|
||||
* Reads the live design tokens (--accent etc.) from CSS so it matches the
|
||||
* active theme/game, and re-renders when data-game / data-theme flip on <html>.
|
||||
*/
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ height?: number; data?: number[]; max?: number }>(),
|
||||
{ height: 200, max: 200 },
|
||||
)
|
||||
|
||||
const el = useTemplateRef<HTMLDivElement>('el')
|
||||
let chart: echarts.ECharts | null = null
|
||||
let ro: ResizeObserver | null = null
|
||||
let mo: MutationObserver | null = null
|
||||
|
||||
const DEFAULT_SERIES = [
|
||||
60, 52, 44, 38, 33, 30, 34, 46, 62, 78, 92, 104,
|
||||
118, 126, 131, 138, 142, 151, 168, 182, 176, 150, 112, 84,
|
||||
]
|
||||
|
||||
function cssVar(name: string, node?: HTMLElement): string {
|
||||
return getComputedStyle(node || document.documentElement).getPropertyValue(name).trim()
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
if (!chart || !el.value) return
|
||||
const node = el.value
|
||||
const accent = cssVar('--accent', node) || '#f26622'
|
||||
const grid = cssVar('--border-subtle', node) || 'rgba(255,255,255,0.06)'
|
||||
const text = cssVar('--text-tertiary', node) || '#767d89'
|
||||
const mono = 'JetBrains Mono, monospace'
|
||||
const hours = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`)
|
||||
const series = props.data ?? DEFAULT_SERIES
|
||||
|
||||
chart.setOption({
|
||||
animationDuration: 700,
|
||||
grid: { left: 8, right: 12, top: 14, bottom: 22, containLabel: true },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: cssVar('--surface-overlay', node) || '#1f2329',
|
||||
borderColor: cssVar('--border-default', node) || 'rgba(255,255,255,0.1)',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: cssVar('--text-primary', node) || '#fff', fontFamily: mono, fontSize: 11 },
|
||||
axisPointer: { type: 'line', lineStyle: { color: accent, opacity: 0.5 } },
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category', data: hours, boundaryGap: false,
|
||||
axisLine: { lineStyle: { color: grid } }, axisTick: { show: false },
|
||||
axisLabel: { color: text, fontFamily: mono, fontSize: 10, interval: 3 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', max: props.max,
|
||||
splitLine: { lineStyle: { color: grid } },
|
||||
axisLabel: { color: text, fontFamily: mono, fontSize: 10 },
|
||||
},
|
||||
series: [{
|
||||
type: 'line', smooth: 0.4, symbol: 'none', data: series,
|
||||
lineStyle: { color: accent, width: 2 },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: accent + '55' },
|
||||
{ offset: 1, color: accent + '00' },
|
||||
]),
|
||||
},
|
||||
markLine: {
|
||||
silent: true, symbol: 'none',
|
||||
lineStyle: { color: text, type: 'dashed', opacity: 0.5 },
|
||||
data: [{ yAxis: props.max, label: { formatter: `cap ${props.max}`, color: text, fontFamily: mono, fontSize: 9 } }],
|
||||
},
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!el.value) return
|
||||
chart = echarts.init(el.value, undefined, { renderer: 'canvas' })
|
||||
render()
|
||||
ro = new ResizeObserver(() => chart?.resize())
|
||||
ro.observe(el.value)
|
||||
mo = new MutationObserver(render)
|
||||
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-game', 'data-theme'] })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ro?.disconnect()
|
||||
mo?.disconnect()
|
||||
chart?.dispose()
|
||||
chart = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="el" :style="{ width: '100%', height: height + 'px' }" />
|
||||
</template>
|
||||
Reference in New Issue
Block a user