From 0e5d923faf9ddbba0e2412b010e3a2084e0257aa Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 3 Dec 2025 00:09:47 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(auth):=20add=20centralized=20u?= =?UTF-8?q?ser=20settings=20synced=20across=20all=20apps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add settings module to mana-core-auth with REST API endpoints - Create user_settings table with globalSettings and appOverrides (JSONB) - Add createUserSettingsStore() factory in shared-theme package - Integrate user settings in all app layouts (calendar, chat, contacts, etc.) - Support for nav position, theme, locale settings with per-app overrides - Optimistic updates with localStorage caching for offline support - Add comprehensive documentation in docs/USER_SETTINGS.md API Endpoints: - GET /api/v1/settings - Get all user settings - PATCH /api/v1/settings/global - Update global settings - PATCH /api/v1/settings/app/:appId - Set app override - DELETE /api/v1/settings/app/:appId - Remove app override πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/lib/stores/user-settings.svelte.ts | 19 + .../apps/web/src/routes/+layout.svelte | 5 +- .../src/lib/stores/user-settings.svelte.ts | 19 + .../web/src/routes/(protected)/+layout.svelte | 5 + .../src/lib/stores/user-settings.svelte.ts | 19 + .../apps/web/src/routes/(app)/+layout.svelte | 5 + .../src/lib/stores/user-settings.svelte.ts | 20 + .../apps/web/src/routes/(app)/+layout.svelte | 9 +- .../src/routes/(app)/settings/+page.svelte | 218 ++++++++++- .../src/lib/stores/user-settings.svelte.ts | 19 + .../apps/web/src/routes/(app)/+layout.svelte | 5 + .../src/lib/stores/user-settings.svelte.ts | 19 + .../apps/web/src/routes/app/+layout.svelte | 9 + .../src/lib/stores/user-settings.svelte.ts | 19 + apps/presi/apps/web/src/routes/+layout.svelte | 7 +- .../src/lib/stores/user-settings.svelte.ts | 19 + .../apps/web/src/routes/+layout.svelte | 5 + .../src/lib/stores/user-settings.svelte.ts | 19 + .../zitare/apps/web/src/routes/+layout.svelte | 7 + docs/USER_SETTINGS.md | 357 ++++++++++++++++++ packages/shared-theme/src/index.ts | 15 + packages/shared-theme/src/types.ts | 104 +++++ .../src/user-settings-store.svelte.ts | 301 +++++++++++++++ .../src/navigation/PillNavigation.svelte | 28 +- services/mana-core-auth/src/app.module.ts | 2 + .../src/db/schema/auth.schema.ts | 24 ++ .../mana-core-auth/src/settings/dto/index.ts | 81 ++++ .../src/settings/settings.controller.ts | 70 ++++ .../src/settings/settings.module.ts | 11 + .../src/settings/settings.service.ts | 191 ++++++++++ 30 files changed, 1624 insertions(+), 7 deletions(-) create mode 100644 apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/chat/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/contacts/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/manacore/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/manadeck/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/picture/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/presi/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/storage/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 apps/zitare/apps/web/src/lib/stores/user-settings.svelte.ts create mode 100644 docs/USER_SETTINGS.md create mode 100644 packages/shared-theme/src/user-settings-store.svelte.ts create mode 100644 services/mana-core-auth/src/settings/dto/index.ts create mode 100644 services/mana-core-auth/src/settings/settings.controller.ts create mode 100644 services/mana-core-auth/src/settings/settings.module.ts create mode 100644 services/mana-core-auth/src/settings/settings.service.ts diff --git a/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts new file mode 100644 index 000000000..6cc7b62ed --- /dev/null +++ b/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts @@ -0,0 +1,19 @@ +/** + * User Settings Store for Calendar + * + * This store syncs settings with mana-core-auth and provides: + * - Global settings that apply to all apps + * - Per-app overrides for customization + * - localStorage caching for offline support + */ + +import { createUserSettingsStore } from '@manacore/shared-theme'; +import { authStore } from './auth.svelte'; + +const MANA_AUTH_URL = 'http://localhost:3001'; + +export const userSettings = createUserSettingsStore({ + appId: 'calendar', + authUrl: MANA_AUTH_URL, + getAccessToken: () => authStore.getAccessToken(), +}); diff --git a/apps/calendar/apps/web/src/routes/+layout.svelte b/apps/calendar/apps/web/src/routes/+layout.svelte index 1b31673f1..798467c0e 100644 --- a/apps/calendar/apps/web/src/routes/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/+layout.svelte @@ -7,6 +7,7 @@ import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui'; import { theme } from '$lib/stores/theme'; import { authStore } from '$lib/stores/auth.svelte'; + import { userSettings } from '$lib/stores/user-settings.svelte'; import { viewStore } from '$lib/stores/view.svelte'; import { calendarsStore } from '$lib/stores/calendars.svelte'; import { settingsStore } from '$lib/stores/settings.svelte'; @@ -144,9 +145,10 @@ // Initialize auth await authStore.initialize(); - // Load calendars if authenticated + // Load calendars and user settings if authenticated if (authStore.isAuthenticated) { await calendarsStore.fetchCalendars(); + await userSettings.load(); } // Initialize sidebar mode from localStorage @@ -196,6 +198,7 @@ onModeChange={handleModeChange} {isCollapsed} onCollapsedChange={handleCollapsedChange} + desktopPosition={userSettings.nav.desktopPosition} showThemeToggle={true} showThemeVariants={true} {themeVariantItems} diff --git a/apps/chat/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/chat/apps/web/src/lib/stores/user-settings.svelte.ts new file mode 100644 index 000000000..b8becd027 --- /dev/null +++ b/apps/chat/apps/web/src/lib/stores/user-settings.svelte.ts @@ -0,0 +1,19 @@ +/** + * User Settings Store for Chat + * + * This store syncs settings with mana-core-auth and provides: + * - Global settings that apply to all apps + * - Per-app overrides for customization + * - localStorage caching for offline support + */ + +import { createUserSettingsStore } from '@manacore/shared-theme'; +import { authStore } from './auth.svelte'; + +const MANA_AUTH_URL = 'http://localhost:3001'; + +export const userSettings = createUserSettingsStore({ + appId: 'chat', + authUrl: MANA_AUTH_URL, + getAccessToken: () => authStore.getAccessToken(), +}); diff --git a/apps/chat/apps/web/src/routes/(protected)/+layout.svelte b/apps/chat/apps/web/src/routes/(protected)/+layout.svelte index c89e2e00c..b910e07a4 100644 --- a/apps/chat/apps/web/src/routes/(protected)/+layout.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/+layout.svelte @@ -4,6 +4,7 @@ import { page } from '$app/stores'; import { locale } from 'svelte-i18n'; import { authStore } from '$lib/stores/auth.svelte'; + import { userSettings } from '$lib/stores/user-settings.svelte'; import { theme } from '$lib/stores/theme'; import { THEME_DEFINITIONS } from '@manacore/shared-theme'; import { @@ -156,6 +157,9 @@ return; } + // Load user settings + await userSettings.load(); + isChecking = false; }); @@ -187,6 +191,7 @@ onModeChange={handleModeChange} {isCollapsed} onCollapsedChange={handleCollapsedChange} + desktopPosition={userSettings.nav.desktopPosition} showThemeToggle={true} showThemeVariants={true} {themeVariantItems} diff --git a/apps/contacts/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/contacts/apps/web/src/lib/stores/user-settings.svelte.ts new file mode 100644 index 000000000..70c7b99ae --- /dev/null +++ b/apps/contacts/apps/web/src/lib/stores/user-settings.svelte.ts @@ -0,0 +1,19 @@ +/** + * User Settings Store for Contacts + * + * This store syncs settings with mana-core-auth and provides: + * - Global settings that apply to all apps + * - Per-app overrides for customization + * - localStorage caching for offline support + */ + +import { createUserSettingsStore } from '@manacore/shared-theme'; +import { authStore } from './auth.svelte'; + +const MANA_AUTH_URL = 'http://localhost:3001'; + +export const userSettings = createUserSettingsStore({ + appId: 'contacts', + authUrl: MANA_AUTH_URL, + getAccessToken: () => authStore.getAccessToken(), +}); diff --git a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte index 7e8980f42..1e967fcc3 100644 --- a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte @@ -7,6 +7,7 @@ import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui'; import { theme } from '$lib/stores/theme'; import { authStore } from '$lib/stores/auth.svelte'; + import { userSettings } from '$lib/stores/user-settings.svelte'; import { THEME_DEFINITIONS } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, @@ -137,6 +138,9 @@ return; } + // Load user settings + await userSettings.load(); + // Initialize sidebar mode from localStorage const savedSidebar = localStorage.getItem('contacts-nav-sidebar'); if (savedSidebar === 'true') { @@ -169,6 +173,7 @@ onModeChange={handleModeChange} {isCollapsed} onCollapsedChange={handleCollapsedChange} + desktopPosition={userSettings.nav.desktopPosition} showThemeToggle={true} showThemeVariants={true} {themeVariantItems} diff --git a/apps/manacore/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/manacore/apps/web/src/lib/stores/user-settings.svelte.ts new file mode 100644 index 000000000..a520d0aeb --- /dev/null +++ b/apps/manacore/apps/web/src/lib/stores/user-settings.svelte.ts @@ -0,0 +1,20 @@ +/** + * User Settings Store for ManaCore + * + * This store syncs settings with mana-core-auth and provides: + * - Global settings that apply to all apps + * - Per-app overrides for customization + * - localStorage caching for offline support + */ + +import { createUserSettingsStore } from '@manacore/shared-theme'; +import { authStore } from './authStore.svelte'; + +// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available +const MANA_AUTH_URL = 'http://localhost:3001'; + +export const userSettings = createUserSettingsStore({ + appId: 'manacore', + authUrl: MANA_AUTH_URL, + getAccessToken: () => authStore.getAccessToken(), +}); diff --git a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte index 811077ccb..4bd318707 100644 --- a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte @@ -11,6 +11,7 @@ import { setLocale, supportedLocales } from '$lib/i18n'; import { theme } from '$lib/stores/theme'; import { authStore } from '$lib/stores/authStore.svelte'; + import { userSettings } from '$lib/stores/user-settings.svelte'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -129,7 +130,7 @@ } }); - onMount(() => { + onMount(async () => { // Initialize sidebar mode from localStorage const savedSidebar = localStorage.getItem('manacore-nav-sidebar'); if (savedSidebar === 'true') { @@ -144,6 +145,11 @@ collapsedStore.set(true); } + // Load user settings from server + if (authStore.isAuthenticated) { + await userSettings.load(); + } + loading = false; }); @@ -174,6 +180,7 @@ onModeChange={handleModeChange} {isCollapsed} onCollapsedChange={handleCollapsedChange} + desktopPosition={userSettings.nav.desktopPosition} showThemeToggle={true} showThemeVariants={true} {themeVariantItems} diff --git a/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte index c130eb364..0a96154c3 100644 --- a/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte @@ -3,6 +3,8 @@ import { Button, Input, Card, PageHeader } from '@manacore/shared-ui'; import { authStore } from '$lib/stores/authStore.svelte'; import { creditsService, type CreditBalance } from '$lib/api/credits'; + import { userSettings } from '$lib/stores/user-settings.svelte'; + import type { NavPosition, ThemeMode } from '@manacore/shared-theme'; let loading = $state(true); let savingProfile = $state(false); @@ -20,13 +22,40 @@ if (authStore.isAuthenticated) { try { creditBalance = await creditsService.getBalance(); + // Load user settings from server + await userSettings.load(); } catch (e) { - console.error('Failed to load credits:', e); + console.error('Failed to load data:', e); } } loading = false; }); + // Navigation position handler + async function handleNavPositionChange(position: NavPosition) { + await userSettings.updateGlobal({ nav: { ...userSettings.globalSettings.nav, desktopPosition: position } }); + } + + // Sidebar collapsed handler + async function handleSidebarChange(collapsed: boolean) { + await userSettings.updateGlobal({ nav: { ...userSettings.globalSettings.nav, sidebarCollapsed: collapsed } }); + } + + // Theme mode handler + async function handleThemeModeChange(mode: ThemeMode) { + await userSettings.updateGlobal({ theme: { ...userSettings.globalSettings.theme, mode } }); + } + + // Color scheme handler + async function handleColorSchemeChange(colorScheme: string) { + await userSettings.updateGlobal({ theme: { ...userSettings.globalSettings.theme, colorScheme } }); + } + + // Locale handler + async function handleLocaleChange(locale: string) { + await userSettings.updateGlobal({ locale }); + } + function formatCredits(amount: number): string { return amount.toLocaleString('de-DE'); } @@ -133,6 +162,193 @@ + + +
+
+
+ + + + +
+
+

