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:
114
frontend/src/composables/useThemeGame.ts
Normal file
114
frontend/src/composables/useThemeGame.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* useThemeGame — the Corrosion design-system theming contract.
|
||||
*
|
||||
* Drives `data-theme` and `data-game` on <html>, the two attributes the token
|
||||
* system keys off (see styles/tokens/colors.css + game-themes.css):
|
||||
* <html data-theme="dark|light" data-game="rust|dune|conan|soulmask|...">
|
||||
*
|
||||
* Dark is primary; Rust (Oxide Orange) is the default/brand accent.
|
||||
*
|
||||
* Runtime swaps add the `cc-skin-swap` class for one frame so every
|
||||
* accent-consuming surface repaints immediately — without it Chrome leaves
|
||||
* elements that read var(--accent) AND have a color/bg transition on the old
|
||||
* accent until the next reflow (see styles/tokens/base.css + readme).
|
||||
*/
|
||||
import { ref, readonly } from 'vue'
|
||||
|
||||
export type Theme = 'dark' | 'light'
|
||||
export type Game =
|
||||
| 'rust'
|
||||
| 'dune'
|
||||
| 'conan'
|
||||
| 'soulmask'
|
||||
| 'ark'
|
||||
| 'valheim'
|
||||
| 'palworld'
|
||||
|
||||
/** The fleet filter: 'all' (every game) plus each individual game. */
|
||||
export type ActiveGame = 'all' | Game
|
||||
|
||||
const THEME_KEY = 'cc-theme'
|
||||
const GAME_KEY = 'cc-game'
|
||||
const ACTIVE_GAME_KEY = 'cc-active-game'
|
||||
|
||||
const VALID_THEMES: readonly Theme[] = ['dark', 'light']
|
||||
const VALID_GAMES: readonly Game[] = [
|
||||
'rust',
|
||||
'dune',
|
||||
'conan',
|
||||
'soulmask',
|
||||
'ark',
|
||||
'valheim',
|
||||
'palworld',
|
||||
]
|
||||
|
||||
// Module-scope singletons so every caller shares one reactive source.
|
||||
const theme = ref<Theme>('dark')
|
||||
const game = ref<Game>('rust')
|
||||
// Fleet filter: 'all' shows every game and uses the neutral house skin (Oxide);
|
||||
// a specific game both filters the fleet AND re-skins the shell (the drill-in rule).
|
||||
const activeGame = ref<ActiveGame>('all')
|
||||
|
||||
function apply(): void {
|
||||
const el = document.documentElement
|
||||
el.classList.add('cc-skin-swap')
|
||||
el.setAttribute('data-theme', theme.value)
|
||||
el.setAttribute('data-game', game.value)
|
||||
// Keep Tailwind's `dark` class in sync — existing views may use `dark:` utilities.
|
||||
el.classList.toggle('dark', theme.value === 'dark')
|
||||
// Drop the swap guard after the paint that picked up the new accent.
|
||||
requestAnimationFrame(() =>
|
||||
requestAnimationFrame(() => el.classList.remove('cc-skin-swap')),
|
||||
)
|
||||
}
|
||||
|
||||
let initialized = false
|
||||
|
||||
/**
|
||||
* Read persisted prefs and apply them to <html>. Call once at app start
|
||||
* (after a tiny inline FOUC guard in index.html has set the initial attrs).
|
||||
*/
|
||||
export function initThemeGame(): void {
|
||||
if (initialized) return
|
||||
const t = localStorage.getItem(THEME_KEY)
|
||||
if (t && (VALID_THEMES as string[]).includes(t)) theme.value = t as Theme
|
||||
const ag = localStorage.getItem(ACTIVE_GAME_KEY)
|
||||
if (ag && (ag === 'all' || (VALID_GAMES as string[]).includes(ag))) {
|
||||
activeGame.value = ag as ActiveGame
|
||||
}
|
||||
// Skin follows the filter: 'all' -> neutral house (rust/oxide), else the game.
|
||||
game.value = activeGame.value === 'all' ? 'rust' : activeGame.value
|
||||
apply()
|
||||
initialized = true
|
||||
}
|
||||
|
||||
export function useThemeGame() {
|
||||
function setTheme(t: Theme): void {
|
||||
theme.value = t
|
||||
localStorage.setItem(THEME_KEY, t)
|
||||
apply()
|
||||
}
|
||||
function setGame(g: Game): void {
|
||||
game.value = g
|
||||
localStorage.setItem(GAME_KEY, g)
|
||||
apply()
|
||||
}
|
||||
function setActiveGame(g: ActiveGame): void {
|
||||
activeGame.value = g
|
||||
localStorage.setItem(ACTIVE_GAME_KEY, g)
|
||||
// 'all' uses the neutral house skin (rust/oxide); a game re-skins to itself.
|
||||
setGame(g === 'all' ? 'rust' : g)
|
||||
}
|
||||
function toggleTheme(): void {
|
||||
setTheme(theme.value === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
return {
|
||||
theme: readonly(theme),
|
||||
game: readonly(game),
|
||||
activeGame: readonly(activeGame),
|
||||
setTheme,
|
||||
setGame,
|
||||
setActiveGame,
|
||||
toggleTheme,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user