diff --git a/apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte b/apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte index b30ea921e..38a12337d 100644 --- a/apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte +++ b/apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte @@ -111,7 +111,7 @@ const browserCacheReady = $derived(webgpuSupported && localLlmStatus.current.state === 'ready'); -
+
+ + +
+ {@render children()} +
+ + diff --git a/apps/mana/apps/web/src/lib/components/settings/SettingsSectionHeader.svelte b/apps/mana/apps/web/src/lib/components/settings/SettingsSectionHeader.svelte new file mode 100644 index 000000000..cbfa14c75 --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/SettingsSectionHeader.svelte @@ -0,0 +1,47 @@ + + + +
+
+
+ +
+
+

{title}

+ {#if description} +

{description}

+ {/if} +
+
+ {#if action} +
{@render action()}
+ {/if} +
diff --git a/apps/mana/apps/web/src/lib/components/settings/SettingsSidebar.svelte b/apps/mana/apps/web/src/lib/components/settings/SettingsSidebar.svelte new file mode 100644 index 000000000..53dd680e7 --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/SettingsSidebar.svelte @@ -0,0 +1,368 @@ + + + + + + diff --git a/apps/mana/apps/web/src/lib/components/settings/searchIndex.ts b/apps/mana/apps/web/src/lib/components/settings/searchIndex.ts new file mode 100644 index 000000000..358d3f41e --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/searchIndex.ts @@ -0,0 +1,215 @@ +/** + * settings/searchIndex — single source of truth for the settings sidebar + * categories and the in-page search index. Editing a single entry here + * updates both the navigation and the search results. + */ +import type { Component } from 'svelte'; +import { User, Gear, Robot, ShieldCheck, CurrencyCircleDollar, Cloud } from '@mana/shared-icons'; + +export type CategoryId = 'profile' | 'general' | 'ai' | 'security' | 'credits' | 'data'; + +export interface Category { + id: CategoryId; + label: string; + description: string; + icon: Component; + /** Anchor ids in this category — used for hash-based deep-links. */ + anchors: string[]; +} + +export const categories: Category[] = [ + { + id: 'profile', + label: 'Profil', + description: 'Persönliche Daten & Konto', + icon: User, + anchors: ['profile', 'account'], + }, + { + id: 'general', + label: 'Allgemein', + description: 'Theme, Sprache, Benachrichtigungen', + icon: Gear, + anchors: ['global'], + }, + { + id: 'ai', + label: 'KI', + description: 'Compute-Backend & Modelle', + icon: Robot, + anchors: ['ai-options'], + }, + { + id: 'security', + label: 'Sicherheit', + description: 'Passkeys, 2FA & Sitzungen', + icon: ShieldCheck, + anchors: ['passkeys', 'sessions', 'two-factor', 'security-log'], + }, + { + id: 'credits', + label: 'Credits', + description: 'Guthaben & Transaktionen', + icon: CurrencyCircleDollar, + anchors: ['credits'], + }, + { + id: 'data', + label: 'Daten & Sync', + description: 'Cloud-Sync, Export & DSGVO', + icon: Cloud, + anchors: ['cloud-sync', 'my-data'], + }, +]; + +export interface SearchEntry { + /** Display label shown in the result list */ + label: string; + /** Extra search keywords (the label is always searched too) */ + keywords?: string[]; + category: CategoryId; + anchor: string; +} + +export const searchIndex: SearchEntry[] = [ + // Profile + { label: 'E-Mail', keywords: ['email', 'mail'], category: 'profile', anchor: 'profile' }, + { label: 'Vorname', keywords: ['name'], category: 'profile', anchor: 'profile' }, + { label: 'Nachname', keywords: ['name'], category: 'profile', anchor: 'profile' }, + { + label: 'Konto-Status', + keywords: ['rolle', 'role', 'aktiv'], + category: 'profile', + anchor: 'account', + }, + { label: 'Benutzer-ID', keywords: ['id', 'uid'], category: 'profile', anchor: 'account' }, + + // General + { + label: 'Theme', + keywords: ['dark', 'light', 'farbe', 'design'], + category: 'general', + anchor: 'global', + }, + { + label: 'Sprache', + keywords: ['language', 'i18n', 'deutsch', 'english'], + category: 'general', + anchor: 'global', + }, + { + label: 'Benachrichtigungen', + keywords: ['notification', 'sound'], + category: 'general', + anchor: 'global', + }, + + // AI + { + label: 'KI-Optionen', + keywords: ['llm', 'ai', 'compute'], + category: 'ai', + anchor: 'ai-options', + }, + { + label: 'Browser-Modell (Gemma)', + keywords: ['gemma', 'webgpu', 'lokal', 'offline'], + category: 'ai', + anchor: 'ai-options', + }, + { + label: 'Mana-Server (KI)', + keywords: ['server', 'self-hosted'], + category: 'ai', + anchor: 'ai-options', + }, + { + label: 'Cloud-KI (Gemini)', + keywords: ['google', 'cloud', 'gemini'], + category: 'ai', + anchor: 'ai-options', + }, + + // Security + { + label: 'Passkeys', + keywords: ['webauthn', 'fido', 'biometrie'], + category: 'security', + anchor: 'passkeys', + }, + { + label: 'Aktive Sessions', + keywords: ['logout', 'gerät', 'device'], + category: 'security', + anchor: 'sessions', + }, + { + label: 'Zwei-Faktor (2FA)', + keywords: ['totp', '2fa', 'mfa'], + category: 'security', + anchor: 'two-factor', + }, + { + label: 'Sicherheits-Log', + keywords: ['audit', 'history', 'verlauf'], + category: 'security', + anchor: 'security-log', + }, + + // Credits + { + label: 'Credits-Guthaben', + keywords: ['balance', 'geld'], + category: 'credits', + anchor: 'credits', + }, + { + label: 'Credits kaufen', + keywords: ['buy', 'pakete', 'kaufen'], + category: 'credits', + anchor: 'credits', + }, + { label: 'Transaktionen', keywords: ['history'], category: 'credits', anchor: 'credits' }, + + // Data + { + label: 'Cloud Sync', + keywords: ['sync', 'backup', 'geräte'], + category: 'data', + anchor: 'cloud-sync', + }, + { + label: 'Daten exportieren', + keywords: ['export', 'dsgvo', 'gdpr', 'json'], + category: 'data', + anchor: 'my-data', + }, + { + label: 'Konto löschen', + keywords: ['delete', 'gdpr', 'dsgvo'], + category: 'data', + anchor: 'my-data', + }, +]; + +/** Tiny case-insensitive ranker — exact > prefix > contains. */ +export function searchSettings(query: string, limit = 8): SearchEntry[] { + const q = query.trim().toLowerCase(); + if (!q) return []; + const results: { entry: SearchEntry; score: number }[] = []; + for (const entry of searchIndex) { + const haystacks = [ + entry.label.toLowerCase(), + ...(entry.keywords ?? []).map((k) => k.toLowerCase()), + ]; + let score = 0; + for (const h of haystacks) { + if (h === q) score = Math.max(score, 100); + else if (h.startsWith(q)) score = Math.max(score, 50); + else if (h.includes(q)) score = Math.max(score, 20); + } + if (score > 0) results.push({ entry, score }); + } + results.sort((a, b) => b.score - a.score); + return results.slice(0, limit).map((r) => r.entry); +} diff --git a/apps/mana/apps/web/src/lib/components/settings/sections/AiSection.svelte b/apps/mana/apps/web/src/lib/components/settings/sections/AiSection.svelte new file mode 100644 index 000000000..dc63a43f2 --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/sections/AiSection.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/apps/mana/apps/web/src/lib/components/settings/sections/CreditsSection.svelte b/apps/mana/apps/web/src/lib/components/settings/sections/CreditsSection.svelte new file mode 100644 index 000000000..2ae591b5b --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/sections/CreditsSection.svelte @@ -0,0 +1,75 @@ + + + + + {#snippet action()} + Alle Details + {/snippet} + + +
+
+

Verfügbar

+

+ {creditBalance ? formatCredits(creditBalance.balance) : '...'} +

+
+
+

Gratis heute

+

+ {creditBalance + ? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}` + : '...'} +

+
+
+

Gesamt verbraucht

+

+ {creditBalance ? formatCredits(creditBalance.totalSpent) : '...'} +

+
+
+ + +
diff --git a/apps/mana/apps/web/src/lib/components/settings/sections/DataSection.svelte b/apps/mana/apps/web/src/lib/components/settings/sections/DataSection.svelte new file mode 100644 index 000000000..e659f4a4f --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/sections/DataSection.svelte @@ -0,0 +1,64 @@ + + + + + {#snippet action()} + Einstellungen + {/snippet} + + + + + + +
+
+
+

Daten ansehen & exportieren

+

+ Sieh alle deine gespeicherten Daten ein und exportiere sie als JSON +

+
+ + Meine Daten + + +
+ +
+
+
+

Konto löschen

+

+ Das Löschen deines Kontos kann nicht rückgängig gemacht werden. +

+
+ + Verwalten + +
+
+
+
diff --git a/apps/mana/apps/web/src/lib/components/settings/sections/GeneralSection.svelte b/apps/mana/apps/web/src/lib/components/settings/sections/GeneralSection.svelte new file mode 100644 index 000000000..99f8a2b39 --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/sections/GeneralSection.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/apps/mana/apps/web/src/lib/components/settings/sections/ProfileSection.svelte b/apps/mana/apps/web/src/lib/components/settings/sections/ProfileSection.svelte new file mode 100644 index 000000000..2275820c8 --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/sections/ProfileSection.svelte @@ -0,0 +1,134 @@ + + + + + + {#if profileSuccess} +
+ Profil erfolgreich aktualisiert! +
+ {/if} + {#if profileError} +
+ {profileError} +
+ {/if} + +
+
+ + +

E-Mail kann nicht geändert werden

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+
+

Konto-Status

+

Dein aktueller Kontostatus

+
+ + Aktiv + +
+ +
+
+

Rolle

+

Deine Berechtigungsstufe

+
+ + {authStore.user?.role || 'user'} + +
+ +
+
+

Benutzer-ID

+

Deine eindeutige Kennung

+
+ + {authStore.user?.id?.slice(0, 8) || '...'}... + +
+
+
diff --git a/apps/mana/apps/web/src/lib/components/settings/sections/SecuritySection.svelte b/apps/mana/apps/web/src/lib/components/settings/sections/SecuritySection.svelte new file mode 100644 index 000000000..0dfc18f96 --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/sections/SecuritySection.svelte @@ -0,0 +1,80 @@ + + + + authStore.registerPasskey(name)} + onDelete={(id) => authStore.deletePasskey(id)} + onRename={(id, name) => authStore.renamePasskey(id, name)} + onRefresh={async () => { + passkeys = await authStore.listPasskeys(); + }} + primaryColor="#6366f1" + /> + + + + authStore.revokeSession(id)} + onRefresh={async () => { + sessionsLoading = true; + sessions = await authStore.listSessions(); + sessionsLoading = false; + }} + primaryColor="#6366f1" + /> + + + + authStore.enableTwoFactor(password)} + onDisable={(password) => authStore.disableTwoFactor(password)} + onGenerateBackupCodes={(password) => authStore.generateBackupCodes(password)} + primaryColor="#6366f1" + /> + + + + { + securityEventsLoading = true; + securityEvents = await authStore.getSecurityEvents(); + securityEventsLoading = false; + }} + primaryColor="#6366f1" + /> + diff --git a/apps/mana/apps/web/src/routes/(app)/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/+layout.svelte index 5d4bf43f3..e8608b154 100644 --- a/apps/mana/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+layout.svelte @@ -184,10 +184,10 @@ updateLlmSettings({ allowedTiers: next }); } - const TIER_TOGGLE_LIST: Array<{ tier: LlmTier; shortLabel: string }> = [ - { tier: 'browser', shortLabel: 'Browser (Gemma 4)' }, - { tier: 'mana-server', shortLabel: 'Server (Gemma 4)' }, - { tier: 'cloud', shortLabel: 'Cloud (Gemini)' }, + const TIER_TOGGLE_LIST: Array<{ tier: LlmTier; shortLabel: string; icon: string }> = [ + { tier: 'browser', shortLabel: 'Browser (Gemma 4)', icon: 'cpu' }, + { tier: 'mana-server', shortLabel: 'Server (Gemma 4)', icon: 'server' }, + { tier: 'cloud', shortLabel: 'Cloud (Gemini)', icon: 'cloud' }, ]; let aiTierItems = $derived([ @@ -195,6 +195,7 @@ ...TIER_TOGGLE_LIST.filter((t) => t.tier !== 'browser' || webgpuSupported).map((t) => ({ id: `ai-tier-${t.tier}`, label: t.shortLabel, + icon: t.icon, active: llmSettings.allowedTiers.includes(t.tier), onClick: () => toggleAiTier(t.tier), })), @@ -209,6 +210,7 @@ : localLlmStatus.current.state === 'downloading' ? `Lade… ${((localLlmStatus.current as { progress: number }).progress * 100).toFixed(0)}%` : 'Modell laden (~500 MB)', + icon: localLlmStatus.current.state === 'ready' ? 'check' : 'download', disabled: localLlmStatus.current.state === 'ready', onClick: localLlmStatus.current.state !== 'ready' ? () => void loadLocalLlm() : undefined, @@ -221,7 +223,7 @@ id: 'ai-settings', label: 'KI-Einstellungen', icon: 'settings', - onClick: () => goto('/settings'), + onClick: () => goto('/settings#ai-options'), }, ]); @@ -238,6 +240,18 @@ return first ? first.shortLabel.split(' (')[0] : 'KI'; }); + let currentAiTierIcon = $derived.by(() => { + const active = llmSettings.allowedTiers; + if (active.length === 0) return 'power'; + const sorted = [...active].sort( + (a, b) => + TIER_TOGGLE_LIST.findIndex((t) => t.tier === a) - + TIER_TOGGLE_LIST.findIndex((t) => t.tier === b) + ); + const first = TIER_TOGGLE_LIST.find((t) => t.tier === sorted[0]); + return first ? first.icon : 'cpu'; + }); + // ── Sync status dropdown ──────────────────────────────── let syncStatusItems = $derived.by(() => { const items: import('@mana/shared-ui').PillDropdownItem[] = []; @@ -816,6 +830,7 @@ showAiTierSelector={true} {aiTierItems} {currentAiTierLabel} + {currentAiTierIcon} showSyncStatus={authStore.isAuthenticated} {syncStatusItems} {currentSyncLabel} diff --git a/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte b/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte index e6150ad52..f28878edf 100644 --- a/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte @@ -1,433 +1,78 @@ -
+
- {#if loading} -
-
+
+ (activeCategory = id)} onJump={jumpTo} /> + +
+ {#if activeCategory === 'profile'} + + {:else if activeCategory === 'general'} + + {:else if activeCategory === 'ai'} + + {:else if activeCategory === 'security'} + + {:else if activeCategory === 'credits'} + + {:else if activeCategory === 'data'} + + {/if}
- {:else} -
- - -
-
-
- -
-
-

Profil

-

Deine persönlichen Informationen

-
-
+
- {#if profileSuccess} -
- Profil erfolgreich aktualisiert! -
- {/if} - - {#if profileError} -
- {profileError} -
- {/if} - -
-
- - -

E-Mail kann nicht geändert werden

-
- -
-
- - -
- -
- - -
-
- - -
-
- - - - - - - -
-
-
-
- ☁️ -
-
-

Cloud Sync

-

- Synchronisiere deine Daten über alle Geräte -

-
-
- - Einstellungen - -
-
-
- - - -
-
-
-
- -
-
-

Credits

-

Dein Guthaben für Mana Apps

-
-
- Alle Details -
- -
-
-

Verfügbar

-

- {creditBalance ? formatCredits(creditBalance.balance) : '...'} -

-
-
-

Gratis heute

-

- {creditBalance - ? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}` - : '...'} -

-
-
-

Gesamt verbraucht

-

- {creditBalance ? formatCredits(creditBalance.totalSpent) : '...'} -

-
-
- - -
-
- - - -
-
-
- -
-
-

Konto

-

Konto- und Sicherheitsinformationen

-
-
- -
-
-
-

Konto-Status

-

Dein aktueller Kontostatus

-
- - Aktiv - -
- -
-
-

Rolle

-

Deine Berechtigungsstufe

-
- - {authStore.user?.role || 'user'} - -
- -
-
-

Benutzer-ID

-

Deine eindeutige Kennung

-
- - {authStore.user?.id?.slice(0, 8) || '...'}... - -
-
-
-
- - - -
- authStore.registerPasskey(name)} - onDelete={(id) => authStore.deletePasskey(id)} - onRename={(id, name) => authStore.renamePasskey(id, name)} - onRefresh={async () => { - passkeys = await authStore.listPasskeys(); - }} - primaryColor="#6366f1" - /> -
-
- - - -
- authStore.revokeSession(id)} - onRefresh={async () => { - sessionsLoading = true; - sessions = await authStore.listSessions(); - sessionsLoading = false; - }} - primaryColor="#6366f1" - /> -
-
- - - -
- authStore.enableTwoFactor(password)} - onDisable={(password) => authStore.disableTwoFactor(password)} - onGenerateBackupCodes={(password) => authStore.generateBackupCodes(password)} - primaryColor="#6366f1" - /> -
-
- - - -
- { - securityEventsLoading = true; - securityEvents = await authStore.getSecurityEvents(); - securityEventsLoading = false; - }} - primaryColor="#6366f1" - /> -
-
- - - - - - - - -
-
-
- -
-
-

