diff --git a/CLAUDE.md b/CLAUDE.md index 936762b..aff1636 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -431,3 +431,7 @@ Things I discovered about myself building a sister platform across multiple sess 20. **Parallel state fields that track related things will drift apart — and the bugs are silent.** When two fields represent aspects of the same state (`captureMode` and `vkiMode`, or `isLoading` and `error`, or `connection_status` and `companion_last_seen`), every code path that mutates one must also update the other. But new code paths get added over time, and they only update the field they know about. Future me: when you see two fields tracking related state, grep for ALL mutation sites of each — if any path updates one but not the other, that's a bug waiting to happen. And when you add a new mutation path, check every sibling field, not just the obvious one. 21. **Route through the component that survives transitions, not the one that doesn't.** When two systems can handle the same job but one is resilient to failure modes and the other isn't, route through the survivor. Don't build infrastructure to prop up the fragile path when the robust path already exists. In this project: NATS request-reply through the companion agent is the robust path; direct WebSocket to the browser is the fragile one. If a feature can work through either, prefer the path that handles disconnects, reconnects, and restarts gracefully. One routing change beats an entire retry/recovery subsystem. + +22. **Build-green is not render-correct — visually verify UI work before calling it done.** The entire design-system re-skin (50+ files, six green commits) rendered almost completely unstyled in the browser — white background, no surfaces, no accent — because the design tokens never loaded. `vue-tsc -b` + `vite build` passed clean the whole time; CSS that *compiles* can still apply *zero* styles. One Playwright screenshot of the login exposed it in seconds. When the deliverable is visual, a green build is necessary but not sufficient: load it in a real browser (Playwright on the dev server at :5174), screenshot it, and assert on `getComputedStyle` — don't trust compilation alone. This is Lesson 17 with teeth. + +23. **Tailwind v4 silently drops a nested `@import` barrel placed after `@import "tailwindcss"`.** `style.css` did `@import "tailwindcss"; @import "./styles/corrosion.css";` where corrosion.css was a barrel of eight `@import` token files. Once Tailwind v4 expands the tailwindcss import in place, the barrel's inner @imports no longer precede all statements, so PostCSS drops them — emitting only an easily-ignored "@import must precede all other statements" warning. Result: every design token resolved empty and the whole panel rendered unstyled. Import token/design CSS files **directly and contiguously** in the entry stylesheet; never via a nested barrel after the Tailwind import. The build warning you wave off as "pre-existing" may be the entire feature silently failing. 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/ErrorBoundary.vue b/frontend/src/components/ErrorBoundary.vue index e92d806..3f0920d 100644 --- a/frontend/src/components/ErrorBoundary.vue +++ b/frontend/src/components/ErrorBoundary.vue @@ -1,6 +1,7 @@ + + 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..1349f66 --- /dev/null +++ b/frontend/src/components/ds/core/Icon.vue @@ -0,0 +1,81 @@ + + + + + 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/components/layout/PublicLayout.vue b/frontend/src/components/layout/PublicLayout.vue index f7c0698..46033c3 100644 --- a/frontend/src/components/layout/PublicLayout.vue +++ b/frontend/src/components/layout/PublicLayout.vue @@ -1,16 +1,33 @@ + + diff --git a/frontend/src/components/loot/LootContainerSidebar.vue b/frontend/src/components/loot/LootContainerSidebar.vue index 3a81c61..90b303d 100644 --- a/frontend/src/components/loot/LootContainerSidebar.vue +++ b/frontend/src/components/loot/LootContainerSidebar.vue @@ -1,7 +1,8 @@ + + diff --git a/frontend/src/components/loot/LootGroupEditor.vue b/frontend/src/components/loot/LootGroupEditor.vue index 09a6551..6dfa862 100644 --- a/frontend/src/components/loot/LootGroupEditor.vue +++ b/frontend/src/components/loot/LootGroupEditor.vue @@ -1,7 +1,13 @@ + + diff --git a/frontend/src/components/loot/LootItemEditor.vue b/frontend/src/components/loot/LootItemEditor.vue index 2776b21..d88436e 100644 --- a/frontend/src/components/loot/LootItemEditor.vue +++ b/frontend/src/components/loot/LootItemEditor.vue @@ -2,7 +2,12 @@ import { computed } from 'vue' import { rustItems } from '@/data/rust-items' import { rustContainers } from '@/data/rust-containers' -import { Trash2, Plus, Settings2 } from 'lucide-vue-next' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.vue' +import IconButton from '@/components/ds/core/IconButton.vue' +import Badge from '@/components/ds/core/Badge.vue' +import Switch from '@/components/ds/forms/Switch.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' import type { PrefabLoot } from '@/types' const props = defineProps<{ @@ -76,157 +81,273 @@ const ungroupedItems = computed(() => { ...(data as any), })) }) + +// Computed boolean for the Switch v-model +const isEnabled = computed({ + get: () => containerData.value?.Enabled ?? true, + set: () => toggleEnabled(), +}) + + diff --git a/frontend/src/components/loot/LootItemPicker.vue b/frontend/src/components/loot/LootItemPicker.vue index 9dac373..717a3f9 100644 --- a/frontend/src/components/loot/LootItemPicker.vue +++ b/frontend/src/components/loot/LootItemPicker.vue @@ -1,7 +1,9 @@ + + diff --git a/frontend/src/components/teleport/PermissionGroupEditor.vue b/frontend/src/components/teleport/PermissionGroupEditor.vue index 3554c39..0df8621 100644 --- a/frontend/src/components/teleport/PermissionGroupEditor.vue +++ b/frontend/src/components/teleport/PermissionGroupEditor.vue @@ -1,6 +1,8 @@ + + diff --git a/frontend/src/components/teleport/WarpEditor.vue b/frontend/src/components/teleport/WarpEditor.vue index 1da6e27..0ce523d 100644 --- a/frontend/src/components/teleport/WarpEditor.vue +++ b/frontend/src/components/teleport/WarpEditor.vue @@ -1,6 +1,8 @@ + + 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..05bf4c5 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,6 +1,24 @@ @import "tailwindcss"; -/* Corrosion Brand — Oxide Orange #F26622 */ +/* Corrosion design tokens — load order matters: fonts → primitives → colors → + game themes → elevation → base (base references color/accent tokens). + Imported DIRECTLY here (not via a nested ./styles/corrosion.css barrel): a + nested @import barrel placed after `@import "tailwindcss"` gets its inner + @imports dropped, because once Tailwind v4 expands in place they no longer + precede all other statements. Keeping them flat + contiguous fixes that. */ +@import "./styles/tokens/fonts.css"; +@import "./styles/tokens/spacing.css"; +@import "./styles/tokens/typography.css"; +@import "./styles/tokens/motion.css"; +@import "./styles/tokens/colors.css"; +@import "./styles/tokens/game-themes.css"; +@import "./styles/tokens/elevation.css"; +@import "./styles/tokens/base.css"; + +/* 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 +33,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 +43,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/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/AutoDoorsView.vue b/frontend/src/views/admin/AutoDoorsView.vue index 678a3cb..bc742b6 100644 --- a/frontend/src/views/admin/AutoDoorsView.vue +++ b/frontend/src/views/admin/AutoDoorsView.vue @@ -1,15 +1,11 @@ + + diff --git a/frontend/src/views/admin/StoreConfigView.vue b/frontend/src/views/admin/StoreConfigView.vue index 008448b..279744d 100644 --- a/frontend/src/views/admin/StoreConfigView.vue +++ b/frontend/src/views/admin/StoreConfigView.vue @@ -3,7 +3,13 @@ import { ref, onMounted, computed } from 'vue' import { useApi } from '@/composables/useApi' import { useToastStore } from '@/stores/toast' import type { StoreConfig } from '@/types' -import { Store, Save, Loader2, AlertTriangle, DollarSign } from 'lucide-vue-next' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.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' +import Switch from '@/components/ds/forms/Switch.vue' const api = useApi() const toast = useToastStore() @@ -22,10 +28,18 @@ const isLoading = ref(false) const saving = ref(false) const isConfigured = ref(false) +// DS Input uses defineModel() which emits string | undefined, but +// paypal_client_id is string | null in the DB type. Bridge null ↔ undefined +// so we never change what gets saved to the API. +const paypalClientId = computed({ + get: () => config.value.paypal_client_id ?? undefined, + set: (v: string | undefined) => { config.value.paypal_client_id = v ?? null }, +}) + const currencyOptions = [ - { value: 'USD', label: 'USD - US Dollar' }, - { value: 'EUR', label: 'EUR - Euro' }, - { value: 'GBP', label: 'GBP - British Pound' }, + { value: 'USD', label: 'USD — US Dollar' }, + { value: 'EUR', label: 'EUR — Euro' }, + { value: 'GBP', label: 'GBP — British Pound' }, ] const showPayPalWarning = computed(() => { @@ -40,7 +54,6 @@ async function fetchConfig() { isConfigured.value = !!data.config.store_name } catch (err: any) { if (err.response?.status === 404) { - // No config yet, use defaults isConfigured.value = false } else { toast.error('Failed to load store configuration') @@ -51,7 +64,6 @@ async function fetchConfig() { } async function saveConfig() { - // Validation if (!config.value.store_name.trim()) { toast.error('Store name is required') return @@ -73,7 +85,6 @@ async function saveConfig() { enabled: config.value.enabled, } - // Only include secret if it was entered if (paypalSecret.value.trim()) { payload.paypal_client_secret = paypalSecret.value } @@ -81,7 +92,7 @@ async function saveConfig() { await api.put('/webstore/config', payload) toast.success('Store configuration saved successfully') isConfigured.value = true - paypalSecret.value = '' // Clear after save + paypalSecret.value = '' } catch (err: any) { const message = err.response?.data?.error || 'Failed to save configuration' toast.error(message) @@ -96,202 +107,181 @@ onMounted(() => {