feat: implement unified theme system across all web apps

SUMMARY:
Create a unified theming architecture with two new shared packages
(@manacore/shared-theme and @manacore/shared-theme-ui) that provides
consistent theming across all 4 web applications while allowing
app-specific primary color customization.

NEW PACKAGES:

@manacore/shared-theme:
- Svelte 5 Runes-based theme store factory
- 4 theme variants: Lume (Gold), Nature (Green), Stone (Blue Gray), Ocean (Blue)
- 3 theme modes: Light, Dark, System (auto-detect)
- HSL-based color system with 18 semantic tokens
- localStorage persistence per app
- System preference detection via matchMedia
- Pre-defined configs for all apps (memoro, manacore, manadeck, maerchenzauber)

@manacore/shared-theme-ui:
- ThemeToggle: Light/dark mode toggle button
- ThemeSelector: Visual theme variant selector with color dots
- ThemeModeSelector: Segmented control for light/dark/system

UPDATED PACKAGES:

@manacore/shared-tailwind:
- Converted from HEX to HSL-based CSS variables
- Updated preset.js with hsl(var(--color-*)) syntax
- themes.css now contains all 4 theme variants with light/dark modes

APP MIGRATIONS:

memoro/web:
- Replaced 145 LOC theme store with 25 LOC shared implementation
- Deleted local ThemeSelector.svelte and ThemeToggle.svelte
- Primary color: Gold (47 95% 58%)

manacore/web:
- Replaced 80 LOC theme store with 25 LOC shared implementation
- Deleted local ThemeToggle.svelte
- Primary color: Indigo (239 84% 67%)

manadeck/web:
- Added new theme store using shared package
- Primary color: Indigo (239 84% 67%)

maerchenzauber/web:
- Added new theme store using shared package
- Primary color: Purple (280 60% 55%)

All app layouts updated with theme.initialize() in onMount.

BENEFITS:
- ~225 LOC of app-specific code reduced to ~100 LOC total
- Single source of truth for theme logic
- Consistent theming experience across all apps
- Easy to add new theme variants
- App-specific primary colors preserved

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-24 21:51:24 +01:00
parent ef70a1af0b
commit 96e0aceb93
31 changed files with 2993 additions and 1089 deletions

View file

@ -1,40 +0,0 @@
<script lang="ts">
import { theme } from '$lib/stores/theme';
let currentTheme = $derived($theme);
function toggleTheme() {
theme.toggleMode();
}
</script>
<button
onclick={toggleTheme}
class="rounded-lg p-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Toggle theme"
title={currentTheme.effectiveMode === 'light'
? 'Switch to dark mode'
: 'Switch to light mode'}
>
{#if currentTheme.effectiveMode === 'light'}
<!-- Moon Icon (Dark Mode) -->
<svg class="h-5 w-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{:else}
<!-- Sun Icon (Light Mode) -->
<svg class="h-5 w-5 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{/if}
</button>

View file

@ -1,79 +1,25 @@
import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';
/**
* ManaCore Theme Store
*
* Uses the shared theme system with ManaCore's indigo primary color.
*/
import { createThemeStore } from '@manacore/shared-theme';
type ThemeMode = 'light' | 'dark' | 'system';
// Re-export types for convenience
export type { ThemeMode, ThemeVariant, EffectiveMode } from '@manacore/shared-theme';
interface ThemeState {
mode: ThemeMode;
effectiveMode: 'light' | 'dark';
}
function createThemeStore() {
const getInitialMode = (): ThemeMode => {
if (browser) {
const stored = localStorage.getItem('theme-mode');
if (stored === 'light' || stored === 'dark' || stored === 'system') {
return stored;
}
}
return 'system';
};
const getSystemPreference = (): 'light' | 'dark' => {
if (browser && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
};
const mode = writable<ThemeMode>(getInitialMode());
const effectiveMode = derived(mode, ($mode) => {
if ($mode === 'system') {
return getSystemPreference();
}
return $mode;
});
const state = derived([mode, effectiveMode], ([$mode, $effectiveMode]) => ({
mode: $mode,
effectiveMode: $effectiveMode
}));
// Apply theme to document
if (browser) {
effectiveMode.subscribe((effective) => {
if (effective === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
});
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
mode.update((m) => m); // Trigger re-evaluation
});
}
return {
subscribe: state.subscribe,
setMode: (newMode: ThemeMode) => {
mode.set(newMode);
if (browser) {
localStorage.setItem('theme-mode', newMode);
}
},
toggleMode: () => {
mode.update((current) => {
const newMode = current === 'light' ? 'dark' : current === 'dark' ? 'system' : 'light';
if (browser) {
localStorage.setItem('theme-mode', newMode);
}
return newMode;
});
}
};
}
export const theme = createThemeStore();
/**
* ManaCore theme store instance
*
* - Default variant: ocean (blue)
* - Custom primary: Indigo (#6366f1)
* - All 4 theme variants available
*/
export const theme = createThemeStore({
appId: 'manacore',
defaultVariant: 'ocean',
primaryColor: {
light: '239 84% 67%', // Indigo #6366f1
dark: '239 84% 67%',
},
});

View file

@ -2,10 +2,15 @@
import '../app.css';
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
import { theme } from '$lib/stores/theme';
let { data, children } = $props();
onMount(() => {
// Initialize theme
const cleanupTheme = theme.initialize();
// Setup auth state change listener
const {
data: { subscription }
} = data.supabase.auth.onAuthStateChange(async (event, session) => {
@ -16,7 +21,10 @@
}
});
return () => subscription.unsubscribe();
return () => {
cleanupTheme();
subscription.unsubscribe();
};
});
</script>