Meine Daten (DSGVO)

-

Datenschutz und Datenexport

-
-
- -
-
-
-

Daten ansehen & exportieren

-

- Sieh alle deine gespeicherten Daten ein und exportiere sie als JSON -

-
- - Meine Daten - - -
- -
-
-
-

Konto loschen

-

- Das Loschen deines Kontos kann nicht ruckgangig gemacht werden. -

-
- - Verwalten - -
-
-
-
-
-
- -

v{APP_VERSION}

- {/if} +

v{APP_VERSION}

diff --git a/packages/shared-ui/src/navigation/PillDropdown.svelte b/packages/shared-ui/src/navigation/PillDropdown.svelte index 693a6060d..4dfd892ce 100644 --- a/packages/shared-ui/src/navigation/PillDropdown.svelte +++ b/packages/shared-ui/src/navigation/PillDropdown.svelte @@ -128,6 +128,14 @@ help: 'M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm2-1.645A3.502 3.502 0 0012 6.5 3.501 3.501 0 008.645 9h2.012A1.5 1.5 0 0112 8.5c.828 0 1.5.672 1.5 1.5 0 .828-.672 1.5-1.5 1.5a1 1 0 00-1 1V14h2v-.645z', // Mana icon (water drop) mana: 'M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z', + // Compute / AI tier icons + cpu: 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z', + server: + 'M5 12V7a2 2 0 012-2h10a2 2 0 012 2v5M5 12h14M5 12v5a2 2 0 002 2h10a2 2 0 002-2v-5M9 8h.01M9 16h.01', + cloud: + 'M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z', + power: 'M12 3v9m6.364-6.364a9 9 0 11-12.728 0', + download: 'M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5 5-5M12 15V3', }; function getIcon(iconName: string) { diff --git a/packages/shared-ui/src/navigation/PillNavigation.svelte b/packages/shared-ui/src/navigation/PillNavigation.svelte index f7eb2e037..0920b6fc1 100644 --- a/packages/shared-ui/src/navigation/PillNavigation.svelte +++ b/packages/shared-ui/src/navigation/PillNavigation.svelte @@ -247,6 +247,8 @@ aiTierItems?: PillDropdownItem[]; /** Current AI tier label, e.g. "Browser" or "Server" */ currentAiTierLabel?: string; + /** Current AI tier icon name (passed to the dropdown trigger) */ + currentAiTierIcon?: string; /** Show sync status dropdown */ showSyncStatus?: boolean; /** Sync status dropdown items */ @@ -348,6 +350,7 @@ showAiTierSelector = false, aiTierItems = [], currentAiTierLabel = 'KI', + currentAiTierIcon = 'cpu', showSyncStatus = false, syncStatusItems = [], currentSyncLabel = 'Sync', @@ -675,7 +678,7 @@ items={aiTierItems} direction={dropdownDirection} label={currentAiTierLabel} - icon="cpu" + icon={currentAiTierIcon} /> {/if}