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 @@