style: auto-format codebase with Prettier

Applied formatting to 1487+ files using pnpm format:write
  - TypeScript/JavaScript files
  - Svelte components
  - Astro pages
  - JSON configs
  - Markdown docs

  13 files still need manual review (Astro JSX comments)
This commit is contained in:
Wuesteon 2025-11-27 18:33:16 +01:00
parent 0241f5554c
commit d36b321d9d
3952 changed files with 661498 additions and 739751 deletions

View file

@ -3,7 +3,12 @@ import type { ThemeVariant, ThemeVariantDefinition, ThemeColors } from './types'
/**
* All available theme variants
*/
export const THEME_VARIANTS: readonly ThemeVariant[] = ['lume', 'nature', 'stone', 'ocean'] as const;
export const THEME_VARIANTS: readonly ThemeVariant[] = [
'lume',
'nature',
'stone',
'ocean',
] as const;
/**
* HSL Color Definitions for all theme variants
@ -22,209 +27,209 @@ export const THEME_VARIANTS: readonly ThemeVariant[] = ['lume', 'nature', 'stone
*/
const lumeLight: ThemeColors = {
primary: '47 95% 58%', // #f8d62b - Gold
primaryForeground: '0 0% 0%', // Black text on gold
secondary: '47 100% 41%', // #D4B200 - Darker gold
secondaryForeground: '0 0% 0%',
background: '0 0% 87%', // #dddddd - Light gray
foreground: '0 0% 17%', // #2c2c2c - Dark text
surface: '0 0% 100%', // #ffffff - White
surfaceHover: '0 0% 96%', // #f5f5f5
surfaceElevated: '0 0% 100%', // #ffffff
muted: '0 0% 90%', // #e6e6e6
mutedForeground: '0 0% 40%', // #666666
border: '0 0% 90%', // #e6e6e6
borderStrong: '0 0% 80%', // #cccccc
error: '6 78% 57%', // #e74c3c
success: '145 63% 42%', // #27ae60
warning: '36 100% 50%', // #f39c12
input: '0 0% 100%', // #ffffff
ring: '47 95% 58%', // Same as primary
primary: '47 95% 58%', // #f8d62b - Gold
primaryForeground: '0 0% 0%', // Black text on gold
secondary: '47 100% 41%', // #D4B200 - Darker gold
secondaryForeground: '0 0% 0%',
background: '0 0% 87%', // #dddddd - Light gray
foreground: '0 0% 17%', // #2c2c2c - Dark text
surface: '0 0% 100%', // #ffffff - White
surfaceHover: '0 0% 96%', // #f5f5f5
surfaceElevated: '0 0% 100%', // #ffffff
muted: '0 0% 90%', // #e6e6e6
mutedForeground: '0 0% 40%', // #666666
border: '0 0% 90%', // #e6e6e6
borderStrong: '0 0% 80%', // #cccccc
error: '6 78% 57%', // #e74c3c
success: '145 63% 42%', // #27ae60
warning: '36 100% 50%', // #f39c12
input: '0 0% 100%', // #ffffff
ring: '47 95% 58%', // Same as primary
};
const lumeDark: ThemeColors = {
primary: '47 95% 58%', // #f8d62b - Gold (same in dark)
primaryForeground: '0 0% 0%', // Black text on gold
secondary: '47 70% 29%', // #7C6B16 - Muted gold
secondaryForeground: '0 0% 100%',
background: '0 0% 6%', // #101010 - Very dark
foreground: '0 0% 100%', // #ffffff - White text
surface: '0 0% 12%', // #1E1E1E - Dark surface
surfaceHover: '0 0% 16%', // #292929
surfaceElevated: '0 0% 14%', // #242424
muted: '0 0% 20%', // #333333
mutedForeground: '0 0% 60%', // #999999
border: '0 0% 26%', // #424242
borderStrong: '0 0% 35%', // #595959
error: '6 78% 57%', // #e74c3c
success: '145 63% 49%', // #2ecc71
warning: '48 100% 50%', // #f1c40f
input: '0 0% 14%', // #242424
ring: '47 95% 58%',
primary: '47 95% 58%', // #f8d62b - Gold (same in dark)
primaryForeground: '0 0% 0%', // Black text on gold
secondary: '47 70% 29%', // #7C6B16 - Muted gold
secondaryForeground: '0 0% 100%',
background: '0 0% 6%', // #101010 - Very dark
foreground: '0 0% 100%', // #ffffff - White text
surface: '0 0% 12%', // #1E1E1E - Dark surface
surfaceHover: '0 0% 16%', // #292929
surfaceElevated: '0 0% 14%', // #242424
muted: '0 0% 20%', // #333333
mutedForeground: '0 0% 60%', // #999999
border: '0 0% 26%', // #424242
borderStrong: '0 0% 35%', // #595959
error: '6 78% 57%', // #e74c3c
success: '145 63% 49%', // #2ecc71
warning: '48 100% 50%', // #f1c40f
input: '0 0% 14%', // #242424
ring: '47 95% 58%',
};
const natureLight: ThemeColors = {
primary: '122 39% 49%', // #4CAF50 - Green
primaryForeground: '0 0% 100%', // White text on green
secondary: '122 38% 63%', // #81C784 - Light green
secondaryForeground: '0 0% 0%',
background: '80 33% 97%', // #FBFDF8 - Very light green tint
foreground: '122 56% 24%', // #1B5E20 - Dark green text
surface: '0 0% 100%', // #ffffff
surfaceHover: '120 25% 95%', // #F1F8E9
surfaceElevated: '0 0% 100%',
muted: '120 25% 95%', // #F1F8E9
mutedForeground: '122 20% 40%',
border: '120 25% 91%', // #E8F5E9
borderStrong: '120 26% 79%', // #C8E6C9
error: '0 65% 67%', // #E57373
success: '122 39% 49%', // Same as primary
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '122 39% 49%',
primary: '122 39% 49%', // #4CAF50 - Green
primaryForeground: '0 0% 100%', // White text on green
secondary: '122 38% 63%', // #81C784 - Light green
secondaryForeground: '0 0% 0%',
background: '80 33% 97%', // #FBFDF8 - Very light green tint
foreground: '122 56% 24%', // #1B5E20 - Dark green text
surface: '0 0% 100%', // #ffffff
surfaceHover: '120 25% 95%', // #F1F8E9
surfaceElevated: '0 0% 100%',
muted: '120 25% 95%', // #F1F8E9
mutedForeground: '122 20% 40%',
border: '120 25% 91%', // #E8F5E9
borderStrong: '120 26% 79%', // #C8E6C9
error: '0 65% 67%', // #E57373
success: '122 39% 49%', // Same as primary
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '122 39% 49%',
};
const natureDark: ThemeColors = {
primary: '122 39% 49%', // #4CAF50
primaryForeground: '0 0% 100%',
secondary: '122 30% 35%', // Muted green
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
foreground: '0 0% 100%', // White
surface: '120 10% 12%', // Slight green tint
surfaceHover: '120 10% 16%',
surfaceElevated: '120 10% 14%',
muted: '120 10% 20%',
mutedForeground: '120 10% 60%',
border: '120 10% 25%',
borderStrong: '120 10% 35%',
error: '0 65% 57%',
success: '122 50% 55%',
warning: '48 100% 50%',
input: '120 10% 14%',
ring: '122 39% 49%',
primary: '122 39% 49%', // #4CAF50
primaryForeground: '0 0% 100%',
secondary: '122 30% 35%', // Muted green
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
foreground: '0 0% 100%', // White
surface: '120 10% 12%', // Slight green tint
surfaceHover: '120 10% 16%',
surfaceElevated: '120 10% 14%',
muted: '120 10% 20%',
mutedForeground: '120 10% 60%',
border: '120 10% 25%',
borderStrong: '120 10% 35%',
error: '0 65% 57%',
success: '122 50% 55%',
warning: '48 100% 50%',
input: '120 10% 14%',
ring: '122 39% 49%',
};
const stoneLight: ThemeColors = {
primary: '200 18% 46%', // #607D8B - Blue gray
primaryForeground: '0 0% 100%',
secondary: '200 15% 62%', // #90A4AE - Light slate
secondaryForeground: '0 0% 0%',
background: '210 17% 97%', // #F5F7F9 - Very light blue gray
foreground: '200 19% 18%', // #263238 - Dark slate
surface: '0 0% 100%',
surfaceHover: '200 10% 94%', // #ECEFF1
surfaceElevated: '0 0% 100%',
muted: '200 10% 94%', // #ECEFF1
mutedForeground: '200 10% 45%',
border: '200 10% 88%', // #CFD8DC
borderStrong: '200 12% 75%', // #B0BEC5
error: '4 90% 63%', // #EF5350
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '200 18% 46%',
primary: '200 18% 46%', // #607D8B - Blue gray
primaryForeground: '0 0% 100%',
secondary: '200 15% 62%', // #90A4AE - Light slate
secondaryForeground: '0 0% 0%',
background: '210 17% 97%', // #F5F7F9 - Very light blue gray
foreground: '200 19% 18%', // #263238 - Dark slate
surface: '0 0% 100%',
surfaceHover: '200 10% 94%', // #ECEFF1
surfaceElevated: '0 0% 100%',
muted: '200 10% 94%', // #ECEFF1
mutedForeground: '200 10% 45%',
border: '200 10% 88%', // #CFD8DC
borderStrong: '200 12% 75%', // #B0BEC5
error: '4 90% 63%', // #EF5350
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '200 18% 46%',
};
const stoneDark: ThemeColors = {
primary: '200 15% 52%', // #78909C - Lighter in dark mode
primaryForeground: '0 0% 0%',
secondary: '200 12% 35%',
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
foreground: '0 0% 100%',
surface: '200 10% 12%',
surfaceHover: '200 10% 16%',
surfaceElevated: '200 10% 14%',
muted: '200 10% 20%',
mutedForeground: '200 10% 60%',
border: '200 10% 25%',
borderStrong: '200 10% 35%',
error: '4 90% 58%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '200 10% 14%',
ring: '200 15% 52%',
primary: '200 15% 52%', // #78909C - Lighter in dark mode
primaryForeground: '0 0% 0%',
secondary: '200 12% 35%',
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
foreground: '0 0% 100%',
surface: '200 10% 12%',
surfaceHover: '200 10% 16%',
surfaceElevated: '200 10% 14%',
muted: '200 10% 20%',
mutedForeground: '200 10% 60%',
border: '200 10% 25%',
borderStrong: '200 10% 35%',
error: '4 90% 58%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '200 10% 14%',
ring: '200 15% 52%',
};
const oceanLight: ThemeColors = {
primary: '199 98% 45%', // #039BE5 - Bright blue
primaryForeground: '0 0% 100%',
secondary: '199 92% 64%', // #4FC3F7 - Light blue
secondaryForeground: '0 0% 0%',
background: '199 100% 97%', // #F5FCFF - Very light blue
foreground: '199 100% 18%', // #01579B - Dark blue
surface: '0 0% 100%',
surfaceHover: '199 100% 94%', // #E1F5FE
surfaceElevated: '0 0% 100%',
muted: '199 100% 94%', // #E1F5FE
mutedForeground: '199 50% 40%',
border: '199 71% 87%', // #B3E5FC
borderStrong: '199 79% 76%', // #81D4FA
error: '4 90% 63%', // #EF5350
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '199 98% 45%',
primary: '199 98% 45%', // #039BE5 - Bright blue
primaryForeground: '0 0% 100%',
secondary: '199 92% 64%', // #4FC3F7 - Light blue
secondaryForeground: '0 0% 0%',
background: '199 100% 97%', // #F5FCFF - Very light blue
foreground: '199 100% 18%', // #01579B - Dark blue
surface: '0 0% 100%',
surfaceHover: '199 100% 94%', // #E1F5FE
surfaceElevated: '0 0% 100%',
muted: '199 100% 94%', // #E1F5FE
mutedForeground: '199 50% 40%',
border: '199 71% 87%', // #B3E5FC
borderStrong: '199 79% 76%', // #81D4FA
error: '4 90% 63%', // #EF5350
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '199 98% 45%',
};
const oceanDark: ThemeColors = {
primary: '199 98% 48%', // Slightly brighter in dark
primaryForeground: '0 0% 0%',
secondary: '199 60% 35%',
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
foreground: '0 0% 100%',
surface: '199 30% 12%', // Slight blue tint
surfaceHover: '199 30% 16%',
surfaceElevated: '199 30% 14%',
muted: '199 20% 20%',
mutedForeground: '199 20% 60%',
border: '199 20% 25%',
borderStrong: '199 20% 35%',
error: '4 90% 58%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '199 30% 14%',
ring: '199 98% 48%',
primary: '199 98% 48%', // Slightly brighter in dark
primaryForeground: '0 0% 0%',
secondary: '199 60% 35%',
secondaryForeground: '0 0% 100%',
background: '0 0% 7%', // #121212
foreground: '0 0% 100%',
surface: '199 30% 12%', // Slight blue tint
surfaceHover: '199 30% 16%',
surfaceElevated: '199 30% 14%',
muted: '199 20% 20%',
mutedForeground: '199 20% 60%',
border: '199 20% 25%',
borderStrong: '199 20% 35%',
error: '4 90% 58%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '199 30% 14%',
ring: '199 98% 48%',
};
/**
* Complete theme variant definitions
*/
export const THEME_DEFINITIONS: Record<ThemeVariant, ThemeVariantDefinition> = {
lume: {
name: 'lume',
label: 'Lume',
emoji: '✨',
hue: 47,
light: lumeLight,
dark: lumeDark,
},
nature: {
name: 'nature',
label: 'Nature',
emoji: '🌿',
hue: 122,
light: natureLight,
dark: natureDark,
},
stone: {
name: 'stone',
label: 'Stone',
emoji: '🪨',
hue: 200,
light: stoneLight,
dark: stoneDark,
},
ocean: {
name: 'ocean',
label: 'Ocean',
emoji: '🌊',
hue: 199,
light: oceanLight,
dark: oceanDark,
},
lume: {
name: 'lume',
label: 'Lume',
emoji: '✨',
hue: 47,
light: lumeLight,
dark: lumeDark,
},
nature: {
name: 'nature',
label: 'Nature',
emoji: '🌿',
hue: 122,
light: natureLight,
dark: natureDark,
},
stone: {
name: 'stone',
label: 'Stone',
emoji: '🪨',
hue: 200,
light: stoneLight,
dark: stoneDark,
},
ocean: {
name: 'ocean',
label: 'Ocean',
emoji: '🌊',
hue: 199,
light: oceanLight,
dark: oceanDark,
},
};
/**

View file

@ -1,24 +1,24 @@
// Types
export type {
ThemeMode,
ThemeVariant,
EffectiveMode,
ThemeState,
ThemeColors,
ThemeVariantDefinition,
AppThemeConfig,
ThemeStore,
HSLValue,
ThemeMode,
ThemeVariant,
EffectiveMode,
ThemeState,
ThemeColors,
ThemeVariantDefinition,
AppThemeConfig,
ThemeStore,
HSLValue,
} from './types';
// Constants
export {
THEME_VARIANTS,
THEME_DEFINITIONS,
DEFAULT_MODE,
DEFAULT_VARIANT,
CSS_VAR_PREFIX,
STORAGE_KEY_SUFFIX,
THEME_VARIANTS,
THEME_DEFINITIONS,
DEFAULT_MODE,
DEFAULT_VARIANT,
CSS_VAR_PREFIX,
STORAGE_KEY_SUFFIX,
} from './constants';
// Store
@ -26,18 +26,18 @@ export { createThemeStore, APP_THEME_CONFIGS } from './store.svelte';
// Utils
export {
isBrowser,
getSystemPreference,
createSystemPreferenceListener,
getThemeColors,
colorsToCssVars,
applyThemeToDocument,
loadThemeFromStorage,
saveThemeToStorage,
parseHSL,
createHSL,
adjustLightness,
adjustSaturation,
getContrastColor,
generateThemeCSS,
isBrowser,
getSystemPreference,
createSystemPreferenceListener,
getThemeColors,
colorsToCssVars,
applyThemeToDocument,
loadThemeFromStorage,
saveThemeToStorage,
parseHSL,
createHSL,
adjustLightness,
adjustSaturation,
getContrastColor,
generateThemeCSS,
} from './utils';

View file

@ -1,24 +1,19 @@
import type {
ThemeMode,
ThemeVariant,
EffectiveMode,
ThemeStore,
AppThemeConfig,
HSLValue,
ThemeMode,
ThemeVariant,
EffectiveMode,
ThemeStore,
AppThemeConfig,
HSLValue,
} from './types';
import { THEME_VARIANTS, DEFAULT_MODE, DEFAULT_VARIANT, STORAGE_KEY_SUFFIX } from './constants';
import {
THEME_VARIANTS,
DEFAULT_MODE,
DEFAULT_VARIANT,
STORAGE_KEY_SUFFIX,
} from './constants';
import {
isBrowser,
getSystemPreference,
createSystemPreferenceListener,
applyThemeToDocument,
loadThemeFromStorage,
saveThemeToStorage,
isBrowser,
getSystemPreference,
createSystemPreferenceListener,
applyThemeToDocument,
loadThemeFromStorage,
saveThemeToStorage,
} from './utils';
/**
@ -42,186 +37,186 @@ import {
* ```
*/
export function createThemeStore(config: AppThemeConfig): ThemeStore {
const {
appId,
defaultMode = DEFAULT_MODE,
defaultVariant = DEFAULT_VARIANT,
primaryColor,
} = config;
const {
appId,
defaultMode = DEFAULT_MODE,
defaultVariant = DEFAULT_VARIANT,
primaryColor,
} = config;
const storageKey = `${appId}${STORAGE_KEY_SUFFIX}`;
const storageKey = `${appId}${STORAGE_KEY_SUFFIX}`;
// Svelte 5 runes state
let mode = $state<ThemeMode>(defaultMode);
let variant = $state<ThemeVariant>(defaultVariant);
let effectiveMode = $state<EffectiveMode>('light');
// Svelte 5 runes state
let mode = $state<ThemeMode>(defaultMode);
let variant = $state<ThemeVariant>(defaultVariant);
let effectiveMode = $state<EffectiveMode>('light');
// Derived state
const isDark = $derived(effectiveMode === 'dark');
// Derived state
const isDark = $derived(effectiveMode === 'dark');
/**
* Calculate effective mode from current mode and system preference
*/
function calculateEffectiveMode(currentMode: ThemeMode): EffectiveMode {
if (currentMode === 'system') {
return getSystemPreference();
}
return currentMode;
}
/**
* Calculate effective mode from current mode and system preference
*/
function calculateEffectiveMode(currentMode: ThemeMode): EffectiveMode {
if (currentMode === 'system') {
return getSystemPreference();
}
return currentMode;
}
/**
* Apply current theme to document and save to storage
*/
function applyTheme(): void {
const newEffectiveMode = calculateEffectiveMode(mode);
effectiveMode = newEffectiveMode;
/**
* Apply current theme to document and save to storage
*/
function applyTheme(): void {
const newEffectiveMode = calculateEffectiveMode(mode);
effectiveMode = newEffectiveMode;
applyThemeToDocument(variant, newEffectiveMode, primaryColor);
saveThemeToStorage(storageKey, mode, variant);
}
applyThemeToDocument(variant, newEffectiveMode, primaryColor);
saveThemeToStorage(storageKey, mode, variant);
}
/**
* Set theme mode
*/
function setMode(newMode: ThemeMode): void {
if (newMode === mode) return;
mode = newMode;
applyTheme();
}
/**
* Set theme mode
*/
function setMode(newMode: ThemeMode): void {
if (newMode === mode) return;
mode = newMode;
applyTheme();
}
/**
* Set theme variant
*/
function setVariant(newVariant: ThemeVariant): void {
if (!THEME_VARIANTS.includes(newVariant)) {
console.warn(`Invalid theme variant: ${newVariant}`);
return;
}
if (newVariant === variant) return;
variant = newVariant;
applyTheme();
}
/**
* Set theme variant
*/
function setVariant(newVariant: ThemeVariant): void {
if (!THEME_VARIANTS.includes(newVariant)) {
console.warn(`Invalid theme variant: ${newVariant}`);
return;
}
if (newVariant === variant) return;
variant = newVariant;
applyTheme();
}
/**
* Toggle between light and dark mode
* If currently on system, switches to opposite of effective mode
*/
function toggleMode(): void {
if (mode === 'system') {
// Switch to opposite of current effective mode
setMode(effectiveMode === 'dark' ? 'light' : 'dark');
} else {
setMode(mode === 'dark' ? 'light' : 'dark');
}
}
/**
* Toggle between light and dark mode
* If currently on system, switches to opposite of effective mode
*/
function toggleMode(): void {
if (mode === 'system') {
// Switch to opposite of current effective mode
setMode(effectiveMode === 'dark' ? 'light' : 'dark');
} else {
setMode(mode === 'dark' ? 'light' : 'dark');
}
}
/**
* Cycle through modes: light dark system light
*/
function cycleMode(): void {
const modes: ThemeMode[] = ['light', 'dark', 'system'];
const currentIndex = modes.indexOf(mode);
const nextIndex = (currentIndex + 1) % modes.length;
setMode(modes[nextIndex]);
}
/**
* Cycle through modes: light dark system light
*/
function cycleMode(): void {
const modes: ThemeMode[] = ['light', 'dark', 'system'];
const currentIndex = modes.indexOf(mode);
const nextIndex = (currentIndex + 1) % modes.length;
setMode(modes[nextIndex]);
}
/**
* Initialize theme store
* - Loads saved preferences from localStorage
* - Sets up system preference listener
* - Applies initial theme
*
* @returns Cleanup function to remove listeners
*/
function initialize(): () => void {
if (!isBrowser()) {
return () => {};
}
/**
* Initialize theme store
* - Loads saved preferences from localStorage
* - Sets up system preference listener
* - Applies initial theme
*
* @returns Cleanup function to remove listeners
*/
function initialize(): () => void {
if (!isBrowser()) {
return () => {};
}
// Load saved preferences
const saved = loadThemeFromStorage(storageKey);
if (saved) {
if (saved.mode && ['light', 'dark', 'system'].includes(saved.mode)) {
mode = saved.mode as ThemeMode;
}
if (saved.variant && THEME_VARIANTS.includes(saved.variant as ThemeVariant)) {
variant = saved.variant as ThemeVariant;
}
}
// Load saved preferences
const saved = loadThemeFromStorage(storageKey);
if (saved) {
if (saved.mode && ['light', 'dark', 'system'].includes(saved.mode)) {
mode = saved.mode as ThemeMode;
}
if (saved.variant && THEME_VARIANTS.includes(saved.variant as ThemeVariant)) {
variant = saved.variant as ThemeVariant;
}
}
// Apply initial theme
applyTheme();
// Apply initial theme
applyTheme();
// Listen for system preference changes
const cleanup = createSystemPreferenceListener((isDark) => {
if (mode === 'system') {
effectiveMode = isDark ? 'dark' : 'light';
applyThemeToDocument(variant, effectiveMode, primaryColor);
}
});
// Listen for system preference changes
const cleanup = createSystemPreferenceListener((isDark) => {
if (mode === 'system') {
effectiveMode = isDark ? 'dark' : 'light';
applyThemeToDocument(variant, effectiveMode, primaryColor);
}
});
return cleanup;
}
return cleanup;
}
return {
get mode() {
return mode;
},
get variant() {
return variant;
},
get effectiveMode() {
return effectiveMode;
},
get isDark() {
return isDark;
},
get variants() {
return THEME_VARIANTS;
},
return {
get mode() {
return mode;
},
get variant() {
return variant;
},
get effectiveMode() {
return effectiveMode;
},
get isDark() {
return isDark;
},
get variants() {
return THEME_VARIANTS;
},
setMode,
setVariant,
toggleMode,
cycleMode,
initialize,
};
setMode,
setVariant,
toggleMode,
cycleMode,
initialize,
};
}
/**
* Pre-defined app configurations for convenience
*/
export const APP_THEME_CONFIGS = {
memoro: {
appId: 'memoro',
defaultVariant: 'lume' as ThemeVariant,
primaryColor: {
light: '47 95% 58%' as HSLValue, // Gold #f8d62b
dark: '47 95% 58%' as HSLValue,
},
},
manacore: {
appId: 'manacore',
defaultVariant: 'ocean' as ThemeVariant,
primaryColor: {
light: '239 84% 67%' as HSLValue, // Indigo #6366f1
dark: '239 84% 67%' as HSLValue,
},
},
manadeck: {
appId: 'manadeck',
defaultVariant: 'ocean' as ThemeVariant,
primaryColor: {
light: '239 84% 67%' as HSLValue, // Indigo #6366f1
dark: '239 84% 67%' as HSLValue,
},
},
maerchenzauber: {
appId: 'maerchenzauber',
defaultVariant: 'nature' as ThemeVariant,
primaryColor: {
light: '280 60% 55%' as HSLValue, // Purple (storytelling magic)
dark: '280 60% 60%' as HSLValue,
},
},
memoro: {
appId: 'memoro',
defaultVariant: 'lume' as ThemeVariant,
primaryColor: {
light: '47 95% 58%' as HSLValue, // Gold #f8d62b
dark: '47 95% 58%' as HSLValue,
},
},
manacore: {
appId: 'manacore',
defaultVariant: 'ocean' as ThemeVariant,
primaryColor: {
light: '239 84% 67%' as HSLValue, // Indigo #6366f1
dark: '239 84% 67%' as HSLValue,
},
},
manadeck: {
appId: 'manadeck',
defaultVariant: 'ocean' as ThemeVariant,
primaryColor: {
light: '239 84% 67%' as HSLValue, // Indigo #6366f1
dark: '239 84% 67%' as HSLValue,
},
},
maerchenzauber: {
appId: 'maerchenzauber',
defaultVariant: 'nature' as ThemeVariant,
primaryColor: {
light: '280 60% 55%' as HSLValue, // Purple (storytelling magic)
dark: '280 60% 60%' as HSLValue,
},
},
} as const;

