feat(workbench): section deep-links + migrate profile & themes to workbench

Section deep-links: /?app=settings#ai-options now switches the Settings
ListView to the KI tab and scrolls to the ai-options anchor. ListView
reads the URL hash on mount and maps it to a category via the existing
searchIndex anchors. The AI-tier dropdown "KI-Einstellungen" link now
targets /?app=settings#ai-options instead of just /?app=settings.

Profile & Themes workbench consolidation — same pattern as Settings:
- Delete standalone /profile and /themes routes (redundant with the
  workbench apps registered in app-registry)
- Migrate all links: PillNavigation profileHref/themesHref, dashboard
  QuickActionsWidget, +layout theme-switcher "Alle Themes", and the
  scene context-menu "Hintergrund ändern"

Credits stays as a standalone route — no workbench app registered for it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-16 11:52:25 +02:00
parent 334c36a68e
commit 0af1dd7ec6
7 changed files with 24 additions and 198 deletions

View file

@ -22,7 +22,7 @@
descKey: 'dashboard.widgets.quick_actions.feedback_desc',
},
{
href: '/profile',
href: '/?app=profile',
icon: '👤',
labelKey: 'dashboard.widgets.quick_actions.profile',
descKey: 'dashboard.widgets.quick_actions.profile_desc',

View file

@ -184,7 +184,7 @@ export function useAiTierItems() {
id: 'ai-settings',
label: 'KI-Einstellungen',
icon: 'settings',
onClick: () => goto('/?app=settings'),
onClick: () => goto('/?app=settings#ai-options'),
},
]);

View file

