mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 07:59:41 +02:00
✨ feat(matrix-web): add theme mode selector in settings
Replace placeholder appearance section with actual theme controls: - Light mode button with sun icon - Dark mode button with moon icon - System mode button (follows OS preference) - Shows current active mode status Uses shared-theme store for consistent theming across the app. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5777c76c47
commit
b097d89318
5 changed files with 143 additions and 150 deletions
|
|
@ -17,6 +17,9 @@
|
|||
BellRinging,
|
||||
SpeakerHigh,
|
||||
Eye,
|
||||
Sun,
|
||||
Moon,
|
||||
Desktop,
|
||||
} from '@manacore/shared-icons';
|
||||
import { VerificationDialog, RecoveryKeyDialog } from '$lib/components/crypto';
|
||||
import {
|
||||
|
|
@ -27,6 +30,7 @@
|
|||
isNotificationSupported,
|
||||
} from '$lib/notifications';
|
||||
import { browser } from '$app/environment';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
|
||||
let verificationDialogOpen = $state(false);
|
||||
let recoveryDialogOpen = $state(false);
|
||||
|
|
@ -208,14 +212,62 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Appearance Section (Placeholder) -->
|
||||
<!-- Appearance Section -->
|
||||
<section class="card">
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-4">
|
||||
<h2 class="flex items-center gap-2 text-lg font-semibold">
|
||||
<Palette class="h-5 w-5" />
|
||||
Erscheinungsbild
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground">Theme-Einstellungen folgen bald...</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-muted-foreground">Wähle dein bevorzugtes Farbschema</p>
|
||||
|
||||
<!-- Theme Mode Selection -->
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||
{theme.mode === 'light'
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('light')}
|
||||
>
|
||||
<Sun class="h-6 w-6" />
|
||||
<span class="text-sm font-medium">Hell</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||
{theme.mode === 'dark'
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('dark')}
|
||||
>
|
||||
<Moon class="h-6 w-6" />
|
||||
<span class="text-sm font-medium">Dunkel</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||
{theme.mode === 'system'
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('system')}
|
||||
>
|
||||
<Desktop class="h-6 w-6" />
|
||||
<span class="text-sm font-medium">System</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Current Status -->
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
{#if theme.mode === 'system'}
|
||||
<span>Aktuell: {theme.isDark ? 'Dunkel' : 'Hell'} (basierend auf System)</span>
|
||||
{:else}
|
||||
<span>Aktuell: {theme.isDark ? 'Dunkel' : 'Hell'}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,61 +1,47 @@
|
|||
import { browser } from '$app/environment';
|
||||
/**
|
||||
* Theme Store - Manages theme state
|
||||
* Uses shared theme store from @manacore/shared-theme
|
||||
*/
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
import { createThemeStore, type ThemeMode } from '@manacore/shared-theme';
|
||||
|
||||
function getInitialTheme(): Theme {
|
||||
if (!browser) return 'system';
|
||||
|
||||
const stored = localStorage.getItem('theme') as Theme | null;
|
||||
if (stored && ['light', 'dark', 'system'].includes(stored)) {
|
||||
return stored;
|
||||
}
|
||||
return 'system';
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
if (!browser) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDark = theme === 'dark' || (theme === 'system' && systemDark);
|
||||
|
||||
if (isDark) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
let currentTheme: Theme = 'system';
|
||||
const sharedTheme = createThemeStore({ appId: 'questions' });
|
||||
|
||||
// Wrapper to maintain backward-compatible API
|
||||
export const theme = {
|
||||
// Legacy API (current → mode)
|
||||
get current() {
|
||||
return currentTheme;
|
||||
return sharedTheme.mode;
|
||||
},
|
||||
|
||||
initialize() {
|
||||
currentTheme = getInitialTheme();
|
||||
applyTheme(currentTheme);
|
||||
|
||||
if (browser) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (currentTheme === 'system') {
|
||||
applyTheme('system');
|
||||
}
|
||||
});
|
||||
}
|
||||
// Forward all other getters
|
||||
get mode() {
|
||||
return sharedTheme.mode;
|
||||
},
|
||||
get isDark() {
|
||||
return sharedTheme.isDark;
|
||||
},
|
||||
get variant() {
|
||||
return sharedTheme.variant;
|
||||
},
|
||||
get variants() {
|
||||
return sharedTheme.variants;
|
||||
},
|
||||
|
||||
set(newTheme: Theme) {
|
||||
currentTheme = newTheme;
|
||||
if (browser) {
|
||||
localStorage.setItem('theme', newTheme);
|
||||
}
|
||||
applyTheme(newTheme);
|
||||
// Legacy API (set → setMode)
|
||||
set(newTheme: ThemeMode) {
|
||||
sharedTheme.setMode(newTheme);
|
||||
},
|
||||
|
||||
// Legacy API (toggle → toggleMode)
|
||||
toggle() {
|
||||
const next = currentTheme === 'light' ? 'dark' : 'light';
|
||||
this.set(next);
|
||||
sharedTheme.toggleMode();
|
||||
},
|
||||
|
||||
// Forward new API
|
||||
initialize: sharedTheme.initialize,
|
||||
setMode: sharedTheme.setMode,
|
||||
setVariant: sharedTheme.setVariant,
|
||||
toggleMode: sharedTheme.toggleMode,
|
||||
cycleMode: sharedTheme.cycleMode,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,95 +1,8 @@
|
|||
/**
|
||||
* Theme Store - Manages theme state
|
||||
* Uses shared theme store from @manacore/shared-theme
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import {
|
||||
THEME_DEFINITIONS,
|
||||
THEME_VARIANTS,
|
||||
type ThemeMode,
|
||||
type ThemeVariant,
|
||||
DEFAULT_VARIANT,
|
||||
} from '@manacore/shared-theme';
|
||||
import { createThemeStore } from '@manacore/shared-theme';
|
||||
|
||||
const STORAGE_KEY_MODE = 'storage-theme-mode';
|
||||
const STORAGE_KEY_VARIANT = 'storage-theme-variant';
|
||||
|
||||
function createThemeStore() {
|
||||
let mode = $state<ThemeMode>('system');
|
||||
let variant = $state<ThemeVariant>(DEFAULT_VARIANT);
|
||||
let systemPrefersDark = $state(false);
|
||||
|
||||
function getSystemPreference(): boolean {
|
||||
if (!browser) return false;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
function applyTheme() {
|
||||
if (!browser) return;
|
||||
|
||||
const isDarkMode = mode === 'dark' || (mode === 'system' && systemPrefersDark);
|
||||
const themeClass = isDarkMode
|
||||
? THEME_DEFINITIONS[variant].darkClass
|
||||
: THEME_DEFINITIONS[variant].lightClass;
|
||||
|
||||
document.documentElement.className = themeClass;
|
||||
}
|
||||
|
||||
return {
|
||||
get mode() {
|
||||
return mode;
|
||||
},
|
||||
get variant() {
|
||||
return variant;
|
||||
},
|
||||
get isDark() {
|
||||
return mode === 'dark' || (mode === 'system' && systemPrefersDark);
|
||||
},
|
||||
get variants() {
|
||||
return THEME_VARIANTS;
|
||||
},
|
||||
|
||||
initialize() {
|
||||
if (!browser) return;
|
||||
|
||||
const savedMode = localStorage.getItem(STORAGE_KEY_MODE) as ThemeMode | null;
|
||||
const savedVariant = localStorage.getItem(STORAGE_KEY_VARIANT) as ThemeVariant | null;
|
||||
|
||||
if (savedMode) mode = savedMode;
|
||||
if (savedVariant && savedVariant in THEME_DEFINITIONS) variant = savedVariant;
|
||||
|
||||
systemPrefersDark = getSystemPreference();
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', (e) => {
|
||||
systemPrefersDark = e.matches;
|
||||
applyTheme();
|
||||
});
|
||||
|
||||
applyTheme();
|
||||
},
|
||||
|
||||
setMode(newMode: ThemeMode) {
|
||||
mode = newMode;
|
||||
if (browser) {
|
||||
localStorage.setItem(STORAGE_KEY_MODE, newMode);
|
||||
}
|
||||
applyTheme();
|
||||
},
|
||||
|
||||
setVariant(newVariant: ThemeVariant) {
|
||||
variant = newVariant;
|
||||
if (browser) {
|
||||
localStorage.setItem(STORAGE_KEY_VARIANT, newVariant);
|
||||
}
|
||||
applyTheme();
|
||||
},
|
||||
|
||||
toggleMode() {
|
||||
const newMode = mode === 'dark' ? 'light' : 'dark';
|
||||
this.setMode(newMode);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const theme = createThemeStore();
|
||||
export const theme = createThemeStore({ appId: 'storage' });
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@
|
|||
>
|
||||
<div
|
||||
class="theme-preview"
|
||||
style="background: linear-gradient(135deg, {def.colors.primary}, {def.colors.accent})"
|
||||
style="background: linear-gradient(135deg, hsl({def.light.primary}), hsl({def.light
|
||||
.secondary}))"
|
||||
>
|
||||
{#if theme.variant === variant}
|
||||
<div class="check-badge">
|
||||
|
|
|
|||
|
|
@ -235,13 +235,30 @@ export const { isSidebarMode, isNavCollapsed } = createSimpleNavigationStores({
|
|||
|
||||
---
|
||||
|
||||
### 2.3 NIEDRIG: Theme Stores Migration
|
||||
### ~~2.3 NIEDRIG: Theme Stores Migration~~ ✅ ERLEDIGT (~150 LOC gespart)
|
||||
|
||||
**Problem:** 2 Apps nutzen nicht `@manacore/shared-theme`:
|
||||
- `apps/storage/apps/web/src/lib/stores/theme.svelte.ts` (96 LOC - custom)
|
||||
- `apps/questions/apps/web/src/lib/stores/theme.ts` (custom)
|
||||
**Status:** 2 Apps zu `createThemeStore()` aus `@manacore/shared-theme` migriert (29.01.2026)
|
||||
|
||||
**Aktion:** Migriere zu `createThemeStore()` aus `@manacore/shared-theme`
|
||||
**Migrierte Apps:**
|
||||
- ✅ `apps/storage/apps/web/src/lib/stores/theme.svelte.ts` (96 → 7 LOC = 89 LOC)
|
||||
- ✅ `apps/questions/apps/web/src/lib/stores/theme.ts` (62 → 47 LOC = 15 LOC, mit Rückwärtskompatibilität)
|
||||
|
||||
**Zusätzlich gefixt:**
|
||||
- ✅ `apps/storage/apps/web/src/routes/themes/+page.svelte` - Bug mit `def.colors.primary` → `def.light.primary`
|
||||
|
||||
**Questions Wrapper (Rückwärtskompatibilität):**
|
||||
```typescript
|
||||
// Legacy API (current, set, toggle) wird auf neue API gemappt
|
||||
export const theme = {
|
||||
get current() { return sharedTheme.mode; }, // Legacy
|
||||
get mode() { return sharedTheme.mode; }, // New
|
||||
set(newTheme) { sharedTheme.setMode(newTheme); }, // Legacy
|
||||
toggle() { sharedTheme.toggleMode(); }, // Legacy
|
||||
// ... new API forwarded
|
||||
};
|
||||
```
|
||||
|
||||
**Einsparung:** ~104 LOC (158 → 54 LOC)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -290,13 +307,37 @@ export const { isSidebarMode, isNavCollapsed } = createSimpleNavigationStores({
|
|||
|
||||
---
|
||||
|
||||
### 3.3 MITTEL: AppSlider Cleanup (240 LOC)
|
||||
### ~~3.3 MITTEL: AppSlider Cleanup~~ ✅ ANALYSIERT (Keine Aktion nötig)
|
||||
|
||||
**Problem:** 8 Apps haben lokale `AppSlider.svelte` Kopien, obwohl shared-ui Version existiert.
|
||||
**Status:** Nach Analyse: Die lokalen AppSlider.svelte Dateien sind KEINE Duplikate (29.01.2026)
|
||||
|
||||
**Betroffene Apps:** calendar, chat, contacts, manadeck, manacore, picture, presi, todo
|
||||
**Ergebnis der Analyse:**
|
||||
Die 8 lokalen `AppSlider.svelte` Dateien sind **Lokalisierungs-Wrapper**, nicht Duplikate:
|
||||
- Sie importieren `AppSlider` aus `@manacore/shared-ui`
|
||||
- Sie mappen `MANA_APPS` aus `@manacore/shared-branding` zu deutschen Labels
|
||||
- Sie übergeben deutsche Lokalisierung (`APP_STATUS_LABELS.de`, `APP_SLIDER_LABELS.de`) an die shared Komponente
|
||||
|
||||
**Aktion:** Verifiziere Import aus `@manacore/shared-ui`, lösche lokale Kopien.
|
||||
**Beispiel (apps/chat/apps/web):**
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { AppSlider } from '@manacore/shared-ui';
|
||||
import { MANA_APPS, APP_STATUS_LABELS, APP_SLIDER_LABELS } from '@manacore/shared-branding';
|
||||
|
||||
const apps = MANA_APPS.map((app) => ({
|
||||
name: app.name,
|
||||
description: app.description.de, // German localization
|
||||
longDescription: app.longDescription.de,
|
||||
// ...
|
||||
}));
|
||||
|
||||
const statusLabels = APP_STATUS_LABELS.de;
|
||||
const labels = APP_SLIDER_LABELS.de;
|
||||
</script>
|
||||
|
||||
<AppSlider {apps} title={labels.title} {statusLabels} ... />
|
||||
```
|
||||
|
||||
**Fazit:** Korrekte Architektur - shared-ui stellt die Komponente bereit, Apps liefern lokalisierte Daten.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue