mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 03:59:40 +02:00
- 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>
230 lines
5.2 KiB
TypeScript
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;
|