From f91ef8483206c1e74b35268198ed0a3b8b6419ed Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 11 Jun 2026 02:12:35 -0400 Subject: [PATCH 1/7] feat(redesign): design-system tokens, 23 Vue components, game-aware shell + Fleet/Solo dashboard 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 --- frontend/index.html | 20 +- frontend/src/assets/corrosion-mark.svg | 8 + .../src/components/brand/CorrosionMark.vue | 29 + frontend/src/components/ds/brand/Logo.vue | 92 +++ frontend/src/components/ds/core/Badge.vue | 62 ++ frontend/src/components/ds/core/Button.vue | 82 +++ frontend/src/components/ds/core/Icon.vue | 73 ++ .../src/components/ds/core/IconButton.vue | 62 ++ frontend/src/components/ds/core/Kbd.vue | 20 + frontend/src/components/ds/core/StatusDot.vue | 48 ++ frontend/src/components/ds/core/Tag.vue | 52 ++ frontend/src/components/ds/data/Avatar.vue | 72 ++ .../src/components/ds/data/ConsoleLine.vue | 54 ++ frontend/src/components/ds/data/Panel.vue | 57 ++ .../src/components/ds/data/PlayersChart.vue | 98 +++ .../src/components/ds/data/ResourceMeter.vue | 53 ++ .../src/components/ds/data/ServerCard.vue | 166 +++++ frontend/src/components/ds/data/StatCard.vue | 62 ++ frontend/src/components/ds/feedback/Alert.vue | 86 +++ .../src/components/ds/feedback/EmptyState.vue | 39 + frontend/src/components/ds/forms/Checkbox.vue | 50 ++ frontend/src/components/ds/forms/Input.vue | 90 +++ frontend/src/components/ds/forms/Select.vue | 86 +++ frontend/src/components/ds/forms/Switch.vue | 59 ++ .../components/ds/navigation/GameSwitcher.vue | 71 ++ .../src/components/ds/navigation/NavItem.vue | 50 ++ .../src/components/ds/navigation/Tabs.vue | 67 ++ .../src/components/layout/DashboardLayout.vue | 683 ++++++++++++++---- frontend/src/composables/useThemeGame.ts | 114 +++ frontend/src/main.ts | 4 + frontend/src/style.css | 17 +- frontend/src/styles/corrosion.css | 15 + frontend/src/styles/tokens/base.css | 75 ++ frontend/src/styles/tokens/colors.css | 136 ++++ frontend/src/styles/tokens/elevation.css | 40 + frontend/src/styles/tokens/fonts.css | 21 + frontend/src/styles/tokens/game-themes.css | 150 ++++ frontend/src/styles/tokens/motion.css | 27 + frontend/src/styles/tokens/spacing.css | 50 ++ frontend/src/styles/tokens/typography.css | 75 ++ frontend/src/views/admin/DashboardView.vue | 602 +++++++++++---- frontend/src/views/admin/_dashboardMock.ts | 159 ++++ 42 files changed, 3577 insertions(+), 299 deletions(-) create mode 100644 frontend/src/assets/corrosion-mark.svg create mode 100644 frontend/src/components/brand/CorrosionMark.vue create mode 100644 frontend/src/components/ds/brand/Logo.vue create mode 100644 frontend/src/components/ds/core/Badge.vue create mode 100644 frontend/src/components/ds/core/Button.vue create mode 100644 frontend/src/components/ds/core/Icon.vue create mode 100644 frontend/src/components/ds/core/IconButton.vue create mode 100644 frontend/src/components/ds/core/Kbd.vue create mode 100644 frontend/src/components/ds/core/StatusDot.vue create mode 100644 frontend/src/components/ds/core/Tag.vue create mode 100644 frontend/src/components/ds/data/Avatar.vue create mode 100644 frontend/src/components/ds/data/ConsoleLine.vue create mode 100644 frontend/src/components/ds/data/Panel.vue create mode 100644 frontend/src/components/ds/data/PlayersChart.vue create mode 100644 frontend/src/components/ds/data/ResourceMeter.vue create mode 100644 frontend/src/components/ds/data/ServerCard.vue create mode 100644 frontend/src/components/ds/data/StatCard.vue create mode 100644 frontend/src/components/ds/feedback/Alert.vue create mode 100644 frontend/src/components/ds/feedback/EmptyState.vue create mode 100644 frontend/src/components/ds/forms/Checkbox.vue create mode 100644 frontend/src/components/ds/forms/Input.vue create mode 100644 frontend/src/components/ds/forms/Select.vue create mode 100644 frontend/src/components/ds/forms/Switch.vue create mode 100644 frontend/src/components/ds/navigation/GameSwitcher.vue create mode 100644 frontend/src/components/ds/navigation/NavItem.vue create mode 100644 frontend/src/components/ds/navigation/Tabs.vue create mode 100644 frontend/src/composables/useThemeGame.ts create mode 100644 frontend/src/styles/corrosion.css create mode 100644 frontend/src/styles/tokens/base.css create mode 100644 frontend/src/styles/tokens/colors.css create mode 100644 frontend/src/styles/tokens/elevation.css create mode 100644 frontend/src/styles/tokens/fonts.css create mode 100644 frontend/src/styles/tokens/game-themes.css create mode 100644 frontend/src/styles/tokens/motion.css create mode 100644 frontend/src/styles/tokens/spacing.css create mode 100644 frontend/src/styles/tokens/typography.css create mode 100644 frontend/src/views/admin/_dashboardMock.ts diff --git a/frontend/index.html b/frontend/index.html index 71230e0..35a0aca 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,5 @@ - + @@ -9,8 +9,24 @@ Corrosion Management + - +
diff --git a/frontend/src/assets/corrosion-mark.svg b/frontend/src/assets/corrosion-mark.svg new file mode 100644 index 0000000..95fd3b4 --- /dev/null +++ b/frontend/src/assets/corrosion-mark.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/brand/CorrosionMark.vue b/frontend/src/components/brand/CorrosionMark.vue new file mode 100644 index 0000000..c492d46 --- /dev/null +++ b/frontend/src/components/brand/CorrosionMark.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/components/ds/brand/Logo.vue b/frontend/src/components/ds/brand/Logo.vue new file mode 100644 index 0000000..15fa7d7 --- /dev/null +++ b/frontend/src/components/ds/brand/Logo.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/frontend/src/components/ds/core/Badge.vue b/frontend/src/components/ds/core/Badge.vue new file mode 100644 index 0000000..1ebe068 --- /dev/null +++ b/frontend/src/components/ds/core/Badge.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/frontend/src/components/ds/core/Button.vue b/frontend/src/components/ds/core/Button.vue new file mode 100644 index 0000000..bcd1190 --- /dev/null +++ b/frontend/src/components/ds/core/Button.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/components/ds/core/Icon.vue b/frontend/src/components/ds/core/Icon.vue new file mode 100644 index 0000000..94419d6 --- /dev/null +++ b/frontend/src/components/ds/core/Icon.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/frontend/src/components/ds/core/IconButton.vue b/frontend/src/components/ds/core/IconButton.vue new file mode 100644 index 0000000..191e437 --- /dev/null +++ b/frontend/src/components/ds/core/IconButton.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/frontend/src/components/ds/core/Kbd.vue b/frontend/src/components/ds/core/Kbd.vue new file mode 100644 index 0000000..1e86492 --- /dev/null +++ b/frontend/src/components/ds/core/Kbd.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/frontend/src/components/ds/core/StatusDot.vue b/frontend/src/components/ds/core/StatusDot.vue new file mode 100644 index 0000000..264d398 --- /dev/null +++ b/frontend/src/components/ds/core/StatusDot.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/frontend/src/components/ds/core/Tag.vue b/frontend/src/components/ds/core/Tag.vue new file mode 100644 index 0000000..d6a696e --- /dev/null +++ b/frontend/src/components/ds/core/Tag.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/frontend/src/components/ds/data/Avatar.vue b/frontend/src/components/ds/data/Avatar.vue new file mode 100644 index 0000000..c0a76f8 --- /dev/null +++ b/frontend/src/components/ds/data/Avatar.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/frontend/src/components/ds/data/ConsoleLine.vue b/frontend/src/components/ds/data/ConsoleLine.vue new file mode 100644 index 0000000..a92ad3c --- /dev/null +++ b/frontend/src/components/ds/data/ConsoleLine.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/components/ds/data/Panel.vue b/frontend/src/components/ds/data/Panel.vue new file mode 100644 index 0000000..eb936d4 --- /dev/null +++ b/frontend/src/components/ds/data/Panel.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/frontend/src/components/ds/data/PlayersChart.vue b/frontend/src/components/ds/data/PlayersChart.vue new file mode 100644 index 0000000..834cb9e --- /dev/null +++ b/frontend/src/components/ds/data/PlayersChart.vue @@ -0,0 +1,98 @@ + + + diff --git a/frontend/src/components/ds/data/ResourceMeter.vue b/frontend/src/components/ds/data/ResourceMeter.vue new file mode 100644 index 0000000..32e87e1 --- /dev/null +++ b/frontend/src/components/ds/data/ResourceMeter.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/frontend/src/components/ds/data/ServerCard.vue b/frontend/src/components/ds/data/ServerCard.vue new file mode 100644 index 0000000..88279e1 --- /dev/null +++ b/frontend/src/components/ds/data/ServerCard.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/frontend/src/components/ds/data/StatCard.vue b/frontend/src/components/ds/data/StatCard.vue new file mode 100644 index 0000000..e244450 --- /dev/null +++ b/frontend/src/components/ds/data/StatCard.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/frontend/src/components/ds/feedback/Alert.vue b/frontend/src/components/ds/feedback/Alert.vue new file mode 100644 index 0000000..056be13 --- /dev/null +++ b/frontend/src/components/ds/feedback/Alert.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/frontend/src/components/ds/feedback/EmptyState.vue b/frontend/src/components/ds/feedback/EmptyState.vue new file mode 100644 index 0000000..7e78037 --- /dev/null +++ b/frontend/src/components/ds/feedback/EmptyState.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/frontend/src/components/ds/forms/Checkbox.vue b/frontend/src/components/ds/forms/Checkbox.vue new file mode 100644 index 0000000..600e5b5 --- /dev/null +++ b/frontend/src/components/ds/forms/Checkbox.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/src/components/ds/forms/Input.vue b/frontend/src/components/ds/forms/Input.vue new file mode 100644 index 0000000..84a2a77 --- /dev/null +++ b/frontend/src/components/ds/forms/Input.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/frontend/src/components/ds/forms/Select.vue b/frontend/src/components/ds/forms/Select.vue new file mode 100644 index 0000000..3e27f0b --- /dev/null +++ b/frontend/src/components/ds/forms/Select.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/frontend/src/components/ds/forms/Switch.vue b/frontend/src/components/ds/forms/Switch.vue new file mode 100644 index 0000000..77d651a --- /dev/null +++ b/frontend/src/components/ds/forms/Switch.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/frontend/src/components/ds/navigation/GameSwitcher.vue b/frontend/src/components/ds/navigation/GameSwitcher.vue new file mode 100644 index 0000000..43ad82a --- /dev/null +++ b/frontend/src/components/ds/navigation/GameSwitcher.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/frontend/src/components/ds/navigation/NavItem.vue b/frontend/src/components/ds/navigation/NavItem.vue new file mode 100644 index 0000000..d10d33e --- /dev/null +++ b/frontend/src/components/ds/navigation/NavItem.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/src/components/ds/navigation/Tabs.vue b/frontend/src/components/ds/navigation/Tabs.vue new file mode 100644 index 0000000..c698356 --- /dev/null +++ b/frontend/src/components/ds/navigation/Tabs.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/frontend/src/components/layout/DashboardLayout.vue b/frontend/src/components/layout/DashboardLayout.vue index 43acc33..e2868a1 100644 --- a/frontend/src/components/layout/DashboardLayout.vue +++ b/frontend/src/components/layout/DashboardLayout.vue @@ -1,103 +1,120 @@ + + diff --git a/frontend/src/composables/useThemeGame.ts b/frontend/src/composables/useThemeGame.ts new file mode 100644 index 0000000..8283bfe --- /dev/null +++ b/frontend/src/composables/useThemeGame.ts @@ -0,0 +1,114 @@ +/** + * useThemeGame — the Corrosion design-system theming contract. + * + * Drives `data-theme` and `data-game` on , the two attributes the token + * system keys off (see styles/tokens/colors.css + game-themes.css): + * + * + * 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('dark') +const game = ref('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('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 . 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, + } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index c7816df..4acb41b 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -5,6 +5,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import { VueFinderPlugin } from 'vuefinder' import App from './App.vue' import router from './router' +import { initThemeGame } from './composables/useThemeGame' import './style.css' import 'vuefinder/dist/vuefinder.css' @@ -17,4 +18,7 @@ app.use(pinia) app.use(router) app.use(VueFinderPlugin) +// Apply the design-system theming contract (data-theme/data-game on ). +initThemeGame() + app.mount('#app') diff --git a/frontend/src/style.css b/frontend/src/style.css index ea6fec3..88d6d34 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,6 +1,10 @@ @import "tailwindcss"; +@import "./styles/corrosion.css"; -/* Corrosion Brand — Oxide Orange #F26622 */ +/* Tailwind utility colors — Oxide ramp (existing views use bg-oxide-*). + The full design-token system (neutral ramp, surfaces, per-game accents, + typography, spacing, elevation, motion) lives in ./styles/ and is the + source of truth for the redesign. */ @theme { --color-oxide-50: #FEF3EB; --color-oxide-100: #FDE3D0; @@ -15,7 +19,8 @@ --color-oxide-950: #3D1506; } -/* Dark mode is default — Rust servers run at night */ +/* Legacy brand vars — retained during the redesign port so any view still + referencing them keeps working; superseded by the ./styles tokens. */ :root { --corrosion-accent: #F26622; --corrosion-dark: #000000; @@ -24,12 +29,8 @@ --corrosion-border: #2a2a2a; } -body { - @apply bg-neutral-950 text-neutral-100 antialiased; - margin: 0; - min-height: 100vh; -} - +/* Body background / text / font now come from the design system + (./styles/tokens/base.css → var(--surface-canvas), var(--text-primary)). */ #app { min-height: 100vh; } diff --git a/frontend/src/styles/corrosion.css b/frontend/src/styles/corrosion.css new file mode 100644 index 0000000..472b0ef --- /dev/null +++ b/frontend/src/styles/corrosion.css @@ -0,0 +1,15 @@ +/* ============================================================ + Corrosion Control — Design System + Root stylesheet. Consumers link THIS one file. + Import order matters: fonts → primitives → colors → game themes + → base. (base.css references color/accent tokens.) + ============================================================ */ + +@import url("tokens/fonts.css"); +@import url("tokens/spacing.css"); +@import url("tokens/typography.css"); +@import url("tokens/motion.css"); +@import url("tokens/colors.css"); +@import url("tokens/game-themes.css"); +@import url("tokens/elevation.css"); +@import url("tokens/base.css"); diff --git a/frontend/src/styles/tokens/base.css b/frontend/src/styles/tokens/base.css new file mode 100644 index 0000000..4fb95a4 --- /dev/null +++ b/frontend/src/styles/tokens/base.css @@ -0,0 +1,75 @@ +/* ============================================================ + Corrosion Control — Base / Reset + Minimal, opinionated. Applies the token system to bare HTML so + specimen cards and kits inherit the look without boilerplate. + ============================================================ */ + +*, *::before, *::after { box-sizing: border-box; } + +html { -webkit-text-size-adjust: 100%; } + +body { + margin: 0; + font-family: var(--font-sans); + font-size: var(--text-base); + line-height: var(--leading-normal); + color: var(--text-primary); + background-color: var(--surface-canvas); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-feature-settings: "cv01", "ss01"; +} + +h1, h2, h3, h4, h5, h6, p, figure { margin: 0; } + +a { color: inherit; text-decoration: none; } + +::selection { + background: var(--accent-soft-strong); + color: var(--text-primary); +} + +/* Tabular numbers everywhere numbers matter */ +.tnum { font-variant-numeric: tabular-nums; } + +/* Custom scrollbars — quiet, on-brand */ +* { + scrollbar-width: thin; + scrollbar-color: var(--border-strong) transparent; +} +*::-webkit-scrollbar { width: 10px; height: 10px; } +*::-webkit-scrollbar-track { background: transparent; } +*::-webkit-scrollbar-thumb { + background: var(--border-strong); + border-radius: var(--radius-pill); + border: 2px solid transparent; + background-clip: content-box; +} +*::-webkit-scrollbar-thumb:hover { background: var(--text-muted); background-clip: content-box; } + +/* Focus-visible default */ +:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} + +/* Reduced motion guard */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* ---- Skin-swap repaint guard ---- + When flipping data-game or data-theme on the root, add the .cc-skin-swap + class for ONE frame. This suppresses transitions during the swap so every + accent-consuming surface repaints immediately — works around a Chrome + custom-property + transition staleness where elements that both read + var(--accent)/var(--accent-text) AND have a color/background transition keep + the old accent until the next reflow. See readme "Theming contract". */ +.cc-skin-swap *, +.cc-skin-swap *::before, +.cc-skin-swap *::after { transition: none !important; } diff --git a/frontend/src/styles/tokens/colors.css b/frontend/src/styles/tokens/colors.css new file mode 100644 index 0000000..8382fbe --- /dev/null +++ b/frontend/src/styles/tokens/colors.css @@ -0,0 +1,136 @@ +/* ============================================================ + Corrosion Control — Color System + ------------------------------------------------------------ + Three layers: + 1. Raw neutral ramp (absolute; identical in both themes) + 2. Semantic surface / text / border tokens (flip per theme) + 3. Status colors (online/offline/warn/info/...) — game-agnostic + Game ACCENT colors live in game-themes.css. + + Theme + game are set together on : + + Dark is primary. Light is full-parity. + ============================================================ */ + +:root { + /* ---- Raw neutral ramp (warm-cool slate, absolute) ---- */ + --n-0: #ffffff; + --n-25: #fafbfc; + --n-50: #f3f5f7; + --n-100: #e8ebef; + --n-150: #dce0e6; + --n-200: #ccd2da; + --n-300: #aeb6c1; + --n-400: #8a929e; + --n-500: #6b7280; + --n-600: #515862; + --n-650: #444a53; + --n-700: #363b43; + --n-750: #2b2f36; + --n-800: #1f2329; + --n-850: #181b20; + --n-900: #121419; + --n-925: #0e0f13; + --n-950: #0a0b0e; + --n-975: #060709; + + /* ---- Semantic: DARK (default) ---- */ + --surface-canvas: var(--n-950); /* app background */ + --surface-sunken: var(--n-975); /* wells, deep insets */ + --surface-base: var(--n-925); /* primary panels */ + --surface-raised: var(--n-850); /* cards, rows */ + --surface-raised-2:var(--n-800); /* nested cards, hover cards */ + --surface-overlay: var(--n-800); /* menus, popovers, dialogs */ + --surface-inset: #07080a; /* inputs, console, code */ + --surface-hover: rgba(255, 255, 255, 0.045); + --surface-active: rgba(255, 255, 255, 0.075); + --surface-selected:rgba(255, 255, 255, 0.06); + + --border-subtle: rgba(255, 255, 255, 0.06); + --border-default: rgba(255, 255, 255, 0.10); + --border-strong: rgba(255, 255, 255, 0.16); + + --text-primary: #f2f4f7; + --text-secondary: #aeb4bf; + --text-tertiary: #767d89; + --text-muted: #565d68; + --text-inverse: #0a0b0e; + --text-on-accent: var(--accent-contrast); + + /* Scrim for modals / image overlays */ + --scrim: rgba(4, 5, 7, 0.66); + + /* ---- Status / semantic (consistent across themes) ---- */ + --status-online: #36c780; + --status-online-soft: rgba(54, 199, 128, 0.14); + --status-online-border: rgba(54, 199, 128, 0.38); + + --status-offline: #e5484d; + --status-offline-soft: rgba(229, 72, 77, 0.14); + --status-offline-border: rgba(229, 72, 77, 0.40); + + --status-warn: #e8a33c; + --status-warn-soft: rgba(232, 163, 60, 0.15); + --status-warn-border: rgba(232, 163, 60, 0.40); + + --status-info: #4c8df0; + --status-info-soft: rgba(76, 141, 240, 0.15); + --status-info-border: rgba(76, 141, 240, 0.40); + + --status-starting: #2bc2d4; /* booting / updating / restarting */ + --status-starting-soft: rgba(43, 194, 212, 0.15); + --status-starting-border: rgba(43, 194, 212, 0.40); + + --status-wiping: #9b7bf0; /* Rust map wipe / maintenance */ + --status-wiping-soft: rgba(155, 123, 240, 0.16); + --status-wiping-border: rgba(155, 123, 240, 0.40); + + /* Aliases used by components */ + --success: var(--status-online); + --danger: var(--status-offline); + --warning: var(--status-warn); + --info: var(--status-info); + + /* Data-viz categorical (ECharts-friendly) */ + --viz-1: var(--accent); + --viz-2: #4c8df0; + --viz-3: #36c780; + --viz-4: #9b7bf0; + --viz-5: #2bc2d4; + --viz-6: #e8a33c; + --viz-grid: var(--border-subtle); +} + +/* ---- Semantic: LIGHT (full parity) ---- */ +[data-theme="light"] { + --surface-canvas: #f5f6f8; + --surface-sunken: #eceef1; + --surface-base: #ffffff; + --surface-raised: #ffffff; + --surface-raised-2:#fbfcfd; + --surface-overlay: #ffffff; + --surface-inset: #f1f3f5; + --surface-hover: rgba(12, 16, 22, 0.04); + --surface-active: rgba(12, 16, 22, 0.07); + --surface-selected:rgba(12, 16, 22, 0.05); + + --border-subtle: rgba(12, 16, 22, 0.07); + --border-default: rgba(12, 16, 22, 0.12); + --border-strong: rgba(12, 16, 22, 0.20); + + --text-primary: #14171c; + --text-secondary: #474e58; + --text-tertiary: #6b727e; + --text-muted: #969ca6; + --text-inverse: #ffffff; + + --scrim: rgba(16, 20, 28, 0.45); + + /* Status soft fills need a touch more alpha on light */ + --status-online-soft: rgba(33, 160, 98, 0.12); + --status-offline-soft: rgba(206, 44, 49, 0.10); + --status-warn-soft: rgba(193, 124, 18, 0.13); + --status-info-soft: rgba(43, 105, 214, 0.10); + --status-starting-soft: rgba(18, 150, 168, 0.12); + --status-wiping-soft: rgba(118, 86, 214, 0.12); +} diff --git a/frontend/src/styles/tokens/elevation.css b/frontend/src/styles/tokens/elevation.css new file mode 100644 index 0000000..1583765 --- /dev/null +++ b/frontend/src/styles/tokens/elevation.css @@ -0,0 +1,40 @@ +/* ============================================================ + Corrosion Control — Elevation, Borders, Glows + Shadows are quiet on dark surfaces; depth comes from layered + fills + hairline borders. Accent "glow" is reserved for + active/live states and game-themed focus. + ============================================================ */ + +:root { + /* Hairline rings (use as box-shadow inset or border) */ + --ring-subtle: inset 0 0 0 1px var(--border-subtle); + --ring-default: inset 0 0 0 1px var(--border-default); + --ring-strong: inset 0 0 0 1px var(--border-strong); + + /* Drop shadows — tuned for dark; subtle and tight */ + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.35); + --shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.40); + --shadow-md: 0 6px 18px -4px rgba(0, 0, 0, 0.50); + --shadow-lg: 0 18px 40px -10px rgba(0, 0, 0, 0.60); + --shadow-xl: 0 32px 70px -16px rgba(0, 0, 0, 0.70); + + /* Popover / menu — combines ring + drop for crisp edges on dark */ + --shadow-pop: 0 0 0 1px var(--border-default), 0 12px 36px -8px rgba(0, 0, 0, 0.66); + + /* Accent glow — game-themed; used on live/active controls & focus */ + --glow-accent: 0 0 0 1px var(--accent-border), 0 0 22px -2px var(--accent-glow); + --glow-accent-sm: 0 0 14px -2px var(--accent-glow); + --glow-online: 0 0 12px -1px rgba(54, 199, 128, 0.55); + + /* Focus ring */ + --focus-ring: 0 0 0 2px var(--surface-canvas), 0 0 0 4px var(--accent); +} + +[data-theme="light"] { + --shadow-xs: 0 1px 2px rgba(16, 20, 28, 0.06); + --shadow-sm: 0 2px 6px rgba(16, 20, 28, 0.08); + --shadow-md: 0 8px 20px -6px rgba(16, 20, 28, 0.12); + --shadow-lg: 0 20px 44px -12px rgba(16, 20, 28, 0.16); + --shadow-xl: 0 32px 70px -18px rgba(16, 20, 28, 0.20); + --shadow-pop: 0 0 0 1px var(--border-default), 0 14px 38px -10px rgba(16, 20, 28, 0.20); +} diff --git a/frontend/src/styles/tokens/fonts.css b/frontend/src/styles/tokens/fonts.css new file mode 100644 index 0000000..5fb6020 --- /dev/null +++ b/frontend/src/styles/tokens/fonts.css @@ -0,0 +1,21 @@ +/* ============================================================ + Corrosion Control — Fonts + Geist — UI / body / app headings + JetBrains Mono — console, data, IDs, telemetry + Oxanium — brand wordmark + marketing display (game-ops flavor) + ------------------------------------------------------------ + NOTE: Loaded from Google Fonts CDN. If you want these self- + hosted (offline), send the woff2 files and these @imports + become @font-face rules. + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Oxanium:wght@500;600;700;800&display=swap'); + +:root { + --font-sans: 'Geist', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code', Menlo, monospace; + /* Brand wordmark + big marketing display — squared, technical, gamey */ + --font-brand: 'Oxanium', 'Geist', system-ui, sans-serif; + /* App-level display headings stay on the neutral sans */ + --font-display: var(--font-sans); +} diff --git a/frontend/src/styles/tokens/game-themes.css b/frontend/src/styles/tokens/game-themes.css new file mode 100644 index 0000000..dc3c463 --- /dev/null +++ b/frontend/src/styles/tokens/game-themes.css @@ -0,0 +1,150 @@ +/* ============================================================ + Corrosion Control — Game Themes (the re-skin layer) + ------------------------------------------------------------ + The shell is neutral. Each game declares an ACCENT ramp + an + ATMOSPHERE (backdrop hues for hero headers, login, glows). + Set on (or dune / ark / valheim / ...). + Default (no data-game) = Corrosion brand = Oxide Orange. + + Accent contract every game must define: + --accent base fill + --accent-hover hover fill + --accent-press pressed / darker + --accent-contrast text/icon ON the accent fill + --accent-text accent used AS text on dark surfaces (lightened for AA) + --accent-soft low-alpha tint background + --accent-soft-strong + --accent-border accent hairline + --accent-glow glow color (box-shadow) + --game-label printable name + --atmo-1 / --atmo-2 backdrop gradient stops + --atmo-haze radial haze color + ============================================================ */ + +/* ---------- Default brand / RUST — Oxide Orange #F26622 ---------- */ +:root, +[data-game="rust"] { + --accent: #f26622; + --accent-hover: #ff7a38; + --accent-press: #d9550f; + --accent-contrast: #190d05; + --accent-text: #ff8a4c; + --accent-soft: rgba(242, 102, 34, 0.13); + --accent-soft-strong: rgba(242, 102, 34, 0.22); + --accent-border: rgba(242, 102, 34, 0.42); + --accent-glow: rgba(242, 102, 34, 0.50); + --game-label: "Rust"; /* @kind other */ + --atmo-1: #2c1206; + --atmo-2: #0a0b0e; + --atmo-haze: rgba(242, 102, 34, 0.30); +} + +/* ---------- DUNE: AWAKENING — Spice Amber / desert gold ---------- */ +[data-game="dune"] { + --accent: #e9a53a; + --accent-hover: #f7b94f; + --accent-press: #c9851f; + --accent-contrast: #1c1303; + --accent-text: #f0bc5e; + --accent-soft: rgba(233, 165, 58, 0.14); + --accent-soft-strong: rgba(233, 165, 58, 0.24); + --accent-border: rgba(233, 165, 58, 0.44); + --accent-glow: rgba(233, 165, 58, 0.46); + --game-label: "Dune: Awakening"; /* @kind other */ + --atmo-1: #2c1e08; + --atmo-2: #0a0b0e; + --atmo-haze: rgba(233, 165, 58, 0.30); +} + +/* ---------- SOULMASK: Ritual Jade ---------- */ +[data-game="soulmask"] { + --accent: #43c47e; + --accent-hover: #59d792; + --accent-press: #2c9c5f; + --accent-contrast: #04140c; + --accent-text: #63d894; + --accent-soft: rgba(67, 196, 126, 0.14); + --accent-soft-strong: rgba(67, 196, 126, 0.24); + --accent-border: rgba(67, 196, 126, 0.42); + --accent-glow: rgba(67, 196, 126, 0.46); + --game-label: "Soulmask"; /* @kind other */ + --atmo-1: #08231a; + --atmo-2: #0a0b0e; + --atmo-haze: rgba(67, 196, 126, 0.26); +} + +/* ---------- CONAN EXILES: Hyborian Bronze ---------- */ +[data-game="conan"] { + --accent: #bb7637; + --accent-hover: #d28d4b; + --accent-press: #985c24; + --accent-contrast: #160d03; + --accent-text: #d59a5e; + --accent-soft: rgba(187, 118, 55, 0.15); + --accent-soft-strong: rgba(187, 118, 55, 0.26); + --accent-border: rgba(187, 118, 55, 0.44); + --accent-glow: rgba(187, 118, 55, 0.46); + --game-label: "Conan Exiles"; /* @kind other */ + --atmo-1: #2a1b0b; + --atmo-2: #0a0b0e; + --atmo-haze: rgba(187, 118, 55, 0.30); +} + +/* ---------- Room to add more (stubs, ready to ship) ---------- */ +[data-game="ark"] { + --accent: #36c2a8; + --accent-hover: #4ad7bd; + --accent-press: #1f9c86; + --accent-contrast: #04140f; + --accent-text: #54dcc2; + --accent-soft: rgba(54, 194, 168, 0.14); + --accent-soft-strong: rgba(54, 194, 168, 0.24); + --accent-border: rgba(54, 194, 168, 0.42); + --accent-glow: rgba(54, 194, 168, 0.46); + --game-label: "ARK"; /* @kind other */ + --atmo-1: #07241f; + --atmo-2: #0a0b0e; + --atmo-haze: rgba(54, 194, 168, 0.26); +} + +[data-game="valheim"] { + --accent: #5b9bf0; + --accent-hover: #74afff; + --accent-press: #3c7ad4; + --accent-contrast: #050d1a; + --accent-text: #7fb2ff; + --accent-soft: rgba(91, 155, 240, 0.14); + --accent-soft-strong: rgba(91, 155, 240, 0.24); + --accent-border: rgba(91, 155, 240, 0.42); + --accent-glow: rgba(91, 155, 240, 0.46); + --game-label: "Valheim"; /* @kind other */ + --atmo-1: #0a1c2e; + --atmo-2: #0a0b0e; + --atmo-haze: rgba(91, 155, 240, 0.26); +} + +[data-game="palworld"] { + --accent: #58b6e8; + --accent-hover: #71c8f6; + --accent-press: #3a92c4; + --accent-contrast: #04121b; + --accent-text: #7fcaf2; + --accent-soft: rgba(88, 182, 232, 0.14); + --accent-soft-strong: rgba(88, 182, 232, 0.24); + --accent-border: rgba(88, 182, 232, 0.42); + --accent-glow: rgba(88, 182, 232, 0.46); + --game-label: "Palworld"; /* @kind other */ + --atmo-1: #08222e; + --atmo-2: #0a0b0e; + --atmo-haze: rgba(88, 182, 232, 0.26); +} + +/* ---------- Light-theme accent legibility ---------- + On light surfaces, accent-as-text reads better at the pressed + (darker) value. Placed last so it wins for --accent-text when + data-theme="light" and data-game=* are both on . */ +[data-theme="light"] { + --accent-text: var(--accent-press); + --accent-soft: color-mix(in srgb, var(--accent) 12%, transparent); + --accent-soft-strong: color-mix(in srgb, var(--accent) 20%, transparent); +} diff --git a/frontend/src/styles/tokens/motion.css b/frontend/src/styles/tokens/motion.css new file mode 100644 index 0000000..127ed7c --- /dev/null +++ b/frontend/src/styles/tokens/motion.css @@ -0,0 +1,27 @@ +/* ============================================================ + Corrosion Control — Motion + Fast, mechanical, precise. No bounce on chrome. Subtle spring + reserved for "live" telemetry (pulses, meters). Respect + prefers-reduced-motion in components. + ============================================================ */ + +:root { + --dur-instant: 80ms; /* @kind other */ + --dur-fast: 120ms; /* @kind other */ + --dur-base: 170ms; /* @kind other */ + --dur-slow: 240ms; /* @kind other */ + --dur-slower: 360ms; /* @kind other */ + + /* Easings */ + --ease-standard: cubic-bezier(0.2, 0, 0, 1); /* @kind other */ + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* @kind other */ + --ease-in: cubic-bezier(0.5, 0, 0.84, 0); /* @kind other */ + --ease-emphasized: cubic-bezier(0.34, 1.4, 0.5, 1); /* @kind other */ + + /* Common transition bundles */ + --transition-colors: color var(--dur-fast) var(--ease-standard), + background-color var(--dur-fast) var(--ease-standard), + border-color var(--dur-fast) var(--ease-standard), + box-shadow var(--dur-fast) var(--ease-standard); + --transition-transform: transform var(--dur-base) var(--ease-out); +} diff --git a/frontend/src/styles/tokens/spacing.css b/frontend/src/styles/tokens/spacing.css new file mode 100644 index 0000000..e3daa47 --- /dev/null +++ b/frontend/src/styles/tokens/spacing.css @@ -0,0 +1,50 @@ +/* ============================================================ + Corrosion Control — Spacing, Radius, Sizing + Dense ops-cockpit scale. 4px base grid, tightened. + ============================================================ */ + +:root { + /* Space scale (px) — used for padding, gap, margins */ + --space-0: 0; + --space-px: 1px; + --space-0-5: 2px; + --space-1: 4px; + --space-1-5: 6px; + --space-2: 8px; + --space-2-5: 10px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-7: 28px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-14: 56px; + --space-16: 64px; + --space-20: 80px; + --space-24: 96px; + --space-32: 128px; + + /* Radius — small & technical; cards stay crisp, not rounded-blobby */ + --radius-xs: 3px; + --radius-sm: 5px; + --radius-md: 7px; + --radius-lg: 10px; + --radius-xl: 14px; + --radius-2xl: 20px; + --radius-pill: 999px; + + /* Control heights — compact rows for data-dense UI */ + --control-h-xs: 24px; + --control-h-sm: 30px; + --control-h-md: 36px; + --control-h-lg: 44px; + + /* Layout primitives */ + --sidebar-w: 248px; + --sidebar-w-collapsed: 64px; + --topbar-h: 56px; + --content-max: 1440px; + --container-pad: var(--space-6); +} diff --git a/frontend/src/styles/tokens/typography.css b/frontend/src/styles/tokens/typography.css new file mode 100644 index 0000000..81c8b66 --- /dev/null +++ b/frontend/src/styles/tokens/typography.css @@ -0,0 +1,75 @@ +/* ============================================================ + Corrosion Control — Typography + Geist for UI & display; JetBrains Mono for telemetry, IDs, + console, and any numeric/code data. Dense reading sizes. + ============================================================ */ + +:root { + /* Font sizes (px) — base UI is 14 (dense) */ + --text-2xs: 11px; + --text-xs: 12px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 15px; + --text-lg: 17px; + --text-xl: 20px; + --text-2xl: 24px; + --text-3xl: 30px; + --text-4xl: 38px; + --text-5xl: 50px; + --text-6xl: 66px; + --text-7xl: 88px; + + /* Line heights */ + --leading-none: 1; + --leading-tight: 1.15; + --leading-snug: 1.3; + --leading-normal: 1.5; + --leading-relaxed: 1.65; + + /* Weights */ + --weight-light: 300; + --weight-regular: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + --weight-black: 800; + + /* Letter spacing */ + --tracking-tighter: -0.03em; + --tracking-tight: -0.015em; + --tracking-normal: 0; + --tracking-wide: 0.02em; + --tracking-wider: 0.06em; + --tracking-caps: 0.10em; /* eyebrows / overlines / labels */ + + /* ---- Semantic roles (font shorthands via custom props) ---- */ + --font-display: var(--font-sans); +} + +/* ---- Optional helper classes (cards/kits can use these) ---- */ +.t-display { + font-family: var(--font-display); + font-weight: var(--weight-bold); + letter-spacing: var(--tracking-tight); + line-height: var(--leading-tight); +} +.t-eyebrow { + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: var(--weight-medium); + letter-spacing: var(--tracking-caps); + text-transform: uppercase; + color: var(--text-tertiary); +} +.t-mono { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + letter-spacing: var(--tracking-normal); +} +.t-metric { + font-family: var(--font-mono); + font-weight: var(--weight-semibold); + font-variant-numeric: tabular-nums; + letter-spacing: var(--tracking-tight); +} diff --git a/frontend/src/views/admin/DashboardView.vue b/frontend/src/views/admin/DashboardView.vue index e6a95ef..a59023a 100644 --- a/frontend/src/views/admin/DashboardView.vue +++ b/frontend/src/views/admin/DashboardView.vue @@ -1,155 +1,497 @@ + + diff --git a/frontend/src/views/admin/_dashboardMock.ts b/frontend/src/views/admin/_dashboardMock.ts new file mode 100644 index 0000000..8c16b9b --- /dev/null +++ b/frontend/src/views/admin/_dashboardMock.ts @@ -0,0 +1,159 @@ +/** + * Dashboard mock data — representative placeholder pending multi-instance backend. + * Current backend is single-server-per-license; the fleet view is a forward-looking + * surface that will bind to a multi-instance API. All data here is static and clearly + * labeled so it is never confused for real tenant data. + * + * Per-game fields are isolated by game key — a Dune row NEVER receives a Rust field + * like `umod`, and vice-versa. See GAME_FIELDS for the row-field contract. + */ + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type ServerStatus = 'online' | 'offline' | 'starting' | 'wiping' | 'updating' +export type GameKey = 'rust' | 'dune' | 'conan' | 'soulmask' + +export interface MockServer { + game: GameKey + gameIcon: string + name: string + region: string + map: string + version: string + status: ServerStatus + players: { cur: number; max: number } + cpu?: number + ram?: number + ramSub?: string + ip: string + // Rust-only + umod?: string + wipe?: string + // Dune-only + sietches?: string + control?: string + // Conan-only + clans?: string + purge?: string + // Soulmask-only + tribe?: string + mask?: string +} + +export interface MockFeedLine { + time: string + level: 'cmd' | 'chat' | 'info' | 'warn' | 'error' | 'connect' | 'kill' + who?: string + msg: string +} + +export interface MockWipe { + game: GameKey + name: string + when: string + tone: 'wiping' | 'starting' | 'warn' | 'online' + label: string +} + +export interface StatItem { + label: string + value: string | number +} + +// --------------------------------------------------------------------------- +// Fleet server roster +// --------------------------------------------------------------------------- + +export const MOCK_SERVERS: MockServer[] = [ + { + game: 'rust', gameIcon: 'box', name: 'Main · 2x Vanilla', region: 'US-East', + map: 'Procedural 4500', version: 'v2024.12', status: 'online', + players: { cur: 142, max: 200 }, cpu: 41, ram: 68, ramSub: '5.4 / 8 GB', + ip: '89.142.0.7:28015', umod: '14', wipe: '2d', + }, + { + game: 'rust', gameIcon: 'box', name: '5x Modded · Build', region: 'US-East', + map: 'Barren 3000', version: 'v2024.12', status: 'online', + players: { cur: 38, max: 100 }, ip: '89.142.0.7:28017', umod: '27', wipe: '2d', + }, + { + game: 'rust', gameIcon: 'box', name: 'Hardcore · Solo/Duo', region: 'US-West', + map: 'Procedural 3500', version: 'v2024.12', status: 'wiping', + players: { cur: 0, max: 80 }, cpu: 8, ram: 30, ramSub: '2.4 / 8 GB', + ip: '74.91.3.2:28015', umod: '9', wipe: 'now', + }, + { + game: 'dune', gameIcon: 'sun', name: 'Arrakis · Hardcore', region: 'EU-Frankfurt', + map: 'Hagga Basin', version: 'v0.9.4', status: 'online', + players: { cur: 54, max: 60 }, cpu: 63, ram: 74, ramSub: '11.8 / 16 GB', + ip: '51.83.12.4:7777', sietches: '3', control: '62%', + }, + { + game: 'dune', gameIcon: 'sun', name: 'Deep Desert · PvP', region: 'EU-Frankfurt', + map: 'Deep Desert', version: 'v0.9.4', status: 'starting', + players: { cur: 0, max: 40 }, ip: '51.83.12.4:7779', sietches: '0', control: '—', + }, + { + game: 'dune', gameIcon: 'sun', name: 'Sietch · Roleplay', region: 'SG-Singapore', + map: 'Hagga Basin', version: 'v0.9.4', status: 'offline', + players: { cur: 0, max: 50 }, ip: '139.99.4.8:7777', sietches: '5', control: '—', + }, + { + game: 'conan', gameIcon: 'swords', name: 'Exiled Lands · PvP-C', region: 'US-East', + map: 'Exiled Lands', version: 'v3.0.5', status: 'online', + players: { cur: 32, max: 40 }, cpu: 48, ram: 60, ramSub: '9.6 / 16 GB', + ip: '89.142.0.7:7777', clans: '7', purge: 'Tier 4', + }, + { + game: 'soulmask', gameIcon: 'drama', name: 'Sienna Plateau · PvE', region: 'EU-Frankfurt', + map: 'Sienna Plateau', version: 'v1.4', status: 'online', + players: { cur: 18, max: 30 }, cpu: 35, ram: 52, ramSub: '8.3 / 16 GB', + ip: '51.83.12.4:8777', tribe: '4', mask: 'Jaguar', + }, +] + +// --------------------------------------------------------------------------- +// Per-game stat field sets — never share slots across games +// --------------------------------------------------------------------------- + +function pl(s: MockServer): string { + return `${s.players.cur} / ${s.players.max}` +} + +export const GAME_FIELDS: Record StatItem[]> = { + rust: (s) => [{ label: 'Players', value: pl(s) }, { label: 'uMod', value: s.umod ?? '—' }, { label: 'Wipe', value: s.wipe ?? '—' }], + dune: (s) => [{ label: 'Players', value: pl(s) }, { label: 'Sietches', value: s.sietches ?? '—' }, { label: 'Control', value: s.control ?? '—' }], + conan: (s) => [{ label: 'Players', value: pl(s) }, { label: 'Clans', value: s.clans ?? '—' }, { label: 'Purge', value: s.purge ?? '—' }], + soulmask: (s) => [{ label: 'Players', value: pl(s) }, { label: 'Tribe', value: s.tribe ?? '—' }, { label: 'Mask', value: s.mask ?? '—' }], +} + +export function buildStats(s: MockServer): StatItem[] { + const fn = GAME_FIELDS[s.game] ?? GAME_FIELDS.rust + return fn(s) +} + +// --------------------------------------------------------------------------- +// Live activity feed +// --------------------------------------------------------------------------- + +export const MOCK_FEED: MockFeedLine[] = [ + { time: '18:42:07', level: 'connect', who: 'ShadowFox', msg: 'connected — 89.142.0.7' }, + { time: '18:41:55', level: 'cmd', who: 'admin', msg: 'oxide.grant group default kits.use' }, + { time: '18:41:30', level: 'kill', who: 'ironMaiden', msg: 'was killed by Scorpion (AK-47, 84m)' }, + { time: '18:40:12', level: 'warn', msg: '5x Modded agent reconnected — telemetry resuming' }, + { time: '18:39:48', level: 'chat', who: 'BlightWalker:', msg: 'anyone selling sulfur?' }, + { time: '18:38:02', level: 'info', msg: 'RaidableBases spawned Tier-3 at G14' }, + { time: '18:36:51', level: 'connect', who: 'Vex', msg: 'connected — 51.83.12.4' }, +] + +// --------------------------------------------------------------------------- +// Upcoming wipes +// --------------------------------------------------------------------------- + +export const MOCK_WIPES: MockWipe[] = [ + { game: 'rust', name: 'Main · 2x Vanilla', when: 'Thu · 18:00 UTC', tone: 'wiping', label: 'Map + BP' }, + { game: 'rust', name: '5x Modded · Build', when: 'Thu · 18:00 UTC', tone: 'wiping', label: 'Map only' }, + { game: 'dune', name: 'Deep Desert · PvP', when: 'Sun · 12:00 UTC', tone: 'starting', label: 'Deep Desert' }, +] From 560d023250226dea6fd36771b120a658a3e4031a Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 11 Jun 2026 02:21:14 -0400 Subject: [PATCH 2/7] feat(redesign): re-skin auth + account views to DS (Phase D batch 1) Auth (Login/Register/ForgotPassword/SetupWizard) + account cluster (Settings/Team/Notifications) re-skinned onto design-system components + tokens. JPEG login banner replaced with the C-gauge mark + Oxanium wordmark. All logic/store/router/handlers preserved (TOTP flow, validators, save handlers, API endpoints). Build green. Co-Authored-By: Claude Opus 4.8 --- .../src/views/admin/NotificationsView.vue | 195 +++--- frontend/src/views/admin/SettingsView.vue | 354 +++++------ frontend/src/views/admin/TeamView.vue | 256 ++++---- .../src/views/auth/ForgotPasswordView.vue | 198 +++++-- frontend/src/views/auth/LoginView.vue | 367 +++++++----- frontend/src/views/auth/RegisterView.vue | 275 +++++---- frontend/src/views/auth/SetupWizardView.vue | 559 ++++++++++++++---- 7 files changed, 1357 insertions(+), 847 deletions(-) diff --git a/frontend/src/views/admin/NotificationsView.vue b/frontend/src/views/admin/NotificationsView.vue index f148b29..3776fb3 100644 --- a/frontend/src/views/admin/NotificationsView.vue +++ b/frontend/src/views/admin/NotificationsView.vue @@ -1,9 +1,13 @@ + + diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index b6d9135..f48cb2f 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -3,14 +3,20 @@ import { ref, onMounted } from 'vue' import { useAuthStore } from '@/stores/auth' import { useToastStore } from '@/stores/toast' import { useApi } from '@/composables/useApi' -import { Settings, Key, Globe, User, Save, Loader2, Eye } from 'lucide-vue-next' + +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.vue' +import Badge from '@/components/ds/core/Badge.vue' +import Tabs from '@/components/ds/navigation/Tabs.vue' +import Input from '@/components/ds/forms/Input.vue' +import Switch from '@/components/ds/forms/Switch.vue' const auth = useAuthStore() const toast = useToastStore() const api = useApi() const saving = ref(false) -const section = ref<'account' | 'license' | 'domain' | 'public'>('account') +const section = ref('account') const accountForm = ref({ username: '', @@ -86,209 +92,225 @@ async function savePublicSite() { onMounted(() => { loadForms() }) + +const tabItems = [ + { value: 'account', label: 'Account', icon: 'user' }, + { value: 'license', label: 'License', icon: 'key' }, + { value: 'domain', label: 'Domain', icon: 'globe' }, + { value: 'public', label: 'Public status', icon: 'eye' }, +] + + diff --git a/frontend/src/views/admin/TeamView.vue b/frontend/src/views/admin/TeamView.vue index 6a3efd2..bca12b2 100644 --- a/frontend/src/views/admin/TeamView.vue +++ b/frontend/src/views/admin/TeamView.vue @@ -1,9 +1,14 @@ + + diff --git a/frontend/src/views/auth/ForgotPasswordView.vue b/frontend/src/views/auth/ForgotPasswordView.vue index c616c73..d7aa3e2 100644 --- a/frontend/src/views/auth/ForgotPasswordView.vue +++ b/frontend/src/views/auth/ForgotPasswordView.vue @@ -2,7 +2,10 @@ import { ref } from 'vue' import { useApi } from '@/composables/useApi' import { RouterLink } from 'vue-router' -import { Mail, ArrowLeft, CheckCircle2, Loader2 } from 'lucide-vue-next' +import Logo from '@/components/ds/brand/Logo.vue' +import Input from '@/components/ds/forms/Input.vue' +import Button from '@/components/ds/core/Button.vue' +import Alert from '@/components/ds/feedback/Alert.vue' const api = useApi() const email = ref('') @@ -27,76 +30,149 @@ async function handleSubmit() { + + diff --git a/frontend/src/views/auth/LoginView.vue b/frontend/src/views/auth/LoginView.vue index 737dda9..5963c04 100644 --- a/frontend/src/views/auth/LoginView.vue +++ b/frontend/src/views/auth/LoginView.vue @@ -4,6 +4,10 @@ import { useRouter } from 'vue-router' import { useApi } from '@/composables/useApi' import { useAuthStore } from '@/stores/auth' import type { AuthResponse } from '@/types' +import Logo from '@/components/ds/brand/Logo.vue' +import Input from '@/components/ds/forms/Input.vue' +import Button from '@/components/ds/core/Button.vue' +import Alert from '@/components/ds/feedback/Alert.vue' const router = useRouter() const api = useApi() @@ -96,158 +100,221 @@ function handleBackToLogin() { + + diff --git a/frontend/src/views/auth/RegisterView.vue b/frontend/src/views/auth/RegisterView.vue index 3079661..89690bf 100644 --- a/frontend/src/views/auth/RegisterView.vue +++ b/frontend/src/views/auth/RegisterView.vue @@ -4,6 +4,10 @@ import { useRouter } from 'vue-router' import { useApi } from '@/composables/useApi' import { useAuthStore } from '@/stores/auth' import type { AuthResponse } from '@/types' +import Logo from '@/components/ds/brand/Logo.vue' +import Input from '@/components/ds/forms/Input.vue' +import Button from '@/components/ds/core/Button.vue' +import Alert from '@/components/ds/feedback/Alert.vue' const router = useRouter() const api = useApi() @@ -74,138 +78,149 @@ async function handleRegister() { + + diff --git a/frontend/src/views/auth/SetupWizardView.vue b/frontend/src/views/auth/SetupWizardView.vue index 57fc042..d2f3262 100644 --- a/frontend/src/views/auth/SetupWizardView.vue +++ b/frontend/src/views/auth/SetupWizardView.vue @@ -3,7 +3,10 @@ import { ref } from 'vue' import { useRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' import { useApi } from '@/composables/useApi' -import { Server, Wifi, CheckCircle, ArrowRight, Loader2 } from 'lucide-vue-next' +import Logo from '@/components/ds/brand/Logo.vue' +import Input from '@/components/ds/forms/Input.vue' +import Button from '@/components/ds/core/Button.vue' +import Alert from '@/components/ds/feedback/Alert.vue' const router = useRouter() const auth = useAuthStore() @@ -21,13 +24,25 @@ const serverForm = ref({ game_port: 28015, }) +// String mirrors for the DS Input component (which binds defineModel). +// Changes sync back to serverForm as numbers before submission. +const serverPortStr = ref(String(serverForm.value.server_port)) +const gamePortStr = ref(String(serverForm.value.game_port)) + +function syncPorts() { + serverForm.value.server_port = parseInt(serverPortStr.value, 10) || serverForm.value.server_port + serverForm.value.game_port = parseInt(gamePortStr.value, 10) || serverForm.value.game_port +} + const connectionTypes = [ - { value: 'bare_metal', label: 'Bare Metal / VPS', desc: 'Direct connection via Companion Agent' }, + { value: 'bare_metal', label: 'Bare metal / VPS', desc: 'Direct connection via Companion Agent' }, { value: 'amp', label: 'AMP (CubeCoders)', desc: 'Connect through AMP panel API' }, { value: 'pterodactyl', label: 'Pterodactyl', desc: 'Connect through Pterodactyl panel API' }, ] async function submitServerConfig() { + syncPorts() + if (!serverForm.value.server_name.trim()) { error.value = 'Server name is required.' return @@ -58,180 +73,464 @@ async function completeSetup() { + + From b42a2d7ea7b2949786cebd834aa8f46447fa8255 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 11 Jun 2026 02:34:46 -0400 Subject: [PATCH 3/7] feat(redesign): re-skin server-ops/operations/store/analytics views to DS (Phase D batch 2) 18 admin views re-skinned onto design-system components + tokens: Server/Players/Plugins/ChatLog, Wipes/WipeProfiles/Maps/Schedules/Alerts, StoreConfig/StoreItems/ModuleStore, Analytics/WipeAnalytics/MapAnalytics/PlayerRetention/StoreRevenue. ECharts now read var(--accent) (token-driven, follows game skin). 14 icons added to the registry. All logic/store/router/handlers/API calls preserved; presentation-only re-skin. Build green. Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/ds/core/Icon.vue | 8 + frontend/src/views/admin/AlertsView.vue | 414 ++++--- frontend/src/views/admin/AnalyticsView.vue | 338 ++++-- frontend/src/views/admin/ChatLogView.vue | 243 ++-- frontend/src/views/admin/MapAnalyticsView.vue | 398 ++++--- frontend/src/views/admin/MapsView.vue | 239 +++- frontend/src/views/admin/ModuleStoreView.vue | 535 +++++---- .../src/views/admin/PlayerRetentionView.vue | 362 ++++-- frontend/src/views/admin/PlayersView.vue | 292 +++-- frontend/src/views/admin/PluginsView.vue | 604 ++++++---- frontend/src/views/admin/SchedulesView.vue | 393 ++++-- frontend/src/views/admin/ServerView.vue | 1052 +++++++++-------- frontend/src/views/admin/StoreConfigView.vue | 340 +++--- frontend/src/views/admin/StoreItemsView.vue | 710 ++++++----- frontend/src/views/admin/StoreRevenueView.vue | 440 ++++--- .../src/views/admin/WipeAnalyticsView.vue | 342 ++++-- frontend/src/views/admin/WipeProfilesView.vue | 651 ++++++---- frontend/src/views/admin/WipesView.vue | 573 ++++++--- 18 files changed, 4826 insertions(+), 3108 deletions(-) diff --git a/frontend/src/components/ds/core/Icon.vue b/frontend/src/components/ds/core/Icon.vue index 94419d6..1349f66 100644 --- a/frontend/src/components/ds/core/Icon.vue +++ b/frontend/src/components/ds/core/Icon.vue @@ -20,6 +20,9 @@ import { Info, OctagonAlert, CircleCheck, Sparkles, Inbox, LayoutDashboard, CalendarClock, Drama, ChevronsUpDown, ServerCog, LayoutGrid, SquareDashed, MemoryStick, CornerDownLeft, + Ban, Flag, + CircleAlert, ArrowDown, Award, DollarSign, FlaskConical, Mail, Package, + Pencil, Save, ShoppingBag, Target, User, } from 'lucide-vue-next' const props = withDefaults( @@ -50,6 +53,11 @@ const registry: Record = { 'layout-dashboard': LayoutDashboard, 'calendar-clock': CalendarClock, drama: Drama, 'chevrons-up-down': ChevronsUpDown, 'server-cog': ServerCog, 'layout-grid': LayoutGrid, 'square-dashed': SquareDashed, 'memory-stick': MemoryStick, 'corner-down-left': CornerDownLeft, + ban: Ban, flag: Flag, + 'alert-circle': CircleAlert, 'arrow-down': ArrowDown, award: Award, + 'dollar-sign': DollarSign, 'flask-conical': FlaskConical, mail: Mail, + package: Package, pencil: Pencil, save: Save, 'shopping-bag': ShoppingBag, + target: Target, user: User, } const cmp = computed(() => registry[props.name] ?? null) diff --git a/frontend/src/views/admin/AlertsView.vue b/frontend/src/views/admin/AlertsView.vue index ee84e2a..54b1669 100644 --- a/frontend/src/views/admin/AlertsView.vue +++ b/frontend/src/views/admin/AlertsView.vue @@ -1,8 +1,14 @@ + + diff --git a/frontend/src/views/admin/AnalyticsView.vue b/frontend/src/views/admin/AnalyticsView.vue index 5033608..2285874 100644 --- a/frontend/src/views/admin/AnalyticsView.vue +++ b/frontend/src/views/admin/AnalyticsView.vue @@ -1,6 +1,5 @@ + + diff --git a/frontend/src/views/admin/ChatLogView.vue b/frontend/src/views/admin/ChatLogView.vue index d2914fd..c358254 100644 --- a/frontend/src/views/admin/ChatLogView.vue +++ b/frontend/src/views/admin/ChatLogView.vue @@ -3,7 +3,14 @@ import { ref, computed, onMounted } from 'vue' import { useApi } from '@/composables/useApi' import { useToastStore } from '@/stores/toast' import type { ChatMessage } from '@/types' -import { MessageSquare, Search, Flag, RefreshCw } from 'lucide-vue-next' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.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 Input from '@/components/ds/forms/Input.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' +import Tabs from '@/components/ds/navigation/Tabs.vue' const api = useApi() const toast = useToastStore() @@ -32,12 +39,18 @@ const filteredMessages = computed(() => { return result }) -function channelBadgeClass(channel: string): string { +const channelTabItems = computed(() => [ + { value: 'all', label: 'All', count: messages.value.length }, + { value: 'global', label: 'Global' }, + { value: 'team', label: 'Team' }, + { value: 'server', label: 'Server' }, +]) + +function channelTone(channel: string): 'accent' | 'info' | 'neutral' { switch (channel) { - case 'global': return 'bg-oxide-500/15 text-oxide-400' - case 'team': return 'bg-blue-500/15 text-blue-400' - case 'server': return 'bg-neutral-700/50 text-neutral-400' - default: return 'bg-neutral-700/50 text-neutral-400' + case 'global': return 'accent' + case 'team': return 'info' + default: return 'neutral' } } @@ -76,89 +89,163 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/MapAnalyticsView.vue b/frontend/src/views/admin/MapAnalyticsView.vue index 6483633..3bba563 100644 --- a/frontend/src/views/admin/MapAnalyticsView.vue +++ b/frontend/src/views/admin/MapAnalyticsView.vue @@ -1,11 +1,17 @@ + + diff --git a/frontend/src/views/admin/MapsView.vue b/frontend/src/views/admin/MapsView.vue index 1335920..8161c97 100644 --- a/frontend/src/views/admin/MapsView.vue +++ b/frontend/src/views/admin/MapsView.vue @@ -4,8 +4,12 @@ import { useApi } from '@/composables/useApi' import { useAuthStore } from '@/stores/auth' import { useToastStore } from '@/stores/toast' import type { MapEntry } from '@/types' -import { Map, Upload, Trash2, RefreshCw, Loader2 } from 'lucide-vue-next' import { safeFileSize } from '@/utils/formatters' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.vue' +import Badge from '@/components/ds/core/Badge.vue' +import IconButton from '@/components/ds/core/IconButton.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' const api = useApi() const auth = useAuthStore() @@ -20,8 +24,8 @@ function formatSize(bytes: number): string { return safeFileSize(bytes) } -function typeBadgeClass(type: string): string { - return type === 'custom' ? 'bg-oxide-500/15 text-oxide-400' : 'bg-blue-500/15 text-blue-400' +function mapTypeTone(type: string): 'accent' | 'info' { + return type === 'custom' ? 'accent' : 'info' } async function fetchMaps() { @@ -92,84 +96,211 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/ModuleStoreView.vue b/frontend/src/views/admin/ModuleStoreView.vue index befaeec..28755af 100644 --- a/frontend/src/views/admin/ModuleStoreView.vue +++ b/frontend/src/views/admin/ModuleStoreView.vue @@ -3,8 +3,17 @@ import { ref, computed, onMounted } from 'vue' import { useApi } from '@/composables/useApi' import { useAuthStore } from '@/stores/auth' import type { Module } from '@/types' -import { ShoppingCart, Package, Search, Filter, X, Check, Download, AlertCircle } from 'lucide-vue-next' import { safeFixed } from '@/utils/formatters' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.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 Tabs from '@/components/ds/navigation/Tabs.vue' +import Alert from '@/components/ds/feedback/Alert.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' +import Input from '@/components/ds/forms/Input.vue' +import Select from '@/components/ds/forms/Select.vue' const api = useApi() const auth = useAuthStore() @@ -22,17 +31,22 @@ const isPurchasing = ref(false) const purchaseError = ref('') const categories = [ - { value: 'all', label: 'All Modules' }, + { value: 'all', label: 'All modules' }, { value: 'loot', label: 'Loot' }, { value: 'events', label: 'Events' }, { value: 'economy', label: 'Economy' }, { value: 'kits', label: 'Kits' }, - { value: 'admin', label: 'Admin Tools' }, + { value: 'admin', label: 'Admin tools' }, { value: 'pvp', label: 'PVP' }, { value: 'pve', label: 'PVE' }, { value: 'building', label: 'Building' }, ] +const tabItems = computed(() => [ + { value: 'catalog', label: 'Catalog', icon: 'package' }, + { value: 'my-modules', label: `My modules (${myModules.value.length})`, icon: 'download' }, +]) + const filteredModules = computed(() => { let result = activeTab.value === 'catalog' ? modules.value : myModules.value @@ -51,6 +65,21 @@ const filteredModules = computed(() => { return result }) +type BadgeTone = 'info' | 'accent' | 'warn' | 'online' | 'neutral' | 'offline' +function categoryTone(category: string): BadgeTone { + const map: Record = { + loot: 'warn', + events: 'accent', + economy: 'online', + kits: 'info', + admin: 'accent', + pvp: 'offline', + pve: 'info', + building: 'warn', + } + return map[category] ?? 'neutral' +} + async function loadCatalog() { isLoading.value = true try { @@ -94,10 +123,8 @@ async function confirmPurchase() { }) if (response.payment_url) { - // Redirect to external payment provider window.location.href = response.payment_url } else if (response.success) { - // Instant purchase confirmed showPurchaseModal.value = false selectedModule.value = null await loadCatalog() @@ -131,20 +158,6 @@ function closeModals() { purchaseError.value = '' } -function categoryBadgeClass(category: string): string { - const colors: Record = { - loot: 'bg-yellow-500/15 text-yellow-400', - events: 'bg-purple-500/15 text-purple-400', - economy: 'bg-green-500/15 text-green-400', - kits: 'bg-blue-500/15 text-blue-400', - admin: 'bg-oxide-500/15 text-oxide-400', - pvp: 'bg-red-500/15 text-red-400', - pve: 'bg-indigo-500/15 text-indigo-400', - building: 'bg-orange-500/15 text-orange-400', - } - return colors[category] || 'bg-neutral-700/50 text-neutral-400' -} - onMounted(() => { loadCatalog() loadMyModules() @@ -152,323 +165,371 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/PlayerRetentionView.vue b/frontend/src/views/admin/PlayerRetentionView.vue index 0536a96..75f4310 100644 --- a/frontend/src/views/admin/PlayerRetentionView.vue +++ b/frontend/src/views/admin/PlayerRetentionView.vue @@ -1,11 +1,15 @@ + + diff --git a/frontend/src/views/admin/PlayersView.vue b/frontend/src/views/admin/PlayersView.vue index fb75a01..d3fc7f2 100644 --- a/frontend/src/views/admin/PlayersView.vue +++ b/frontend/src/views/admin/PlayersView.vue @@ -3,7 +3,14 @@ import { ref, computed, onMounted } from 'vue' import { useServerStore } from '@/stores/server' import { useApi } from '@/composables/useApi' import { useToastStore } from '@/stores/toast' -import { Users, Search, Ban, LogOut, Shield, RefreshCw } from 'lucide-vue-next' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.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 Input from '@/components/ds/forms/Input.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' +import Tabs from '@/components/ds/navigation/Tabs.vue' const server = useServerStore() const api = useApi() @@ -50,6 +57,12 @@ const filteredPlayers = computed(() => { const onlineCount = computed(() => players.value.filter(p => p.is_online).length) +const statusTabItems = computed(() => [ + { value: 'all', label: 'All', count: players.value.length }, + { value: 'online', label: 'Online', count: players.value.filter(p => p.is_online).length }, + { value: 'offline', label: 'Offline', count: players.value.filter(p => !p.is_online).length }, +]) + function formatPlaytime(seconds: number): string { const h = Math.floor(seconds / 3600) const m = Math.floor((seconds % 3600) / 60) @@ -58,7 +71,7 @@ function formatPlaytime(seconds: number): string { } function formatConnectedTime(iso: string | null): string { - if (!iso) return '\u2014' + if (!iso) return '—' const diff = Date.now() - new Date(iso).getTime() const m = Math.floor(diff / 60000) const h = Math.floor(m / 60) @@ -66,6 +79,18 @@ function formatConnectedTime(iso: string | null): string { return `${m}m` } +function playerStatusTone(player: Player): 'online' | 'offline' | 'warn' { + if (player.is_banned) return 'warn' + if (player.is_online) return 'online' + return 'offline' +} + +function playerStatusLabel(player: Player): string { + if (player.is_banned) return 'Banned' + if (player.is_online) return 'Online' + return 'Offline' +} + async function fetchPlayers() { isLoading.value = true try { @@ -106,132 +131,197 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/PluginsView.vue b/frontend/src/views/admin/PluginsView.vue index d0deb43..573d7f3 100644 --- a/frontend/src/views/admin/PluginsView.vue +++ b/frontend/src/views/admin/PluginsView.vue @@ -4,7 +4,15 @@ import { usePluginStore } from '@/stores/plugins' import type { UmodPlugin } from '@/stores/plugins' import { useToastStore } from '@/stores/toast' import type { PluginEntry } from '@/types' -import { Puzzle, Search, Download, RefreshCw, Power, PowerOff, Trash2, Loader2, Upload, X } from 'lucide-vue-next' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.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 Input from '@/components/ds/forms/Input.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' +import Alert from '@/components/ds/feedback/Alert.vue' +import Tabs from '@/components/ds/navigation/Tabs.vue' const pluginStore = usePluginStore() const toast = useToastStore() @@ -35,6 +43,12 @@ const browsePlugins = computed(() => pluginStore.browseResults?.data ?? []) const loadedCount = computed(() => pluginStore.plugins.filter((p: PluginEntry) => p.is_loaded).length) +const tabItems = [ + { value: 'installed', label: 'Installed' }, + { value: 'browse', label: 'Browse uMod' }, + { value: 'upload', label: 'Upload custom' }, +] + function sourceLabel(source: string): string { switch (source) { case 'umod': return 'uMod' @@ -44,11 +58,11 @@ function sourceLabel(source: string): string { } } -function sourceBadgeClass(source: string): string { +function sourceTone(source: string): 'online' | 'accent' | 'neutral' { switch (source) { - case 'umod': return 'bg-green-500/10 text-green-400' - case 'corrosion_module': return 'bg-oxide-500/15 text-oxide-400' - default: return 'bg-neutral-700/50 text-neutral-400' + case 'umod': return 'online' + case 'corrosion_module': return 'accent' + default: return 'neutral' } } @@ -168,274 +182,249 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/SchedulesView.vue b/frontend/src/views/admin/SchedulesView.vue index b813d46..7a4430c 100644 --- a/frontend/src/views/admin/SchedulesView.vue +++ b/frontend/src/views/admin/SchedulesView.vue @@ -1,8 +1,14 @@