Feat: Build out server shell

This commit is contained in:
MangoPig
2026-06-16 13:11:14 +01:00
parent 35586729ba
commit 829d7b3d8f
28 changed files with 1990 additions and 149 deletions

View File

@@ -2,10 +2,31 @@
import type { ThemeDefinition } from "./schema";
export const defaultThemePresetPath = "/themes/moku-default.json";
export const themePresetMetas = [
{
id: "moku-default",
name: "Moku Default",
description: "The baseline Moku theme preset, matching the original shell styling tokens.",
path: "/themes/moku-default.json",
},
{
id: "moku-midnight",
name: "Moku Midnight",
description: "The active warm, low-light Moku theme preset inspired by the Midnight Discord palette direction.",
path: "/themes/moku-midnight.json",
},
] as const satisfies readonly (Pick<ThemeDefinition, "id" | "name" | "description"> & { path: string })[];
export const defaultThemePresetPath = "/themes/moku-midnight.json";
export const defaultThemePresetMeta = {
id: "moku-default",
name: "Moku Default",
description: "The baseline Moku theme preset, matching the current shell styling tokens.",
id: "moku-midnight",
name: "Moku Midnight",
description: "The active warm, low-light Moku theme preset inspired by the Midnight Discord palette direction.",
} satisfies Pick<ThemeDefinition, "id" | "name" | "description">;
export const resolveThemePresetPath = (presetId: string): string | null => {
const match = themePresetMetas.find((preset) => preset.id === presetId);
return match?.path ?? null;
};

View File

@@ -1,11 +1,12 @@
// Path: Frontend/src/theme/runtime.ts
import { defaultThemePresetPath } from "./presets";
import { defaultThemePresetMeta, defaultThemePresetPath, resolveThemePresetPath } from "./presets";
import { createCssVariableMap, isThemeModeName, validateThemeDefinition, type ThemeDefinition, type ThemeModeName } from "./schema";
export type Theme = ThemeModeName;
export const THEME_STORAGE_KEY = "theme";
export const THEME_PRESET_STORAGE_KEY = "theme-preset";
export const DEFAULT_THEME: Theme = "light";
let activeThemeDefinition: ThemeDefinition | null = null;
@@ -21,6 +22,14 @@ const getRootElement = (): HTMLElement | null => {
return canUseDom() ? document.documentElement : null;
};
const persistThemePreset = (themeDefinition: ThemeDefinition): void => {
if (!canUseStorage()) {
return;
}
localStorage.setItem(THEME_PRESET_STORAGE_KEY, themeDefinition.id);
};
const setDocumentThemeMode = (theme: Theme): void => {
const rootElement = getRootElement();
@@ -71,10 +80,31 @@ export const getDocumentTheme = (): Theme => {
export const applyThemeDefinition = (themeDefinition: ThemeDefinition, theme: Theme): void => {
activeThemeDefinition = themeDefinition;
persistThemePreset(themeDefinition);
setDocumentThemeMode(theme);
applyThemeVariables(themeDefinition, theme);
};
export const resolvePreferredThemePresetId = (): string => {
if (!canUseStorage()) {
return defaultThemePresetMeta.id;
}
const stored = localStorage.getItem(THEME_PRESET_STORAGE_KEY);
if (stored && resolveThemePresetPath(stored)) {
return stored;
}
return defaultThemePresetMeta.id;
};
export const resolvePreferredThemePresetPath = (): string => {
const presetId = resolvePreferredThemePresetId();
return resolveThemePresetPath(presetId) ?? defaultThemePresetPath;
};
export const initializeThemeRuntime = async (): Promise<ThemeDefinition | null> => {
if (typeof window === "undefined") {
return null;
@@ -88,7 +118,7 @@ export const initializeThemeRuntime = async (): Promise<ThemeDefinition | null>
if (!themeInitializationPromise) {
themeInitializationPromise = (async (): Promise<ThemeDefinition | null> => {
try {
const response = await fetch(defaultThemePresetPath, {
const response = await fetch(resolvePreferredThemePresetPath(), {
headers: {
Accept: "application/json",
},

View File

@@ -17,7 +17,7 @@ export const THEME_Z_INDEX_KEYS = ["base", "dropdown", "sticky", "overlay", "mod
export const THEME_MOTION_KEYS = ["durationFast", "durationBase", "durationSlow", "easeStandard"] as const;
export const THEME_TYPE_SCALE_KEYS = ["caption", "label", "body", "title", "heading", "display"] as const;
export const THEME_FONT_FAMILY_KEYS = ["sans", "heading", "display", "serif", "mono"] as const;
export const THEME_MODE_COLOR_KEYS = ["canvas", "surface", "surfaceMuted", "surfaceHover", "border", "borderStrong", "text", "textMuted", "accent", "accentStrong", "accentSoft", "accentContrast", "success", "danger", "warning", "focusRing"] as const;
export const THEME_MODE_COLOR_KEYS = ["canvas", "surface", "surfaceMuted", "surfaceHover", "border", "borderStrong", "text", "textMuted", "accent", "accentStrong", "accentSoft", "accentContrast", "primaryOne", "primaryTwo", "primaryThree", "success", "danger", "warning", "focusRing"] as const;
export type ThemeModeName = (typeof THEME_MODE_NAMES)[number];
@@ -334,6 +334,9 @@ export const createCssVariableMap = (theme: ThemeDefinition, mode: ThemeModeName
"--color-accent-strong": modeTokens.colors.accentStrong,
"--color-accent-soft": modeTokens.colors.accentSoft,
"--color-accent-contrast": modeTokens.colors.accentContrast,
"--color-primary-1": modeTokens.colors.primaryOne,
"--color-primary-2": modeTokens.colors.primaryTwo,
"--color-primary-3": modeTokens.colors.primaryThree,
"--color-success": modeTokens.colors.success,
"--color-danger": modeTokens.colors.danger,
"--color-warning": modeTokens.colors.warning,