Standard App-Einstellungen

+

+ Diese Einstellungen gelten fΓΌr alle Mana Apps +

+
+
+ +
+ +
+

+ Navigation +

+ +
+
+

Position (Desktop)

+

Position der Navigation auf großen Bildschirmen

+
+
+ + +
+
+ +
+
+

Sidebar eingeklappt

+

Standard-Zustand der Sidebar

+
+ +
+
+ + +
+

+ Erscheinungsbild +

+ +
+
+

Farbmodus

+

Hell, Dunkel oder automatisch

+
+
+ + + +
+
+ +
+
+

Farbschema

+

Akzentfarbe der BenutzeroberflΓ€che

+
+
+ {#each [ + { id: 'ocean', label: 'Ozean', color: 'bg-blue-500' }, + { id: 'forest', label: 'Wald', color: 'bg-green-500' }, + { id: 'sunset', label: 'Sonnenuntergang', color: 'bg-orange-500' }, + { id: 'lavender', label: 'Lavendel', color: 'bg-purple-500' } + ] as scheme} + + {/each} +
+
+
+ + +
+

+ Sprache +

+ +
+
+

Anzeigesprache

+

Sprache der BenutzeroberflΓ€che

+
+
+ {#each [ + { id: 'de', label: 'DE' }, + { id: 'en', label: 'EN' }, + { id: 'fr', label: 'FR' }, + { id: 'es', label: 'ES' }, + { id: 'it', label: 'IT' } + ] as lang} + + {/each} +
+
+
+
+ + {#if userSettings.syncing} +
+
+ Einstellungen werden synchronisiert... +
+ {/if} +
+
+
diff --git a/apps/manadeck/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/manadeck/apps/web/src/lib/stores/user-settings.svelte.ts new file mode 100644 index 000000000..8757dc7d8 --- /dev/null +++ b/apps/manadeck/apps/web/src/lib/stores/user-settings.svelte.ts @@ -0,0 +1,19 @@ +/** + * User Settings Store for ManaDeck + * + * This store syncs settings with mana-core-auth and provides: + * - Global settings that apply to all apps + * - Per-app overrides for customization + * - localStorage caching for offline support + */ + +import { createUserSettingsStore } from '@manacore/shared-theme'; +import { authStore } from './authStore.svelte'; + +const MANA_AUTH_URL = 'http://localhost:3001'; + +export const userSettings = createUserSettingsStore({ + appId: 'manadeck', + authUrl: MANA_AUTH_URL, + getAccessToken: () => authStore.getAccessToken(), +}); diff --git a/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte b/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte index e9454f691..97fe87661 100644 --- a/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte @@ -4,6 +4,7 @@ import { onMount } from 'svelte'; import { locale } from 'svelte-i18n'; import { authStore } from '$lib/stores/authStore.svelte'; + import { userSettings } from '$lib/stores/user-settings.svelte'; import { theme } from '$lib/stores/theme'; import { isSidebarMode as sidebarModeStore, @@ -135,6 +136,9 @@ return; } + // Load user settings + await userSettings.load(); + // Initialize sidebar mode from localStorage const savedSidebar = localStorage.getItem('manadeck-nav-sidebar'); if (savedSidebar === 'true') { @@ -177,6 +181,7 @@ onModeChange={handleModeChange} {isCollapsed} onCollapsedChange={handleCollapsedChange} + desktopPosition={userSettings.nav.desktopPosition} showThemeToggle={true} showThemeVariants={true} {themeVariantItems} diff --git a/apps/picture/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/picture/apps/web/src/lib/stores/user-settings.svelte.ts new file mode 100644 index 000000000..84fa5e818 --- /dev/null +++ b/apps/picture/apps/web/src/lib/stores/user-settings.svelte.ts @@ -0,0 +1,19 @@ +/** + * User Settings Store for Picture + * + * This store syncs settings with mana-core-auth and provides: + * - Global settings that apply to all apps + * - Per-app overrides for customization + * - localStorage caching for offline support + */ + +import { createUserSettingsStore } from '@manacore/shared-theme'; +import { authStore } from './auth.svelte'; + +const MANA_AUTH_URL = 'http://localhost:3001'; + +export const userSettings = createUserSettingsStore({ + appId: 'picture', + authUrl: MANA_AUTH_URL, + getAccessToken: () => authStore.getAccessToken(), +}); diff --git a/apps/picture/apps/web/src/routes/app/+layout.svelte b/apps/picture/apps/web/src/routes/app/+layout.svelte index 9fd9638f3..8f3c884a1 100644 --- a/apps/picture/apps/web/src/routes/app/+layout.svelte +++ b/apps/picture/apps/web/src/routes/app/+layout.svelte @@ -1,5 +1,6 @@ +``` + +### Settings lesen + +```svelte + + + +

Nav Position: {userSettings.nav.desktopPosition}

+

Theme Mode: {userSettings.theme.mode}

+

Locale: {userSettings.locale}

+ + +

Loading: {userSettings.syncing}

+

Has Override: {userSettings.hasAppOverride}

+``` + +### Settings Γ€ndern + +```typescript +// Globale Settings Γ€ndern (gilt fΓΌr alle Apps) +await userSettings.updateGlobal({ + locale: 'en', + theme: { mode: 'dark' } +}); + +// App-Override setzen (nur diese App) +await userSettings.updateAppOverride({ + nav: { desktopPosition: 'bottom' }, + theme: { colorScheme: 'nature' } +}); + +// App-Override lΓΆschen (zurΓΌck zu Global) +await userSettings.removeAppOverride(); +``` + +### Beispiel: PillNavigation + +```svelte + +``` + +## Store API Referenz + +### Properties (readonly) + +| Property | Type | Beschreibung | +|----------|------|--------------| +| `nav` | `NavSettings` | Resolved Navigation Settings | +| `theme` | `ThemeSettings` | Resolved Theme Settings | +| `locale` | `string` | Aktuelle Sprache | +| `globalSettings` | `GlobalSettings` | Rohe globale Settings | +| `hasAppOverride` | `boolean` | Hat diese App einen Override? | +| `syncing` | `boolean` | Wird gerade synchronisiert? | +| `loaded` | `boolean` | Wurden Settings geladen? | + +### Methods + +| Method | Beschreibung | +|--------|--------------| +| `load()` | Settings vom Server laden | +| `updateGlobal(settings)` | Globale Settings aktualisieren | +| `updateAppOverride(settings)` | App-Override setzen | +| `removeAppOverride()` | App-Override lΓΆschen | + +## Features + +### Optimistic Updates + +Γ„nderungen werden sofort in der UI angezeigt, bevor die Server-Antwort eintrifft. Bei einem Fehler wird automatisch auf den vorherigen Zustand zurΓΌckgesetzt. + +### localStorage Cache + +Settings werden lokal gecached fΓΌr: +- Schnelle UI beim App-Start (keine Wartezeit auf Server) +- Offline-UnterstΓΌtzung (letzte bekannte Settings) + +Cache-Key: `manacore-user-settings-{appId}` + +### Deep Merge + +Partielle Updates werden automatisch gemerged: + +```typescript +// Nur colorScheme Γ€ndern, mode bleibt erhalten +await userSettings.updateGlobal({ + theme: { colorScheme: 'nature' } +}); +// Ergebnis: { theme: { mode: 'system', colorScheme: 'nature' } } +``` + +## Datenbank-Schema + +Tabelle: `auth.user_settings` + +| Column | Type | Beschreibung | +|--------|------|--------------| +| `user_id` | TEXT (PK) | Referenz zu auth.users | +| `global_settings` | JSONB | Globale Einstellungen | +| `app_overrides` | JSONB | Pro-App Überschreibungen | +| `created_at` | TIMESTAMP | Erstellungszeitpunkt | +| `updated_at` | TIMESTAMP | Letzte Aktualisierung | + +## Dateien + +### Backend (mana-core-auth) + +| Datei | Beschreibung | +|-------|--------------| +| `src/settings/settings.module.ts` | NestJS Module | +| `src/settings/settings.controller.ts` | REST API Endpoints | +| `src/settings/settings.service.ts` | Business Logic | +| `src/settings/dto/index.ts` | DTOs fΓΌr Requests | +| `src/db/schema/auth.schema.ts` | Drizzle Schema (`userSettings`) | + +### Shared Package + +| Datei | Beschreibung | +|-------|--------------| +| `packages/shared-theme/src/user-settings-store.svelte.ts` | Svelte 5 Store Factory | +| `packages/shared-theme/src/types.ts` | TypeScript Interfaces | + +### App Integration (Beispiel Calendar) + +| Datei | Beschreibung | +|-------|--------------| +| `apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts` | Store-Instanz | +| `apps/calendar/apps/web/src/routes/+layout.svelte` | Integration in Layout | + +## Integrierte Apps + +Folgende Apps nutzen bereits die zentralen User Settings: + +- Calendar (`calendar`) +- Chat (`chat`) +- Contacts (`contacts`) +- ManaCore (`manacore`) +- ManaDeck (`manadeck`) +- Picture (`picture`) +- Presi (`presi`) +- Storage (`storage`) +- Zitare (`zitare`) + +## Neue App integrieren + +1. **Store erstellen** in `src/lib/stores/user-settings.svelte.ts`: + ```typescript + import { createUserSettingsStore } from '@manacore/shared-theme'; + import { authStore } from './auth.svelte'; + + export const userSettings = createUserSettingsStore({ + appId: 'my-app', // Eindeutige ID + authUrl: import.meta.env.PUBLIC_MANA_CORE_AUTH_URL, + getAccessToken: () => authStore.getAccessToken() + }); + ``` + +2. **Im Layout laden** nach Auth-Check: + ```typescript + if (authStore.isAuthenticated) { + await userSettings.load(); + } + ``` + +3. **Settings verwenden**: + ```svelte + + ``` diff --git a/packages/shared-theme/src/index.ts b/packages/shared-theme/src/index.ts index 3082935f2..cfbe3ac16 100644 --- a/packages/shared-theme/src/index.ts +++ b/packages/shared-theme/src/index.ts @@ -15,8 +15,20 @@ export type { A11ySettings, A11yStore, A11yStoreConfig, + // User Settings Types (synced via mana-core-auth) + NavPosition, + NavSettings, + ThemeSettings, + GlobalSettings, + AppOverride, + UserSettingsResponse, + UserSettingsStore, + UserSettingsStoreConfig, } from './types'; +// User Settings Constants +export { DEFAULT_GLOBAL_SETTINGS } from './types'; + // Constants export { THEME_VARIANTS, @@ -44,6 +56,9 @@ export { createThemeStore, APP_THEME_CONFIGS } from './store.svelte'; // A11y Store export { createA11yStore } from './a11y-store.svelte'; +// User Settings Store +export { createUserSettingsStore } from './user-settings-store.svelte'; + // Utils export { isBrowser, diff --git a/packages/shared-theme/src/types.ts b/packages/shared-theme/src/types.ts index 1664405ee..db804c8f9 100644 --- a/packages/shared-theme/src/types.ts +++ b/packages/shared-theme/src/types.ts @@ -203,3 +203,107 @@ export interface A11yStoreConfig { /** Default settings override */ defaults?: Partial; } + +// ============================================================================ +// Global User Settings Types (synced via mana-core-auth) +// ============================================================================ + +/** + * Navigation position for desktop (mobile always at bottom) + */ +export type NavPosition = 'top' | 'bottom'; + +/** + * Navigation settings + */ +export interface NavSettings { + /** Desktop navigation position */ + desktopPosition: NavPosition; + /** Whether sidebar is collapsed */ + sidebarCollapsed: boolean; +} + +/** + * Theme settings (synced to server) + */ +export interface ThemeSettings { + /** Theme mode preference */ + mode: ThemeMode; + /** Color scheme / variant */ + colorScheme: string; +} + +/** + * Global settings that apply to all apps by default + */ +export interface GlobalSettings { + nav: NavSettings; + theme: ThemeSettings; + locale: string; +} + +/** + * Per-app override settings (partial, only overridden values) + */ +export interface AppOverride { + nav?: Partial; + theme?: Partial; +} + +/** + * Full user settings response from API + */ +export interface UserSettingsResponse { + globalSettings: GlobalSettings; + appOverrides: Record; +} + +/** + * Default global settings + */ +export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { + nav: { desktopPosition: 'top', sidebarCollapsed: false }, + theme: { mode: 'system', colorScheme: 'ocean' }, + locale: 'de', +}; + +/** + * User settings store interface + */ +export interface UserSettingsStore { + /** Resolved navigation settings (global + app override) */ + readonly nav: NavSettings; + /** Resolved theme settings (global + app override) */ + readonly theme: ThemeSettings; + /** Current locale */ + readonly locale: string; + /** Raw global settings */ + readonly globalSettings: GlobalSettings; + /** Whether current app has an override */ + readonly hasAppOverride: boolean; + /** Whether settings are being synced */ + readonly syncing: boolean; + /** Whether settings are loaded */ + readonly loaded: boolean; + + /** Load settings from server */ + load: () => Promise; + /** Update global settings */ + updateGlobal: (settings: Partial) => Promise; + /** Update app-specific override */ + updateAppOverride: (settings: AppOverride) => Promise; + /** Remove app override (revert to global) */ + removeAppOverride: () => Promise; +} + +/** + * User settings store configuration + */ +export interface UserSettingsStoreConfig { + /** App identifier (e.g., 'calendar', 'chat') */ + appId: string; + /** Auth service base URL */ + authUrl: string; + /** Function to get current access token */ + getAccessToken: () => Promise; +} diff --git a/packages/shared-theme/src/user-settings-store.svelte.ts b/packages/shared-theme/src/user-settings-store.svelte.ts new file mode 100644 index 000000000..0f31692a0 --- /dev/null +++ b/packages/shared-theme/src/user-settings-store.svelte.ts @@ -0,0 +1,301 @@ +import type { + UserSettingsStore, + UserSettingsStoreConfig, + GlobalSettings, + AppOverride, + NavSettings, + ThemeSettings, + UserSettingsResponse, +} from './types'; +import { DEFAULT_GLOBAL_SETTINGS } from './types'; +import { isBrowser } from './utils'; + +const STORAGE_KEY_PREFIX = 'manacore-user-settings'; + +/** + * Create a User Settings store for your app + * + * This store syncs settings with mana-core-auth and provides: + * - Global settings that apply to all apps + * - Per-app overrides for customization + * - localStorage caching for offline support + * + * @example + * ```typescript + * import { createUserSettingsStore } from '@manacore/shared-theme'; + * + * export const userSettings = createUserSettingsStore({ + * appId: 'calendar', + * authUrl: 'http://localhost:3001', + * getAccessToken: () => authStore.getAccessToken() + * }); + * + * // In +layout.svelte + * $effect(() => { + * if (authStore.isAuthenticated) { + * userSettings.load(); + * } + * }); + * ``` + */ +export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSettingsStore { + const { appId, authUrl, getAccessToken } = config; + const storageKey = `${STORAGE_KEY_PREFIX}-${appId}`; + + // State + let globalSettings = $state({ ...DEFAULT_GLOBAL_SETTINGS }); + let appOverrides = $state>({}); + let syncing = $state(false); + let loaded = $state(false); + + // Derived: resolved nav settings (global + app override) + const nav = $derived({ + ...globalSettings.nav, + ...(appOverrides[appId]?.nav || {}), + }); + + // Derived: resolved theme settings (global + app override) + const theme = $derived({ + ...globalSettings.theme, + ...(appOverrides[appId]?.theme || {}), + }); + + // Derived: current locale + const locale = $derived(globalSettings.locale); + + // Derived: whether this app has an override + const hasAppOverride = $derived(!!appOverrides[appId]); + + /** + * Save current settings to localStorage (for offline fallback) + */ + function saveToStorage(): void { + if (!isBrowser()) return; + try { + localStorage.setItem( + storageKey, + JSON.stringify({ + globalSettings, + appOverrides, + timestamp: Date.now(), + }) + ); + } catch (e) { + console.error('Failed to save user settings to storage:', e); + } + } + + /** + * Load settings from localStorage (fallback) + */ + function loadFromStorage(): boolean { + if (!isBrowser()) return false; + try { + const stored = localStorage.getItem(storageKey); + if (stored) { + const data = JSON.parse(stored); + if (data.globalSettings) { + globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings }; + } + if (data.appOverrides) { + appOverrides = data.appOverrides; + } + return true; + } + } catch (e) { + console.error('Failed to load user settings from storage:', e); + } + return false; + } + + /** + * Make an API request to the settings endpoint + */ + async function apiRequest( + method: string, + path: string, + body?: object + ): Promise { + const token = await getAccessToken(); + if (!token) { + console.warn('No access token available for settings API'); + return null; + } + + try { + const response = await fetch(`${authUrl}/api/v1/settings${path}`, { + method, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + console.error(`Settings API error: ${response.status}`); + return null; + } + + return await response.json(); + } catch (e) { + console.error('Settings API request failed:', e); + return null; + } + } + + /** + * Load settings from server + */ + async function load(): Promise { + // Load from cache first for instant UI + loadFromStorage(); + + syncing = true; + try { + const data = await apiRequest('GET', ''); + + if (data?.success) { + globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings }; + appOverrides = data.appOverrides || {}; + saveToStorage(); + loaded = true; + } + } finally { + syncing = false; + } + } + + /** + * Update global settings + */ + async function updateGlobal(settings: Partial): Promise { + // Optimistic update + const previousGlobal = { ...globalSettings }; + globalSettings = { + nav: { ...globalSettings.nav, ...settings.nav }, + theme: { ...globalSettings.theme, ...settings.theme }, + locale: settings.locale ?? globalSettings.locale, + }; + saveToStorage(); + + syncing = true; + try { + const data = await apiRequest( + 'PATCH', + '/global', + settings + ); + + if (data?.success) { + globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings }; + appOverrides = data.appOverrides || {}; + saveToStorage(); + } else { + // Rollback on failure + globalSettings = previousGlobal; + saveToStorage(); + } + } finally { + syncing = false; + } + } + + /** + * Update app-specific override + */ + async function updateAppOverride(settings: AppOverride): Promise { + // Optimistic update + const previousOverrides = { ...appOverrides }; + appOverrides = { + ...appOverrides, + [appId]: { + ...appOverrides[appId], + ...settings, + }, + }; + saveToStorage(); + + syncing = true; + try { + const data = await apiRequest( + 'PATCH', + `/app/${appId}`, + settings + ); + + if (data?.success) { + globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings }; + appOverrides = data.appOverrides || {}; + saveToStorage(); + } else { + // Rollback on failure + appOverrides = previousOverrides; + saveToStorage(); + } + } finally { + syncing = false; + } + } + + /** + * Remove app override (revert to global settings) + */ + async function removeAppOverride(): Promise { + // Optimistic update + const previousOverrides = { ...appOverrides }; + const newOverrides = { ...appOverrides }; + delete newOverrides[appId]; + appOverrides = newOverrides; + saveToStorage(); + + syncing = true; + try { + const data = await apiRequest( + 'DELETE', + `/app/${appId}` + ); + + if (data?.success) { + globalSettings = { ...DEFAULT_GLOBAL_SETTINGS, ...data.globalSettings }; + appOverrides = data.appOverrides || {}; + saveToStorage(); + } else { + // Rollback on failure + appOverrides = previousOverrides; + saveToStorage(); + } + } finally { + syncing = false; + } + } + + return { + get nav() { + return nav; + }, + get theme() { + return theme; + }, + get locale() { + return locale; + }, + get globalSettings() { + return globalSettings; + }, + get hasAppOverride() { + return hasAppOverride; + }, + get syncing() { + return syncing; + }, + get loaded() { + return loaded; + }, + + load, + updateGlobal, + updateAppOverride, + removeAppOverride, + }; +} diff --git a/packages/shared-ui/src/navigation/PillNavigation.svelte b/packages/shared-ui/src/navigation/PillNavigation.svelte index 9b3339ccb..b8e4d25dd 100644 --- a/packages/shared-ui/src/navigation/PillNavigation.svelte +++ b/packages/shared-ui/src/navigation/PillNavigation.svelte @@ -131,6 +131,8 @@ onA11yReduceMotionChange?: (reduce: boolean) => void; /** Show a11y quick toggles in theme dropdown */ showA11yQuickToggles?: boolean; + /** Desktop navigation position (mobile always at bottom) */ + desktopPosition?: 'top' | 'bottom'; } let { @@ -172,6 +174,7 @@ a11yReduceMotion = false, onA11yReduceMotionChange, showA11yQuickToggles = false, + desktopPosition = 'top', }: Props = $props(); // Type guards for elements @@ -313,6 +316,7 @@