View file

@ -18,9 +18,9 @@ export type EffectiveMode = 'light' | 'dark';
* Complete theme state
*/
export interface ThemeState {
mode: ThemeMode;
variant: ThemeVariant;
effectiveMode: EffectiveMode;
mode: ThemeMode;
variant: ThemeVariant;
effectiveMode: EffectiveMode;
}
/**
@ -33,101 +33,101 @@ export type HSLValue = string;
* Theme color definition using HSL values
*/
export interface ThemeColors {
/** Primary brand color */
primary: HSLValue;
/** Primary color for text on primary background */
primaryForeground: HSLValue;
/** Secondary accent color */
secondary: HSLValue;
/** Secondary foreground */
secondaryForeground: HSLValue;
/** Page background */
background: HSLValue;
/** Main text color */
foreground: HSLValue;
/** Card/content surface */
surface: HSLValue;
/** Surface hover state */
surfaceHover: HSLValue;
/** Elevated surface (modals, dropdowns) */
surfaceElevated: HSLValue;
/** Muted/disabled elements */
muted: HSLValue;
/** Muted text */
mutedForeground: HSLValue;
/** Border color */
border: HSLValue;
/** Strong border (focus, active) */
borderStrong: HSLValue;
/** Error/destructive color */
error: HSLValue;
/** Success color */
success: HSLValue;
/** Warning color */
warning: HSLValue;
/** Input background */
input: HSLValue;
/** Focus ring color */
ring: HSLValue;
/** Primary brand color */
primary: HSLValue;
/** Primary color for text on primary background */
primaryForeground: HSLValue;
/** Secondary accent color */
secondary: HSLValue;
/** Secondary foreground */
secondaryForeground: HSLValue;
/** Page background */
background: HSLValue;
/** Main text color */
foreground: HSLValue;
/** Card/content surface */
surface: HSLValue;
/** Surface hover state */
surfaceHover: HSLValue;
/** Elevated surface (modals, dropdowns) */
surfaceElevated: HSLValue;
/** Muted/disabled elements */
muted: HSLValue;
/** Muted text */
mutedForeground: HSLValue;
/** Border color */
border: HSLValue;
/** Strong border (focus, active) */
borderStrong: HSLValue;
/** Error/destructive color */
error: HSLValue;
/** Success color */
success: HSLValue;
/** Warning color */
warning: HSLValue;
/** Input background */
input: HSLValue;
/** Focus ring color */
ring: HSLValue;
}
/**
* Theme variant definition with light and dark mode colors
*/
export interface ThemeVariantDefinition {
name: string;
label: string;
emoji: string;
/** The primary hue for this variant (used for accent colors) */
hue: number;
light: ThemeColors;
dark: ThemeColors;
name: string;
label: string;
emoji: string;
/** The primary hue for this variant (used for accent colors) */
hue: number;
light: ThemeColors;
dark: ThemeColors;
}
/**
* App-specific theme configuration
*/
export interface AppThemeConfig {
/** App identifier for localStorage key */
appId: string;
/** Default theme mode */
defaultMode?: ThemeMode;
/** Default theme variant */
defaultVariant?: ThemeVariant;
/**
* App-specific primary color override (HSL value)
* This allows each app to have its own brand color
* while sharing the same theme variants
*/
primaryColor?: {
light: HSLValue;
dark: HSLValue;
};
/** App identifier for localStorage key */
appId: string;
/** Default theme mode */
defaultMode?: ThemeMode;
/** Default theme variant */
defaultVariant?: ThemeVariant;
/**
* App-specific primary color override (HSL value)
* This allows each app to have its own brand color
* while sharing the same theme variants
*/
primaryColor?: {
light: HSLValue;
dark: HSLValue;
};
}
/**
* Theme store interface
*/
export interface ThemeStore {
/** Current theme mode (user preference) */
readonly mode: ThemeMode;
/** Current theme variant */
readonly variant: ThemeVariant;
/** Effective mode (resolved from system) */
readonly effectiveMode: EffectiveMode;
/** Whether dark mode is active */
readonly isDark: boolean;
/** All available variants */
readonly variants: readonly ThemeVariant[];
/** Current theme mode (user preference) */
readonly mode: ThemeMode;
/** Current theme variant */
readonly variant: ThemeVariant;
/** Effective mode (resolved from system) */
readonly effectiveMode: EffectiveMode;
/** Whether dark mode is active */
readonly isDark: boolean;
/** All available variants */
readonly variants: readonly ThemeVariant[];
/** Set theme mode */
setMode: (mode: ThemeMode) => void;
/** Set theme variant */
setVariant: (variant: ThemeVariant) => void;
/** Toggle between light and dark mode */
toggleMode: () => void;
/** Cycle through modes: light → dark → system → light */
cycleMode: () => void;
/** Initialize theme (call on mount) */
initialize: () => () => void;
/** Set theme mode */
setMode: (mode: ThemeMode) => void;
/** Set theme variant */
setVariant: (variant: ThemeVariant) => void;
/** Toggle between light and dark mode */
toggleMode: () => void;
/** Cycle through modes: light → dark → system → light */
cycleMode: () => void;
/** Initialize theme (call on mount) */
initialize: () => () => void;
}

