first commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# 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 <change> --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.
|
||||
@@ -0,0 +1,13 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("platform web shared theme CSS", () => {
|
||||
it("suppresses nested state frames inside shared framed surfaces", () => {
|
||||
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
|
||||
const nestedStateReset = themeCss.slice(themeCss.indexOf("A state view inside an already framed surface"));
|
||||
|
||||
expect(nestedStateReset).toContain(".state-view::after");
|
||||
expect(nestedStateReset).toContain("content: none");
|
||||
expect(nestedStateReset).toContain("background: transparent");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
applyThemePalette,
|
||||
defaultThemeBackgroundId,
|
||||
defaultThemePaletteId,
|
||||
getThemeBackgroundPreset,
|
||||
getThemePalette,
|
||||
themeBackgroundPresets,
|
||||
themePaletteChangeEvent,
|
||||
themePalettes
|
||||
} from "./tokens";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("platform web theme palettes", () => {
|
||||
it("offers black mecha and magical-girl palettes with usable swatches", () => {
|
||||
expect(themePalettes.map((palette) => palette.label)).toEqual(["黑色机甲", "魔法少女"]);
|
||||
|
||||
for (const palette of themePalettes) {
|
||||
expect(palette.swatches.length).toBeGreaterThanOrEqual(5);
|
||||
expect(palette.variables["--accent"]).toMatch(/^#[0-9a-f]{6}$/i);
|
||||
expect(palette.variables["--surface"]).toContain("rgba");
|
||||
expect(palette.variables["--rim-light"]).toContain("rgba");
|
||||
expect(palette.variables["--crystal-rim"]).toContain("rgba");
|
||||
expect(palette.variables["--sparkle-gold"]).toContain("rgba");
|
||||
expect(palette.variables["--panel-material"]).toContain("var");
|
||||
expect(palette.variables["--menu-item-bg"]).toContain("gradient");
|
||||
expect(palette.variables["--frame-accessory-top"]).toContain("data:image/svg+xml");
|
||||
expect(palette.variables["--frame-accessory-bottom"]).toContain("data:image/svg+xml");
|
||||
expect(palette.variables["--frame-accessory-opacity"]).toMatch(/^0\.\d{2}$/);
|
||||
expect(accessorySizeNumbers(palette.variables["--frame-accessory-size"])).toEqual(
|
||||
expect.arrayContaining([expect.any(Number)])
|
||||
);
|
||||
expect(palette.variables["--custom-background-overlay"]).toBe("transparent");
|
||||
|
||||
const accessoryOpacity = Number(palette.variables["--frame-accessory-opacity"]);
|
||||
const largestAccessoryDimension = Math.max(...accessorySizeNumbers(palette.variables["--frame-accessory-size"]));
|
||||
|
||||
if (palette.id === "magical-girl") {
|
||||
expect(accessoryOpacity).toBeGreaterThanOrEqual(0.65);
|
||||
expect(largestAccessoryDimension).toBeGreaterThanOrEqual(56);
|
||||
expect(largestAccessoryDimension).toBeLessThanOrEqual(80);
|
||||
expect(
|
||||
new Set([
|
||||
palette.variables["--frame-accessory-heart"],
|
||||
palette.variables["--frame-accessory-wand"],
|
||||
palette.variables["--frame-accessory-moon"],
|
||||
palette.variables["--frame-accessory-ribbon"],
|
||||
palette.variables["--frame-accessory-crystal"],
|
||||
palette.variables["--frame-accessory-circle"]
|
||||
]).size
|
||||
).toBe(6);
|
||||
} else {
|
||||
expect(accessoryOpacity).toBeLessThanOrEqual(0.4);
|
||||
expect(largestAccessoryDimension).toBeLessThanOrEqual(60);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to the default black mecha palette", () => {
|
||||
expect(defaultThemePaletteId).toBe("mecha-black");
|
||||
expect(getThemePalette("missing").id).toBe("mecha-black");
|
||||
});
|
||||
|
||||
it("clears stale palette variables and publishes the active palette", () => {
|
||||
const removeProperty = vi.fn();
|
||||
const setProperty = vi.fn();
|
||||
const dispatchEvent = vi.fn(() => true);
|
||||
|
||||
class TestCustomEvent<T> {
|
||||
readonly type: string;
|
||||
readonly detail: T | null;
|
||||
|
||||
constructor(type: string, init?: CustomEventInit<T>) {
|
||||
this.type = type;
|
||||
this.detail = init?.detail ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal("document", {
|
||||
documentElement: {
|
||||
dataset: {},
|
||||
style: { removeProperty, setProperty }
|
||||
}
|
||||
});
|
||||
vi.stubGlobal("window", { dispatchEvent });
|
||||
vi.stubGlobal("CustomEvent", TestCustomEvent);
|
||||
|
||||
applyThemePalette("mecha-black");
|
||||
|
||||
expect(removeProperty).toHaveBeenCalledWith("--frame-accessory-heart");
|
||||
expect(setProperty).toHaveBeenCalledWith("--accent", "#48e6ff");
|
||||
expect(dispatchEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: themePaletteChangeEvent,
|
||||
detail: { paletteId: "mecha-black" }
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("offers mecha and magical desktop presets with a mecha default", () => {
|
||||
expect(defaultThemeBackgroundId).toBe("mecha-grid");
|
||||
expect(themeBackgroundPresets.map((preset) => preset.label)).toEqual(["机甲格纳库", "粉月魔法阵"]);
|
||||
|
||||
for (const preset of themeBackgroundPresets) {
|
||||
expect(preset.preview).toContain("gradient");
|
||||
expect(preset.variables["--workspace-background-pattern-image"]).toContain("gradient");
|
||||
expect(preset.variables["--workspace-background-pattern-opacity"]).toMatch(/^0\./);
|
||||
}
|
||||
|
||||
expect(getThemeBackgroundPreset("missing").id).toBe("mecha-grid");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function accessorySizeNumbers(sizeValue: string): number[] {
|
||||
return Array.from(sizeValue.matchAll(/(\d+)px/g), (match) => Number(match[1]));
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
export const themeTokens = {
|
||||
appName: "Mecha Game Console",
|
||||
contentMaxWidth: "1180px",
|
||||
accent: "#48e6ff",
|
||||
accentSecondary: "#ff77c8",
|
||||
narrowBreakpoint: 760
|
||||
} as const;
|
||||
|
||||
export type ThemePaletteId = "mecha-black" | "magical-girl";
|
||||
export type ThemeBackgroundId = "mecha-grid" | "magic-stage";
|
||||
|
||||
export const themePaletteChangeEvent = "platform-web:theme-palette-change";
|
||||
|
||||
export interface ThemePaletteChangeDetail {
|
||||
paletteId: ThemePaletteId;
|
||||
}
|
||||
|
||||
export interface ThemePalette {
|
||||
id: ThemePaletteId;
|
||||
label: string;
|
||||
summary: string;
|
||||
swatches: string[];
|
||||
variables: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ThemeBackgroundPreset {
|
||||
id: ThemeBackgroundId;
|
||||
label: string;
|
||||
summary: string;
|
||||
preview: string;
|
||||
variables: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface WorkspaceThemeState {
|
||||
paletteId: ThemePaletteId;
|
||||
backgroundPresetId: ThemeBackgroundId;
|
||||
backgroundImage: string | null;
|
||||
}
|
||||
|
||||
const backgroundStorageKey = "platform-web.theme.background";
|
||||
const backgroundPresetStorageKey = "platform-web.theme.backgroundPreset";
|
||||
const paletteStorageKey = "platform-web.theme.palette";
|
||||
|
||||
const mechaFrameAccessoryTop =
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 132 48'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 31h34l9-12h28l7 8h18' stroke='%2348e6ff' stroke-opacity='.86' stroke-width='2.4'/%3E%3Cpath d='M75 17h16l10 10M25 36h22M100 32h15' stroke='%23ffb84d' stroke-opacity='.68' stroke-width='1.7'/%3E%3Ccircle cx='58' cy='24' r='8' stroke='%239af7ff' stroke-opacity='.58' stroke-width='1.6'/%3E%3Cpath d='M58 15v18M49 24h18' stroke='%239af7ff' stroke-opacity='.34' stroke-width='1.2'/%3E%3Cpath d='M106 10l4 4 6-1-4 4 2 6-6-3-5 4 1-7-5-4 6 1z' stroke='%23ffb84d' stroke-opacity='.58' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E\")";
|
||||
|
||||
const mechaFrameAccessoryBottom =
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 132 56'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 38h30l12-19 15 25h25' stroke='%2348e6ff' stroke-opacity='.78' stroke-width='2.4'/%3E%3Cpath d='M34 38l9-13 8 13M74 37h21' stroke='%23ffb84d' stroke-opacity='.62' stroke-width='1.7'/%3E%3Ccircle cx='100' cy='26' r='15' stroke='%239af7ff' stroke-opacity='.54' stroke-width='1.6'/%3E%3Ccircle cx='100' cy='26' r='7' stroke='%23ffb84d' stroke-opacity='.62' stroke-width='1.4'/%3E%3Cpath d='M84 26h32M100 10v32M89 15l22 22M111 15L89 37' stroke='%239af7ff' stroke-opacity='.28' stroke-width='1.1'/%3E%3Cpath d='M18 20l11-7 16 4-13 9z' stroke='%239af7ff' stroke-opacity='.58' stroke-width='1.5'/%3E%3C/g%3E%3C/svg%3E\")";
|
||||
|
||||
const magicalFrameAccessoryHeart =
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 48'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M36 38S23 29 23 20c0-7 9-9 13-3 4-6 13-4 13 3 0 9-13 18-13 18z' fill='%23ff77c8' fill-opacity='.12' stroke='%23fff6fb' stroke-width='2'/%3E%3Cpath d='M22 20c-6-4-11-2-15 2 5 0 8 2 11 6-5-1-8 1-10 4 7-1 12 1 17 5M50 20c6-4 11-2 15 2-5 0-8 2-11 6 5-1 8 1 10 4-7-1-12 1-17 5' stroke='%23ffd66d' stroke-opacity='.92' stroke-width='1.7'/%3E%3Cpath d='M36 8l2 4 5 1-4 3 1 5-4-3-4 3 1-5-4-3 5-1z' fill='%23ffd66d' fill-opacity='.22' stroke='%23ffd66d' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E\")";
|
||||
|
||||
const magicalFrameAccessoryWand =
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 48'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 40L45 13' stroke='%23ff77c8' stroke-width='3'/%3E%3Cpath d='M49 5l3 6 7 1-5 5 1 7-6-4-7 4 2-7-6-5 7-1z' fill='%23ffd66d' fill-opacity='.2' stroke='%23ffd66d' stroke-width='1.9'/%3E%3Cpath d='M12 14l1.5 3 3.5.5-2.5 2.5.5 3.5-3-2-3 2 .5-3.5L6 17.5l3.5-.5zM59 29l1.5 3 3.5.5-2.5 2.5.5 3.5-3-2-3 2 .5-3.5-2.5-2.5 3.5-.5z' stroke='%23fff6fb' stroke-opacity='.9' stroke-width='1.3'/%3E%3Cpath d='M15 36l7 7' stroke='%23ffd66d' stroke-opacity='.86' stroke-width='1.6'/%3E%3C/g%3E%3C/svg%3E\")";
|
||||
|
||||
const magicalFrameAccessoryMoon =
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 48'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M42 8c-9 2-15 10-13 19 2 10 12 15 21 11-6-1-11-6-12-12-2-7 0-13 4-18z' fill='%23ff77c8' fill-opacity='.1' stroke='%23fff6fb' stroke-width='2'/%3E%3Cpath d='M19 19l2.5 5 5.5 1-4 4 1 6-5-3-5 3 1-6-4-4 5.5-1z' fill='%23ffd66d' fill-opacity='.2' stroke='%23ffd66d' stroke-width='1.7'/%3E%3Cpath d='M48 10c5 1 9 4 12 8M50 38c5-2 9-5 12-10' stroke='%23ff77c8' stroke-opacity='.84' stroke-width='1.6'/%3E%3Ccircle cx='59' cy='24' r='2' fill='%23fff6fb'/%3E%3C/g%3E%3C/svg%3E\")";
|
||||
|
||||
const magicalFrameAccessoryRibbon =
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 48'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M35 22c-7-8-17-9-20-4-3 6 5 12 20 8M37 22c7-8 17-9 20-4 3 6-5 12-20 8' fill='%23ff77c8' fill-opacity='.12' stroke='%23fff6fb' stroke-width='2'/%3E%3Cpath d='M35 25L23 42l13-5 5 5 3-17M37 25l12 17-13-5-5 5-3-17' stroke='%23ff77c8' stroke-width='1.8'/%3E%3Ccircle cx='36' cy='24' r='5' fill='%23ffd66d' fill-opacity='.18' stroke='%23ffd66d' stroke-width='1.6'/%3E%3Cpath d='M10 10l1.5 3 3.5.5-2.5 2.5.5 3.5-3-2-3 2 .5-3.5L4 13.5l3.5-.5zM60 8l1.5 3 3.5.5-2.5 2.5.5 3.5-3-2-3 2 .5-3.5-2.5-2.5 3.5-.5z' stroke='%23ffd66d' stroke-width='1.2'/%3E%3C/g%3E%3C/svg%3E\")";
|
||||
|
||||
const magicalFrameAccessoryCrystal =
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 48'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M36 5l16 14-6 22H26l-6-22z' fill='%23ff77c8' fill-opacity='.09' stroke='%23fff6fb' stroke-width='2'/%3E%3Cpath d='M20 19h32M36 5l-8 14 8 22 8-22zM26 41l10-22 10 22' stroke='%23ffd66d' stroke-opacity='.88' stroke-width='1.35'/%3E%3Cpath d='M10 16l2 4 4 .5-3 3 .8 4.5-3.8-2.4L6 28l1-4.5-3-3 4-.5zM59 9l1.5 3 3.5.5-2.5 2.5.5 3.5-3-2-3 2 .5-3.5-2.5-2.5 3.5-.5z' stroke='%23ff77c8' stroke-width='1.3'/%3E%3C/g%3E%3C/svg%3E\")";
|
||||
|
||||
const magicalFrameAccessoryCircle =
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 48'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='36' cy='24' r='18' stroke='%23fff6fb' stroke-width='1.7'/%3E%3Ccircle cx='36' cy='24' r='11' stroke='%23ffd66d' stroke-width='1.4'/%3E%3Cpath d='M36 6v36M18 24h36M23 11l26 26M49 11L23 37' stroke='%23ff77c8' stroke-opacity='.68' stroke-width='1.1'/%3E%3Cpath d='M36 14l3 6 7 1-5 5 1 7-6-4-6 4 1-7-5-5 7-1z' fill='%23ffd66d' fill-opacity='.12' stroke='%23ffd66d' stroke-width='1.2'/%3E%3C/g%3E%3C/svg%3E\")";
|
||||
|
||||
const mechaMaterialVariables = {
|
||||
"--surface": "rgba(5, 12, 18, 0.58)",
|
||||
"--surface-solid": "rgba(6, 14, 21, 0.88)",
|
||||
"--surface-raised": "rgba(10, 24, 32, 0.64)",
|
||||
"--glass-wash": "linear-gradient(145deg, rgba(33, 55, 64, 0.46), rgba(5, 12, 18, 0.42) 46%, rgba(72, 230, 255, 0.08))",
|
||||
"--shine-sweep": "linear-gradient(112deg, transparent 0 38%, rgba(132, 243, 255, 0.24) 46%, transparent 56%)",
|
||||
"--glass-tint": "rgba(72, 230, 255, 0.08)",
|
||||
"--crystal-rim": "rgba(137, 239, 255, 0.64)",
|
||||
"--moonbeam": "rgba(60, 176, 255, 0.34)",
|
||||
"--diamond-line": "rgba(119, 237, 255, 0.34)",
|
||||
"--glass-shadow": "rgba(0, 0, 0, 0.56)",
|
||||
"--crystal-spark": "rgba(172, 250, 255, 0.92)",
|
||||
"--sparkle": "rgba(196, 252, 255, 0.96)",
|
||||
"--sparkle-gold": "rgba(255, 184, 77, 0.82)",
|
||||
"--corner-sparkle":
|
||||
"linear-gradient(135deg, rgba(72, 230, 255, 0.72) 0 2px, transparent 2px 100%), linear-gradient(315deg, rgba(255, 184, 77, 0.72) 0 2px, transparent 2px 100%), radial-gradient(circle at 86% 18%, rgba(132, 243, 255, 0.88) 0 1.4px, transparent 2.4px)",
|
||||
"--jelly-highlight": "linear-gradient(145deg, rgba(132, 243, 255, 0.16), transparent 32%), radial-gradient(circle at 82% 16%, rgba(255, 184, 77, 0.16), transparent 24%)",
|
||||
"--sugar-dust":
|
||||
"linear-gradient(90deg, rgba(72, 230, 255, 0.08) 1px, transparent 1px), linear-gradient(180deg, rgba(72, 230, 255, 0.07) 1px, transparent 1px), radial-gradient(circle at 88% 18%, rgba(255, 184, 77, 0.62) 0 1.2px, transparent 2px)",
|
||||
"--crystal-edge-glow": "0 0 0 1px rgba(72, 230, 255, 0.28), 0 0 22px rgba(72, 230, 255, 0.2), 0 0 44px rgba(0, 0, 0, 0.48)",
|
||||
"--jelly-inset": "inset 0 1px 0 rgba(180, 248, 255, 0.22), inset 0 -1px 0 rgba(72, 230, 255, 0.16), inset 0 0 24px rgba(72, 230, 255, 0.08)",
|
||||
"--frosted-edge": "linear-gradient(180deg, rgba(132, 243, 255, 0.18), rgba(7, 12, 18, 0.74) 48%, rgba(255, 184, 77, 0.1))",
|
||||
"--frosted-surface": "linear-gradient(145deg, rgba(14, 28, 38, 0.62), rgba(5, 12, 18, 0.46) 48%, rgba(72, 230, 255, 0.07))",
|
||||
"--frame-corner": "linear-gradient(135deg, rgba(72, 230, 255, 0.9) 0 2px, transparent 2px), linear-gradient(315deg, rgba(255, 184, 77, 0.72) 0 2px, transparent 2px)",
|
||||
"--frame-accent": "linear-gradient(90deg, rgba(72, 230, 255, 0), rgba(72, 230, 255, 0.78), rgba(255, 184, 77, 0.58), rgba(72, 230, 255, 0))",
|
||||
"--frame-accessory-top": mechaFrameAccessoryTop,
|
||||
"--frame-accessory-bottom": mechaFrameAccessoryBottom,
|
||||
"--frame-accessory-opacity": "0.22",
|
||||
"--frame-accessory-size": "44px 16px, 48px 20px",
|
||||
"--menu-item-bg": "linear-gradient(90deg, rgba(72, 230, 255, 0.14) 0 1px, transparent 1px 100%), linear-gradient(135deg, rgba(17, 31, 43, 0.94), rgba(4, 8, 13, 0.82) 58%, rgba(255, 184, 77, 0.08))",
|
||||
"--menu-item-active-bg": "linear-gradient(90deg, rgba(72, 230, 255, 0.44) 0 3px, transparent 3px 100%), linear-gradient(135deg, rgba(20, 51, 64, 0.98), rgba(6, 11, 18, 0.9) 52%, rgba(255, 184, 77, 0.18))",
|
||||
"--menu-glyph-bg": "linear-gradient(135deg, rgba(72, 230, 255, 0.28), rgba(255, 184, 77, 0.14)), repeating-linear-gradient(90deg, rgba(137, 239, 255, 0.18) 0 1px, transparent 1px 6px), rgba(5, 9, 15, 0.9)",
|
||||
"--menu-title-shadow": "0 0 14px rgba(72, 230, 255, 0.5), 0 0 2px rgba(255, 184, 77, 0.8)",
|
||||
"--menu-active-outline": "linear-gradient(90deg, rgba(72, 230, 255, 0.95), rgba(255, 184, 77, 0.72), rgba(72, 230, 255, 0.95))",
|
||||
"--panel-material": "var(--sugar-dust), var(--glass-wash), linear-gradient(135deg, rgba(5, 12, 18, 0.46), rgba(10, 24, 32, 0.38))",
|
||||
"--panel-shadow": "var(--jelly-inset), inset 0 0 0 1px rgba(119, 237, 255, 0.24), inset 9px 0 0 rgba(72, 230, 255, 0.06), 0 18px 42px rgba(0, 0, 0, 0.42), 0 0 28px rgba(72, 230, 255, 0.11)"
|
||||
} as const;
|
||||
|
||||
const magicalMaterialVariables = {
|
||||
"--surface": "rgba(88, 18, 52, 0.48)",
|
||||
"--surface-solid": "rgba(78, 14, 46, 0.82)",
|
||||
"--surface-raised": "rgba(118, 26, 68, 0.54)",
|
||||
"--glass-wash": "linear-gradient(180deg, rgba(255, 226, 242, 0.2), rgba(120, 24, 68, 0.32) 56%, rgba(72, 12, 42, 0.28))",
|
||||
"--shine-sweep": "radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.58) 0 2px, transparent 3px)",
|
||||
"--glass-tint": "rgba(255, 119, 200, 0.1)",
|
||||
"--crystal-rim": "rgba(255, 236, 250, 0.92)",
|
||||
"--moonbeam": "rgba(255, 185, 226, 0.76)",
|
||||
"--diamond-line": "rgba(255, 217, 239, 0.72)",
|
||||
"--glass-shadow": "rgba(72, 0, 34, 0.36)",
|
||||
"--crystal-spark": "rgba(255, 246, 253, 0.98)",
|
||||
"--sparkle": "rgba(255, 255, 255, 0.98)",
|
||||
"--sparkle-gold": "rgba(255, 221, 117, 0.9)",
|
||||
"--corner-sparkle":
|
||||
"radial-gradient(circle at 86% 14%, rgba(255, 255, 255, 0.98) 0 2.4px, transparent 3.2px), radial-gradient(circle at 93% 23%, rgba(255, 221, 117, 0.92) 0 1.8px, transparent 2.8px)",
|
||||
"--jelly-highlight": "radial-gradient(circle at 18% 14%, rgba(255, 255, 255, 0.72), transparent 30%), linear-gradient(145deg, rgba(255, 255, 255, 0.34), transparent 34%)",
|
||||
"--sugar-dust":
|
||||
"radial-gradient(circle at 14% 20%, rgba(255, 255, 255, 0.95) 0 1.4px, transparent 2.2px), radial-gradient(circle at 86% 18%, rgba(255, 221, 117, 0.9) 0 1.6px, transparent 2.6px), radial-gradient(circle at 78% 84%, rgba(255, 119, 200, 0.76) 0 1.8px, transparent 2.8px)",
|
||||
"--crystal-edge-glow": "0 0 0 1px rgba(255, 217, 239, 0.78), 0 0 18px rgba(255, 119, 200, 0.46), 0 0 34px rgba(255, 221, 117, 0.18)",
|
||||
"--jelly-inset": "inset 0 1px 0 rgba(255, 255, 255, 0.52), inset 0 -1px 0 rgba(255, 119, 200, 0.28), inset 0 0 20px rgba(255, 255, 255, 0.18)",
|
||||
"--frosted-edge": "linear-gradient(180deg, rgba(255, 255, 255, 0.58), rgba(255, 119, 200, 0.2) 48%, rgba(255, 221, 117, 0.18))",
|
||||
"--frosted-surface": "linear-gradient(180deg, rgba(255, 226, 242, 0.18), rgba(118, 24, 66, 0.36) 52%, rgba(72, 12, 42, 0.28))",
|
||||
"--frame-corner": "radial-gradient(circle at 0 0, rgba(255, 221, 117, 0.9) 0 4px, transparent 5px), radial-gradient(circle at 100% 100%, rgba(255, 119, 200, 0.9) 0 4px, transparent 5px)",
|
||||
"--frame-accent": "linear-gradient(90deg, rgba(255, 221, 117, 0), rgba(255, 221, 117, 0.86), rgba(255, 119, 200, 0.78), rgba(255, 221, 117, 0))",
|
||||
"--frame-accessory-top": magicalFrameAccessoryWand,
|
||||
"--frame-accessory-bottom": magicalFrameAccessoryCircle,
|
||||
"--frame-accessory-heart": magicalFrameAccessoryHeart,
|
||||
"--frame-accessory-wand": magicalFrameAccessoryWand,
|
||||
"--frame-accessory-moon": magicalFrameAccessoryMoon,
|
||||
"--frame-accessory-ribbon": magicalFrameAccessoryRibbon,
|
||||
"--frame-accessory-crystal": magicalFrameAccessoryCrystal,
|
||||
"--frame-accessory-circle": magicalFrameAccessoryCircle,
|
||||
"--frame-accessory-opacity": "0.78",
|
||||
"--frame-accessory-size": "68px 44px",
|
||||
"--menu-item-bg": "radial-gradient(circle at 92% 12%, rgba(255, 255, 255, 0.52) 0 3px, transparent 4px), linear-gradient(180deg, rgba(255, 185, 226, 0.22), rgba(135, 10, 65, 0.42) 68%, rgba(94, 0, 44, 0.48))",
|
||||
"--menu-item-active-bg": "radial-gradient(circle at 96% 18%, rgba(255, 255, 255, 0.72) 0 3px, transparent 4px), linear-gradient(180deg, rgba(255, 203, 232, 0.34), rgba(177, 22, 86, 0.58) 54%, rgba(105, 0, 48, 0.62))",
|
||||
"--menu-glyph-bg": "radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.78), transparent 34%), radial-gradient(circle at 72% 80%, rgba(255, 221, 117, 0.42), transparent 30%), linear-gradient(135deg, rgba(255, 119, 200, 0.82), rgba(255, 221, 117, 0.44))",
|
||||
"--menu-title-shadow": "0 2px 0 rgba(96, 24, 24, 0.86), 1px 0 0 rgba(96, 24, 24, 0.86), -1px 0 0 rgba(96, 24, 24, 0.86), 0 0 12px rgba(255, 221, 117, 0.48)",
|
||||
"--menu-active-outline": "linear-gradient(90deg, rgba(255, 221, 117, 0.96), rgba(255, 246, 253, 0.92), rgba(255, 119, 200, 0.9), rgba(255, 221, 117, 0.96))",
|
||||
"--panel-material": "var(--sugar-dust), var(--glass-wash), linear-gradient(180deg, rgba(106, 22, 60, 0.3), rgba(72, 12, 42, 0.3))",
|
||||
"--panel-shadow": "var(--jelly-inset), inset 0 0 0 1px rgba(255, 217, 239, 0.5), 0 18px 42px rgba(72, 0, 34, 0.28), 0 0 30px rgba(255, 119, 200, 0.2), 0 0 16px rgba(255, 221, 117, 0.1)"
|
||||
} as const;
|
||||
|
||||
export const themePalettes: ThemePalette[] = [
|
||||
{
|
||||
id: "mecha-black",
|
||||
label: "黑色机甲",
|
||||
summary: "黑钢面板、青蓝扫描线、琥珀能量灯",
|
||||
swatches: ["#05090f", "#111c28", "#48e6ff", "#ffb84d", "#e7f7ff"],
|
||||
variables: {
|
||||
"--ink": "#e7f7ff",
|
||||
"--ink-soft": "#b9d2dc",
|
||||
"--ink-faint": "#84a1ad",
|
||||
...mechaMaterialVariables,
|
||||
"--line": "rgba(116, 236, 255, 0.32)",
|
||||
"--line-strong": "rgba(132, 243, 255, 0.7)",
|
||||
"--accent": "#48e6ff",
|
||||
"--accent-deep": "#9af7ff",
|
||||
"--accent-soft": "rgba(72, 230, 255, 0.16)",
|
||||
"--teal": "#4fffd7",
|
||||
"--teal-soft": "rgba(79, 255, 215, 0.14)",
|
||||
"--pink": "#ff4d6d",
|
||||
"--pink-soft": "rgba(255, 77, 109, 0.13)",
|
||||
"--gold": "#ffb84d",
|
||||
"--gold-soft": "rgba(255, 184, 77, 0.18)",
|
||||
"--danger": "#ff5a6e",
|
||||
"--danger-soft": "rgba(255, 90, 110, 0.14)",
|
||||
"--success": "#4fffd7",
|
||||
"--success-soft": "rgba(79, 255, 215, 0.18)",
|
||||
"--background-base": "#05070b",
|
||||
"--background-glow-1": "rgba(72, 230, 255, 0.16)",
|
||||
"--background-glow-2": "rgba(255, 184, 77, 0.11)",
|
||||
"--background-glow-3": "rgba(255, 77, 109, 0.1)",
|
||||
"--rim-light": "rgba(180, 248, 255, 0.64)",
|
||||
"--candy-glow": "rgba(72, 230, 255, 0.32)",
|
||||
"--ultimate-effect-alpha": "0.86",
|
||||
"--custom-background-overlay": "transparent",
|
||||
"--code-surface": "#05090f",
|
||||
"--code-ink": "#dffcff",
|
||||
"--code-muted": "#87afba"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "magical-girl",
|
||||
label: "魔法少女",
|
||||
summary: "粉色舞台、金色星框、显眼魔法阵",
|
||||
swatches: ["#8b0a40", "#ff77c8", "#ffd66d", "#fff6fb", "#8cf0ff"],
|
||||
variables: {
|
||||
"--ink": "#fff8fd",
|
||||
"--ink-soft": "#ffe8f5",
|
||||
"--ink-faint": "#ffd0e9",
|
||||
...magicalMaterialVariables,
|
||||
"--line": "rgba(255, 201, 230, 0.42)",
|
||||
"--line-strong": "rgba(255, 225, 139, 0.76)",
|
||||
"--accent": "#ff77c8",
|
||||
"--accent-deep": "#fff6fb",
|
||||
"--accent-soft": "rgba(255, 119, 200, 0.18)",
|
||||
"--teal": "#8cf0ff",
|
||||
"--teal-soft": "rgba(140, 240, 255, 0.16)",
|
||||
"--pink": "#ff8fd2",
|
||||
"--pink-soft": "rgba(255, 143, 210, 0.22)",
|
||||
"--gold": "#ffd66d",
|
||||
"--gold-soft": "rgba(255, 214, 109, 0.24)",
|
||||
"--danger": "#ff8a9d",
|
||||
"--danger-soft": "rgba(255, 138, 157, 0.18)",
|
||||
"--success": "#8cf0ff",
|
||||
"--success-soft": "rgba(140, 240, 255, 0.2)",
|
||||
"--background-base": "#7a0b39",
|
||||
"--background-glow-1": "rgba(255, 119, 200, 0.42)",
|
||||
"--background-glow-2": "rgba(255, 214, 109, 0.28)",
|
||||
"--background-glow-3": "rgba(140, 240, 255, 0.18)",
|
||||
"--rim-light": "rgba(255, 255, 255, 0.92)",
|
||||
"--candy-glow": "rgba(255, 119, 200, 0.48)",
|
||||
"--ultimate-effect-alpha": "0.96",
|
||||
"--custom-background-overlay": "transparent",
|
||||
"--code-surface": "#4b092a",
|
||||
"--code-ink": "#fff4fb",
|
||||
"--code-muted": "#ffc6e4"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const themePaletteVariableNames = Array.from(new Set(themePalettes.flatMap((palette) => Object.keys(palette.variables))));
|
||||
|
||||
export const defaultThemePaletteId: ThemePaletteId = "mecha-black";
|
||||
export const defaultThemeBackgroundId: ThemeBackgroundId = "mecha-grid";
|
||||
|
||||
export const themeBackgroundPresets: ThemeBackgroundPreset[] = [
|
||||
{
|
||||
id: "mecha-grid",
|
||||
label: "机甲格纳库",
|
||||
summary: "黑钢格栅、雷达圆环、青蓝扫描光",
|
||||
preview:
|
||||
"radial-gradient(circle at 76% 18%, rgba(72, 230, 255, 0.86), transparent 18%), repeating-linear-gradient(90deg, rgba(72, 230, 255, 0.34) 0 1px, transparent 1px 18px), linear-gradient(135deg, #05090f, #111c28)",
|
||||
variables: {
|
||||
"--workspace-background-pattern-image":
|
||||
"radial-gradient(circle at 76% 18%, transparent 0 68px, rgba(72, 230, 255, 0.3) 69px 71px, transparent 72px), radial-gradient(circle at 76% 18%, transparent 0 34px, rgba(255, 184, 77, 0.24) 35px 36px, transparent 37px), conic-gradient(from 0deg at 76% 18%, transparent 0 8%, rgba(72, 230, 255, 0.24) 8% 9%, transparent 9% 28%, rgba(72, 230, 255, 0.18) 28% 29%, transparent 29% 100%), linear-gradient(90deg, rgba(72, 230, 255, 0.08) 1px, transparent 1px), linear-gradient(180deg, rgba(72, 230, 255, 0.07) 1px, transparent 1px), linear-gradient(135deg, transparent 0 48%, rgba(255, 184, 77, 0.1) 49% 50%, transparent 51% 100%), radial-gradient(circle at 20% 78%, rgba(255, 77, 109, 0.12), transparent 28%)",
|
||||
"--workspace-background-pattern-size": "auto, auto, auto, 42px 42px, 42px 42px, 180px 180px, auto",
|
||||
"--workspace-background-pattern-position": "center, center, center, 0 0, 0 0, 0 0, center",
|
||||
"--workspace-background-pattern-repeat": "no-repeat, no-repeat, no-repeat, repeat, repeat, repeat, no-repeat",
|
||||
"--workspace-background-pattern-opacity": "0.92",
|
||||
"--workspace-background-pattern-filter": "saturate(1.18) contrast(1.08)"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "magic-stage",
|
||||
label: "粉月魔法阵",
|
||||
summary: "粉色舞台、星星边框、底部大型魔法阵",
|
||||
preview:
|
||||
"radial-gradient(circle at 86% 86%, transparent 0 28%, rgba(255, 255, 255, 0.72) 29% 30%, transparent 31%), radial-gradient(circle at 25% 22%, #ff77c8, transparent 34%), linear-gradient(180deg, #f08bb4, #8b0a40)",
|
||||
variables: {
|
||||
"--workspace-background-pattern-image":
|
||||
"radial-gradient(circle, rgba(255, 255, 255, 0.9) 0 1.4px, transparent 2.2px), radial-gradient(circle, rgba(255, 221, 117, 0.86) 0 1.8px, transparent 2.8px), radial-gradient(circle at 88% 84%, transparent 0 112px, rgba(255, 255, 255, 0.28) 113px 115px, transparent 116px), radial-gradient(circle at 88% 84%, transparent 0 72px, rgba(255, 119, 200, 0.28) 73px 75px, transparent 76px), conic-gradient(from 30deg at 88% 84%, transparent 0 8%, rgba(255, 221, 117, 0.18) 8% 9%, transparent 9% 24%, rgba(255, 255, 255, 0.18) 24% 25%, transparent 25% 100%), linear-gradient(180deg, rgba(255, 203, 226, 0.26), rgba(122, 11, 57, 0.12)), radial-gradient(circle at 18% 20%, rgba(255, 143, 210, 0.28), transparent 28%)",
|
||||
"--workspace-background-pattern-size": "76px 76px, 118px 118px, auto, auto, auto, auto, auto",
|
||||
"--workspace-background-pattern-position": "12px 18px, 42px 58px, center, center, center, center, center",
|
||||
"--workspace-background-pattern-repeat": "repeat, repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat",
|
||||
"--workspace-background-pattern-opacity": "0.96",
|
||||
"--workspace-background-pattern-filter": "saturate(1.24)"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export function getThemePalette(id: string | null | undefined): ThemePalette {
|
||||
return themePalettes.find((palette) => palette.id === id) ?? themePalettes[0];
|
||||
}
|
||||
|
||||
export function getThemeBackgroundPreset(id: string | null | undefined): ThemeBackgroundPreset {
|
||||
return themeBackgroundPresets.find((preset) => preset.id === id) ?? themeBackgroundPresets[0];
|
||||
}
|
||||
|
||||
export function loadThemeState(): WorkspaceThemeState {
|
||||
if (typeof window === "undefined") {
|
||||
return { paletteId: defaultThemePaletteId, backgroundPresetId: defaultThemeBackgroundId, backgroundImage: null };
|
||||
}
|
||||
try {
|
||||
return {
|
||||
paletteId: getThemePalette(window.localStorage.getItem(paletteStorageKey)).id,
|
||||
backgroundPresetId: getThemeBackgroundPreset(window.localStorage.getItem(backgroundPresetStorageKey)).id,
|
||||
backgroundImage: window.localStorage.getItem(backgroundStorageKey)
|
||||
};
|
||||
} catch {
|
||||
return { paletteId: defaultThemePaletteId, backgroundPresetId: defaultThemeBackgroundId, backgroundImage: null };
|
||||
}
|
||||
}
|
||||
|
||||
export function persistThemePalette(paletteId: ThemePaletteId): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.localStorage.setItem(paletteStorageKey, paletteId);
|
||||
} catch {
|
||||
// storage may be unavailable; the palette stays session-only
|
||||
}
|
||||
}
|
||||
|
||||
export function persistThemeBackgroundPreset(backgroundPresetId: ThemeBackgroundId): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.localStorage.setItem(backgroundPresetStorageKey, backgroundPresetId);
|
||||
} catch {
|
||||
// storage may be unavailable; the preset stays session-only
|
||||
}
|
||||
}
|
||||
|
||||
export function persistBackgroundImage(dataUrl: string | null): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (dataUrl) {
|
||||
window.localStorage.setItem(backgroundStorageKey, dataUrl);
|
||||
} else {
|
||||
window.localStorage.removeItem(backgroundStorageKey);
|
||||
}
|
||||
} catch {
|
||||
// storage may be unavailable (private mode); the background stays session-only
|
||||
}
|
||||
}
|
||||
|
||||
export function applyThemePalette(paletteId: ThemePaletteId): void {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
const palette = getThemePalette(paletteId);
|
||||
for (const name of themePaletteVariableNames) {
|
||||
document.documentElement.style.removeProperty(name);
|
||||
}
|
||||
for (const [name, value] of Object.entries(palette.variables)) {
|
||||
document.documentElement.style.setProperty(name, value);
|
||||
}
|
||||
document.documentElement.dataset.themePalette = palette.id;
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent<ThemePaletteChangeDetail>(themePaletteChangeEvent, { detail: { paletteId: palette.id } }));
|
||||
}
|
||||
}
|
||||
|
||||
export function applyThemeBackgroundPreset(backgroundPresetId: ThemeBackgroundId): void {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
const preset = getThemeBackgroundPreset(backgroundPresetId);
|
||||
for (const [name, value] of Object.entries(preset.variables)) {
|
||||
document.documentElement.style.setProperty(name, value);
|
||||
}
|
||||
document.documentElement.dataset.themeBackground = preset.id;
|
||||
}
|
||||
|
||||
export function applyBackgroundImage(dataUrl: string | null): void {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
if (dataUrl) {
|
||||
document.documentElement.style.setProperty("--workspace-background-image", `url(${JSON.stringify(dataUrl)})`);
|
||||
document.documentElement.dataset.customBackground = "true";
|
||||
} else {
|
||||
document.documentElement.style.removeProperty("--workspace-background-image");
|
||||
delete document.documentElement.dataset.customBackground;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user