managarten/packages/shared-theme/src/store.svelte.ts
Till-JS 54383bf7c2 feat(theme): add ThemePage components and distinct background colors
- Add unique background colors for each theme variant:
  - Lume: warm cream/gold tint
  - Nature: green tint in dark mode
  - Stone: blue-gray tint in dark mode
  - Ocean: blue tint in dark mode

- Create shared-theme-ui components:
  - ThemeColorPreview: color circles preview component
  - ThemeCard: individual theme card with status support
  - ThemeGrid: responsive grid layout
  - ThemePage: full page component with mode selector

- Integrate theme page in Chat app:
  - Add /themes route with ThemePage component
  - Add "🎨 Alle Themes" link to PillNavigation dropdown
  - Add palette icon to shared-ui icon set

- Migrate Presi and Picture apps to shared-theme system
- Update semantic color usage across all apps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 09:03:20 +01:00

230 lines
5.2 KiB
TypeScript

import type {
ThemeMode,
ThemeVariant,
EffectiveMode,
ThemeStore,
AppThemeConfig,
HSLValue,
} from './types';
import { THEME_VARIANTS, DEFAULT_MODE, DEFAULT_VARIANT, STORAGE_KEY_SUFFIX } from './constants';
import {
isBrowser,
getSystemPreference,
createSystemPreferenceListener,
applyThemeToDocument,
loadThemeFromStorage,
saveThemeToStorage,
} from './utils';
/**
* Create a theme store for your app
*
* @example
* ```typescript
* // Basic usage
* import { createThemeStore } from '@manacore/shared-theme';
*
* export const theme = createThemeStore({ appId: 'myapp' });
*
* // With custom primary color
* export const theme = createThemeStore({
* appId: 'memoro',
* primaryColor: {
* light: '47 95% 58%', // Gold
* dark: '47 95% 58%',
* },
* });
* ```
*/
export function createThemeStore(config: AppThemeConfig): ThemeStore {
const {
appId,
defaultMode = DEFAULT_MODE,
defaultVariant = DEFAULT_VARIANT,
primaryColor,
} = config;
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');
// 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;
}
/**
* 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);
}
/**
* 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();
}
/**
* 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]);
}
/**
* 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;
}
}
// Apply initial theme
applyTheme();
// Listen for system preference changes
const cleanup = createSystemPreferenceListener((isDark) => {
if (mode === 'system') {
effectiveMode = isDark ? 'dark' : 'light';
applyThemeToDocument(variant, effectiveMode, primaryColor);
}
});
return cleanup;
}
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,
};
}
/**
* 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,
},
},
picture: {
appId: 'picture',
defaultVariant: 'ocean' as ThemeVariant,
primaryColor: {
light: '217 91% 60%' as HSLValue, // Blue #3b82f6
dark: '217 91% 60%' as HSLValue,
},
},
} as const;