View file

@ -5,151 +5,149 @@ import { THEME_DEFINITIONS, CSS_VAR_PREFIX } from './constants';
* Check if code is running in browser
*/
export function isBrowser(): boolean {
return typeof window !== 'undefined' && typeof document !== 'undefined';
return typeof window !== 'undefined' && typeof document !== 'undefined';
}
/**
* Get system color scheme preference
*/
export function getSystemPreference(): EffectiveMode {
if (!isBrowser()) return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
if (!isBrowser()) return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
/**
* Create a media query listener for system preference changes
*/
export function createSystemPreferenceListener(callback: (isDark: boolean) => void): () => void {
if (!isBrowser()) return () => {};
if (!isBrowser()) return () => {};
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => callback(e.matches);
const handler = (e: MediaQueryListEvent) => callback(e.matches);
// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}
// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}
// Legacy browsers
mediaQuery.addListener(handler);
return () => mediaQuery.removeListener(handler);
// Legacy browsers
mediaQuery.addListener(handler);
return () => mediaQuery.removeListener(handler);
}
/**
* Get colors for a specific variant and mode
*/
export function getThemeColors(
variant: ThemeVariant,
mode: EffectiveMode,
primaryOverride?: { light: HSLValue; dark: HSLValue }
variant: ThemeVariant,
mode: EffectiveMode,
primaryOverride?: { light: HSLValue; dark: HSLValue }
): ThemeColors {
const definition = THEME_DEFINITIONS[variant];
const colors = mode === 'dark' ? { ...definition.dark } : { ...definition.light };
const definition = THEME_DEFINITIONS[variant];
const colors = mode === 'dark' ? { ...definition.dark } : { ...definition.light };
// Apply app-specific primary color override
if (primaryOverride) {
const overrideColor = mode === 'dark' ? primaryOverride.dark : primaryOverride.light;
colors.primary = overrideColor;
colors.ring = overrideColor;
}
// Apply app-specific primary color override
if (primaryOverride) {
const overrideColor = mode === 'dark' ? primaryOverride.dark : primaryOverride.light;
colors.primary = overrideColor;
colors.ring = overrideColor;
}
return colors;
return colors;
}
/**
* Convert ThemeColors to CSS variables object
*/
export function colorsToCssVars(colors: ThemeColors): Record<string, string> {
return {
[`${CSS_VAR_PREFIX}-primary`]: colors.primary,
[`${CSS_VAR_PREFIX}-primary-foreground`]: colors.primaryForeground,
[`${CSS_VAR_PREFIX}-secondary`]: colors.secondary,
[`${CSS_VAR_PREFIX}-secondary-foreground`]: colors.secondaryForeground,
[`${CSS_VAR_PREFIX}-background`]: colors.background,
[`${CSS_VAR_PREFIX}-foreground`]: colors.foreground,
[`${CSS_VAR_PREFIX}-surface`]: colors.surface,
[`${CSS_VAR_PREFIX}-surface-hover`]: colors.surfaceHover,
[`${CSS_VAR_PREFIX}-surface-elevated`]: colors.surfaceElevated,
[`${CSS_VAR_PREFIX}-muted`]: colors.muted,
[`${CSS_VAR_PREFIX}-muted-foreground`]: colors.mutedForeground,
[`${CSS_VAR_PREFIX}-border`]: colors.border,
[`${CSS_VAR_PREFIX}-border-strong`]: colors.borderStrong,
[`${CSS_VAR_PREFIX}-error`]: colors.error,
[`${CSS_VAR_PREFIX}-success`]: colors.success,
[`${CSS_VAR_PREFIX}-warning`]: colors.warning,
[`${CSS_VAR_PREFIX}-input`]: colors.input,
[`${CSS_VAR_PREFIX}-ring`]: colors.ring,
};
return {
[`${CSS_VAR_PREFIX}-primary`]: colors.primary,
[`${CSS_VAR_PREFIX}-primary-foreground`]: colors.primaryForeground,
[`${CSS_VAR_PREFIX}-secondary`]: colors.secondary,
[`${CSS_VAR_PREFIX}-secondary-foreground`]: colors.secondaryForeground,
[`${CSS_VAR_PREFIX}-background`]: colors.background,
[`${CSS_VAR_PREFIX}-foreground`]: colors.foreground,
[`${CSS_VAR_PREFIX}-surface`]: colors.surface,
[`${CSS_VAR_PREFIX}-surface-hover`]: colors.surfaceHover,
[`${CSS_VAR_PREFIX}-surface-elevated`]: colors.surfaceElevated,
[`${CSS_VAR_PREFIX}-muted`]: colors.muted,
[`${CSS_VAR_PREFIX}-muted-foreground`]: colors.mutedForeground,
[`${CSS_VAR_PREFIX}-border`]: colors.border,
[`${CSS_VAR_PREFIX}-border-strong`]: colors.borderStrong,
[`${CSS_VAR_PREFIX}-error`]: colors.error,
[`${CSS_VAR_PREFIX}-success`]: colors.success,
[`${CSS_VAR_PREFIX}-warning`]: colors.warning,
[`${CSS_VAR_PREFIX}-input`]: colors.input,
[`${CSS_VAR_PREFIX}-ring`]: colors.ring,
};
}
/**
* Apply theme to document
*/
export function applyThemeToDocument(
variant: ThemeVariant,
effectiveMode: EffectiveMode,
primaryOverride?: { light: HSLValue; dark: HSLValue }
variant: ThemeVariant,
effectiveMode: EffectiveMode,
primaryOverride?: { light: HSLValue; dark: HSLValue }
): void {
if (!isBrowser()) return;
if (!isBrowser()) return;
const root = document.documentElement;
const colors = getThemeColors(variant, effectiveMode, primaryOverride);
const cssVars = colorsToCssVars(colors);
const root = document.documentElement;
const colors = getThemeColors(variant, effectiveMode, primaryOverride);
const cssVars = colorsToCssVars(colors);
// Set CSS variables
Object.entries(cssVars).forEach(([key, value]) => {
root.style.setProperty(key, value);
});
// Set CSS variables
Object.entries(cssVars).forEach(([key, value]) => {
root.style.setProperty(key, value);
});
// Set data-theme attribute
root.setAttribute('data-theme', variant);
// Set data-theme attribute
root.setAttribute('data-theme', variant);
// Set dark class
if (effectiveMode === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
// Set dark class
if (effectiveMode === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
// Update color-scheme for native elements
root.style.colorScheme = effectiveMode;
// Update color-scheme for native elements
root.style.colorScheme = effectiveMode;
}
/**
* Load theme from localStorage
*/
export function loadThemeFromStorage(storageKey: string): { mode?: string; variant?: string } | null {
if (!isBrowser()) return null;
export function loadThemeFromStorage(
storageKey: string
): { mode?: string; variant?: string } | null {
if (!isBrowser()) return null;
try {
const stored = localStorage.getItem(storageKey);
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.warn('Failed to load theme from storage:', e);
}
try {
const stored = localStorage.getItem(storageKey);
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.warn('Failed to load theme from storage:', e);
}
return null;
return null;
}
/**
* Save theme to localStorage
*/
export function saveThemeToStorage(
storageKey: string,
mode: string,
variant: string
): void {
if (!isBrowser()) return;
export function saveThemeToStorage(storageKey: string, mode: string, variant: string): void {
if (!isBrowser()) return;
try {
localStorage.setItem(storageKey, JSON.stringify({ mode, variant }));
} catch (e) {
console.warn('Failed to save theme to storage:', e);
}
try {
localStorage.setItem(storageKey, JSON.stringify({ mode, variant }));
} catch (e) {
console.warn('Failed to save theme to storage:', e);
}
}
/**
@ -157,46 +155,46 @@ export function saveThemeToStorage(
* Input: "47 95% 58%" -> { h: 47, s: 95, l: 58 }
*/
export function parseHSL(hsl: HSLValue): { h: number; s: number; l: number } {
const parts = hsl.split(' ');
return {
h: parseFloat(parts[0]),
s: parseFloat(parts[1]),
l: parseFloat(parts[2]),
};
const parts = hsl.split(' ');
return {
h: parseFloat(parts[0]),
s: parseFloat(parts[1]),
l: parseFloat(parts[2]),
};
}
/**
* Create HSL string from components
*/
export function createHSL(h: number, s: number, l: number): HSLValue {
return `${h} ${s}% ${l}%`;
return `${h} ${s}% ${l}%`;
}
/**
* Adjust lightness of an HSL color
*/
export function adjustLightness(hsl: HSLValue, amount: number): HSLValue {
const { h, s, l } = parseHSL(hsl);
const newL = Math.max(0, Math.min(100, l + amount));
return createHSL(h, s, newL);
const { h, s, l } = parseHSL(hsl);
const newL = Math.max(0, Math.min(100, l + amount));
return createHSL(h, s, newL);
}
/**
* Adjust saturation of an HSL color
*/
export function adjustSaturation(hsl: HSLValue, amount: number): HSLValue {
const { h, s, l } = parseHSL(hsl);
const newS = Math.max(0, Math.min(100, s + amount));
return createHSL(h, newS, l);
const { h, s, l } = parseHSL(hsl);
const newS = Math.max(0, Math.min(100, s + amount));
return createHSL(h, newS, l);
}
/**
* Get contrasting text color (black or white) for a background
*/
export function getContrastColor(backgroundHSL: HSLValue): HSLValue {
const { l } = parseHSL(backgroundHSL);
// Use white text for dark backgrounds, black for light
return l > 55 ? '0 0% 0%' : '0 0% 100%';
const { l } = parseHSL(backgroundHSL);
// Use white text for dark backgrounds, black for light
return l > 55 ? '0 0% 0%' : '0 0% 100%';
}
/**
@ -204,42 +202,42 @@ export function getContrastColor(backgroundHSL: HSLValue): HSLValue {
* Useful for generating static CSS files
*/
export function generateThemeCSS(
primaryOverrides?: Record<string, { light: HSLValue; dark: HSLValue }>
primaryOverrides?: Record<string, { light: HSLValue; dark: HSLValue }>
): string {
let css = '';
let css = '';
// Root (default Lume light)
const defaultColors = getThemeColors('lume', 'light', primaryOverrides?.['lume']);
const defaultVars = colorsToCssVars(defaultColors);
css += ':root {\n';
Object.entries(defaultVars).forEach(([key, value]) => {
css += ` ${key}: ${value};\n`;
});
css += '}\n\n';
// Root (default Lume light)
const defaultColors = getThemeColors('lume', 'light', primaryOverrides?.['lume']);
const defaultVars = colorsToCssVars(defaultColors);
css += ':root {\n';
Object.entries(defaultVars).forEach(([key, value]) => {
css += ` ${key}: ${value};\n`;
});
css += '}\n\n';
// Each variant
for (const [variantName, definition] of Object.entries(THEME_DEFINITIONS)) {
const variant = variantName as ThemeVariant;
const override = primaryOverrides?.[variant];
// Each variant
for (const [variantName, definition] of Object.entries(THEME_DEFINITIONS)) {
const variant = variantName as ThemeVariant;
const override = primaryOverrides?.[variant];
// Light mode
const lightColors = getThemeColors(variant, 'light', override);
const lightVars = colorsToCssVars(lightColors);
css += `[data-theme="${variant}"] {\n`;
Object.entries(lightVars).forEach(([key, value]) => {
css += ` ${key}: ${value};\n`;
});
css += '}\n\n';
// Light mode
const lightColors = getThemeColors(variant, 'light', override);
const lightVars = colorsToCssVars(lightColors);
css += `[data-theme="${variant}"] {\n`;
Object.entries(lightVars).forEach(([key, value]) => {
css += ` ${key}: ${value};\n`;
});
css += '}\n\n';
// Dark mode
const darkColors = getThemeColors(variant, 'dark', override);
const darkVars = colorsToCssVars(darkColors);
css += `[data-theme="${variant}"].dark,\n.dark[data-theme="${variant}"] {\n`;
Object.entries(darkVars).forEach(([key, value]) => {
css += ` ${key}: ${value};\n`;
});
css += '}\n\n';
}
// Dark mode
const darkColors = getThemeColors(variant, 'dark', override);
const darkVars = colorsToCssVars(darkColors);
css += `[data-theme="${variant}"].dark,\n.dark[data-theme="${variant}"] {\n`;
Object.entries(darkVars).forEach(([key, value]) => {
css += ` ${key}: ${value};\n`;
});
css += '}\n\n';
}
return css;
return css;
}