# platform_web/theme This directory owns the platform_web visual system. Keep the console in a unified game-operations style with two first-party theme families: the default black mecha console and the optional magical-girl console. ## Style Contract - The UI is a game operations console, not a landing page and not a generic SaaS dashboard. - The default visual language is black mecha: dark cockpit panels, cyan scanner light, angular frame cuts, tactical grid lines, and amber energy accents. - The optional magical-girl visual language uses pink moonlight, jelly glass, gold star borders, ribbon glow, and large visible magic-circle motifs. - The primary menu is a compact admin sidebar with two states: an expanded text menu and a collapsed icon rail. It must keep the same route order, readable Chinese labels, icon-only tooltips in collapsed mode, and active state framing. In the magical-girl theme it should feel like a pink moonlit operations rail; in the default theme it should feel like a black mecha console rail with restrained scanner accents. - Global floating decoration must come from `components/MagicalParticleLayer.tsx`, using a full-workspace background image layer plus a lightweight global particle DOM layer that reads the active theme variables. Do not add fixed page-local decorative spans or page-local backdrop systems for sparkles, hearts, moons, sigils, or snowflakes. - Each theme must have its own visible low-cost ultimate effect: mecha themes use scanner/core/targeting effects, and magical-girl themes use large magic circles, star glints, and ribbon bursts. Do not implement the global effect as dozens of tiny rotating particles. - The background desktop is part of the interface. Major surfaces must remain translucent enough for the selected built-in or uploaded background to show through while preserving readable text. - Operational clarity wins over decoration. Logs, diffs, forms, errors, warning states, destructive confirmations, LLM review output, and operation/job feedback must stay readable and traceable. - Default visual assets must be original CSS/generated motifs. Do not bundle recognizable third-party character art. Users may upload their own backgrounds. ## Files - `tokens.ts`: theme palette IDs, built-in desktop preset IDs, CSS variable values, local-storage keys, apply/persist helpers, and the live palette-change event used to keep shell chrome synchronized after theme switches. - `base.css`: shared CSS primitives for the shell, collapsible sidebar navigation, account/profile panel, cards, panels, tables, drawers, dialogs, logs, diffs, plugin groups, command buttons, mecha frame cuts, magical star frames, global background/particle layer placement, and responsive behavior. - `tokens.test.ts`: tests that lock the default black mecha palette, built-in mecha/magical desktop presets, palette-variable cleanup, and theme-change notifications. - `base-css.test.js`: CSS contract tests for shared frame behavior that should not be represented in TypeScript token tests. ## Background Rules - `defaultThemePaletteId` must remain `mecha-black` unless an OpenSpec change explicitly replaces the visual direction. - `defaultThemeBackgroundId` should point to a built-in mecha desktop preset that works without uploaded imagery. - Built-in presets use CSS variables named `--workspace-background-pattern-*` and render behind the app shell. - Uploaded backgrounds use `--workspace-background-image`, set `data-custom-background="true"`, and take visual precedence over the selected preset. - Removing an uploaded background must reveal the selected built-in preset again. - Theme palette switches must keep uploaded backgrounds intact. Changing from magical-girl to black mecha, or back again, must not clear `--workspace-background-image` or alter the custom-background fallback preset. - Any new preset must include an `id`, `label`, `summary`, `preview`, and all required `--workspace-background-pattern-*` variables. Current presets are 机甲格纳库 and 粉月魔法阵. ## Theme Switching Rules - `applyThemePalette()` is the single writer for active palette variables. It must remove the union of known palette variables before applying the selected palette so previous-theme variables, such as magical-girl accessory SVGs, cannot leak into black mecha. - After applying a palette, `applyThemePalette()` must dispatch `themePaletteChangeEvent`. Shell chrome such as the sidebar subtitle and mini swatch strip should subscribe to that event instead of keeping stale local theme labels. - `data-theme-palette` on the document root is the source of truth for theme-specific CSS selectors. Do not infer the active theme from route state, component-local state, or background preset IDs. - Theme changes must be visually atomic: root marker, CSS variables, menu/sidebar chrome, active navigation frame, and shared surface accessories should all reflect the same selected palette immediately. ## Surface Rules - Use shared classes such as `metric-card`, `overview-card`, `console-panel`, `catalog-card`, `server-card`, `server-detail-header`, `resource-table-wrap`, `provider-table-wrap`, `drawer-panel`, `confirm-panel`, `plugin-group`, and `operation-item` instead of creating page-local card styles. - Shared framed surfaces should use `var(--panel-material)`, `var(--panel-shadow)`, `var(--frame-corner)`, and `var(--frame-accent)` so each theme can change structure, fill, and glow style beyond simple color swaps. - A visual region should have only one ornamental frame at a hierarchy level. If a `.state-view` is nested inside a shared framed parent such as `.console-panel`, `.catalog-card`, `.server-card`, `.resource-table-wrap`, `.provider-table-wrap`, `.server-table-wrap`, `.plugin-group`, or `.operation-item`, the parent owns the frame and the nested state view must render as transparent, borderless content with no `::before` or `::after` accessory. - Standalone `.state-view` instances may keep their own readable state treatment when they are not inside an already framed surface. - Menu frames should use `var(--menu-item-bg)`, `var(--menu-item-active-bg)`, `var(--menu-glyph-bg)`, `var(--menu-title-shadow)`, and `var(--menu-active-outline)` so each theme changes active-state treatment, icon material, and rail/sidebar structure. - Shared decoration variables are part of the contract: `--frosted-edge`, `--frosted-surface`, `--corner-sparkle`, `--jelly-highlight`, `--sugar-dust`, `--crystal-edge-glow`, and `--jelly-inset`. In mecha themes these become scanner/grid/bevel materials; in magical themes they become star, ribbon, and jelly-glass materials. - Full-screen ambient motifs use the shared `MagicalParticleLayer` background layer and global particle DOM layer. They should remain non-interactive, theme-colored, reduced-motion aware, and behind operational surfaces. Page code should not create one-off fixed decoration containers. - Keep framed repeated items at 8px radius or less. Pills and circular avatars are allowed for native pill/circle controls. - Do not replace shared panels with opaque white cards, heavy dark themes, unthemed gradients, or one-off color systems. - Do not use color as the only status signal. Pair color with text or familiar status icons. ## Adding UI 1. Reuse existing shared surface, command, table, form, status, and state-view classes first. 2. If a new shared pattern is truly needed, add it in `base.css` and describe its intended use here. 3. When placing empty/loading/error states inside an existing shared panel, verify the state view does not introduce a second framed panel or accessory layer. 4. If a new palette or background preset is added, update `tokens.ts`, `tokens.test.ts`, and any CSS contract tests together. 5. Run `npm run typecheck`, `npm test`, `npm run build`, `scripts/check-structure.sh`, and `openspec validate --strict` before claiming completion. 6. For page or interaction changes, perform a browser walkthrough before marking visual acceptance tasks complete. At minimum, switch black mecha -> magical-girl -> black mecha with both built-in and uploaded backgrounds when the change touches theme switching, frame accessories, or custom-background styling.