@ -3,10 +3,15 @@
credits, data). Profile and Themes live in their own workbench apps.
-->
<script lang="ts">
import { tick } from 'svelte';
import { onMount, tick } from 'svelte';
import { APP_VERSION } from '$lib/version';
import SettingsSidebar from '$lib/components/settings/SettingsSidebar.svelte';
import type { CategoryId, SearchEntry } from '$lib/components/settings/searchIndex';
import {
categories,
type CategoryId,
type SearchEntry,
} from '$lib/components/settings/searchIndex';
import GeneralSection from '$lib/components/settings/sections/GeneralSection.svelte';
import AiSection from '$lib/components/settings/sections/AiSection.svelte';
import SecuritySection from '$lib/components/settings/sections/SecuritySection.svelte';
@ -15,6 +20,17 @@
let activeCategory = $state<CategoryId>('general');
onMount(() => {
const hash = window.location.hash?.slice(1);
if (!hash) return;
const cat = categories.find((c) => c.anchors.includes(hash));
if (cat) activeCategory = cat.id;
void tick().then(() => {
const el = document.getElementById(hash);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
function jumpTo(entry: SearchEntry) {
activeCategory = entry.category;
void tick().then(() => {

View file

@ -166,7 +166,7 @@
id: 'all-themes',
label: $_('nav.all_themes'),
icon: 'palette',
onClick: () => goto('/themes'),
onClick: () => goto('/?app=themes'),
active: false,
},
]);
@ -975,10 +975,10 @@
{appItems}
{userEmail}
manaHref="/mana"
profileHref="/profile"
profileHref="/?app=profile"
spiralHref="/spiral"
creditsHref="/credits"
themesHref="/themes"
themesHref="/?app=themes"
helpHref="/help"
allAppsHref="/apps"
{spotlightActions}

View file

@ -302,7 +302,7 @@
id: 'wallpaper',
label: 'Hintergrund ändern',
icon: Image,
action: () => goto('/themes'),
action: () => goto('/?app=themes'),
},
];
if (scenes.length > 1) {

View file

@ -1,162 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ProfilePage, PageHeader } from '@mana/shared-ui';
import type { UserProfile, ProfileActions } from '@mana/shared-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
import { profileService, type UserProfile as ApiUserProfile } from '$lib/api/profile';
import {
EditProfileModal,
ChangePasswordModal,
DeleteAccountModal,
} from '$lib/components/profile';
// Profile data from API
let apiProfile = $state<ApiUserProfile | null>(null);
let loading = $state(true);
// Modal states
let showEditModal = $state(false);
let showPasswordModal = $state(false);
let showDeleteModal = $state(false);
// Toast notification
let toastMessage = $state<string | null>(null);
onMount(async () => {
await loadProfile();
});
async function loadProfile() {
try {
apiProfile = await profileService.getProfile();
} catch (e) {
console.error('Failed to load profile:', e);
} finally {
loading = false;
}
}
// Map auth store user to UserProfile (use API profile when available)
let userProfile = $derived<UserProfile>({
id: apiProfile?.id || authStore.user?.id || '',
email: apiProfile?.email || authStore.user?.email || '',
displayName: apiProfile?.name || undefined,
role: apiProfile?.role || authStore.user?.role,
createdAt: apiProfile?.createdAt,
});
// Profile actions
const actions: ProfileActions = {
onEditProfile: () => {
showEditModal = true;
},
onChangePassword: () => {
showPasswordModal = true;
},
onLogout: async () => {
await authStore.signOut();
goto('/login');
},
onDeleteAccount: () => {
showDeleteModal = true;
},
};
function handleProfileUpdate(user: ApiUserProfile) {
apiProfile = user;
showToast('Profil erfolgreich aktualisiert');
}
function handlePasswordChange() {
showToast('Passwort erfolgreich geändert');
}
async function handleAccountDeleted() {
showToast('Konto wird gelöscht...');
await authStore.signOut();
goto('/login');
}
function showToast(message: string) {
toastMessage = message;
setTimeout(() => {
toastMessage = null;
}, 3000);
}
</script>
<PageHeader title="Profil" backHref="/" sticky />
{#if loading}
<div class="flex items-center justify-center py-12">
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
></div>
</div>
{:else}
<ProfilePage
user={userProfile}
appName="Mana"
{actions}
pageTitle="Profil"
accountInfoTitle="Konto-Informationen"
actionsTitle="Aktionen"
emailLabel="E-Mail"
nameLabel="Name"
memberSinceLabel="Mitglied seit"
lastLoginLabel="Letzter Login"
roleLabel="Rolle"
editProfileLabel="Profil bearbeiten"
changePasswordLabel="Passwort ändern"
logoutLabel="Abmelden"
deleteAccountLabel="Konto löschen"
deleteAccountWarning="Diese Aktion kann nicht rückgängig gemacht werden."
/>
{/if}
<!-- Modals -->
<EditProfileModal
show={showEditModal}
user={apiProfile}
onClose={() => (showEditModal = false)}
onSuccess={handleProfileUpdate}
/>
<ChangePasswordModal
show={showPasswordModal}
onClose={() => (showPasswordModal = false)}
onSuccess={handlePasswordChange}
/>
<DeleteAccountModal
show={showDeleteModal}
userEmail={apiProfile?.email || authStore.user?.email || ''}
onClose={() => (showDeleteModal = false)}
onSuccess={handleAccountDeleted}
/>
<!-- Toast Notification -->
{#if toastMessage}
<div
class="fixed bottom-4 right-4 z-50 px-4 py-3 bg-green-600 text-white rounded-lg shadow-lg animate-fade-in"
>
{toastMessage}
</div>
{/if}
<style>
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.2s ease-out;
}
</style>

View file

@ -1,28 +0,0 @@
<script lang="ts">
import { ThemePage } from '@mana/shared-theme-ui';
import { PageHeader } from '@mana/shared-ui';
import { theme } from '$lib/stores/theme';
import { wallpaperStore } from '$lib/stores/wallpaper.svelte';
import WallpaperPicker from '$lib/components/wallpaper/WallpaperPicker.svelte';
</script>
<svelte:head>
<title>Themes | Mana</title>
</svelte:head>
<PageHeader title="Themes" backHref="/" sticky />
<ThemePage
currentVariant={theme.variant}
onSelectTheme={(v) => theme.setVariant(v)}
showModeSelector={true}
currentMode={theme.mode}
onModeChange={(m) => theme.setMode(m)}
showBackButton={false}
transparent={wallpaperStore.hasWallpaper}
>
<section class="mt-8 pt-8 border-t border-border">
<h2 class="text-sm font-medium text-muted-foreground mb-4">Hintergrund</h2>
<WallpaperPicker />
</section>
</ThemePage>