refactor(mail): reorganize routes into (app) layout group

Move all authenticated routes into (app) layout group for better
code organization and layout management. Add missing stores.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-05 04:20:06 +01:00
parent ebec369a57
commit 2f7450b5af
11 changed files with 319 additions and 222 deletions

View file

@ -0,0 +1,4 @@
import { writable } from 'svelte/store';
export const isSidebarMode = writable(false);
export const isNavCollapsed = writable(false);

View file

@ -0,0 +1,6 @@
import { createTheme, type ThemeStore } from '@manacore/shared-theme';
export const theme: ThemeStore = createTheme({
storagePrefix: 'mail',
variants: ['default', 'ocean', 'blue', 'purple', 'green', 'orange'],
});

View file

@ -0,0 +1,51 @@
/**
* User Settings Store for Mail
* Manages user preferences and settings
*/
interface UserSettings {
nav: {
desktopPosition: 'left' | 'center' | 'right';
};
}
const defaultSettings: UserSettings = {
nav: {
desktopPosition: 'center',
},
};
let settings = $state<UserSettings>({ ...defaultSettings });
let isLoaded = $state(false);
export const userSettings = {
get nav() {
return settings.nav;
},
get isLoaded() {
return isLoaded;
},
async load() {
if (typeof window === 'undefined') return;
// Load from localStorage
const saved = localStorage.getItem('mail-user-settings');
if (saved) {
try {
const parsed = JSON.parse(saved);
settings = { ...defaultSettings, ...parsed };
} catch {
// Ignore parse errors
}
}
isLoaded = true;
},
update(updates: Partial<UserSettings>) {
settings = { ...settings, ...updates };
if (typeof localStorage !== 'undefined') {
localStorage.setItem('mail-user-settings', JSON.stringify(settings));
}
},
};

View file

@ -0,0 +1,249 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems } from '@manacore/shared-branding';
import { setLocale, supportedLocales } from '$lib/i18n';
import { authStore } from '$lib/stores/auth.svelte';
import { accountsStore } from '$lib/stores/accounts.svelte';
import { foldersStore } from '$lib/stores/folders.svelte';
import { theme } from '$lib/stores/theme';
import {
isSidebarMode as sidebarModeStore,
isNavCollapsed as collapsedStore,
} from '$lib/stores/navigation';
import { userSettings } from '$lib/stores/user-settings.svelte';
let { children } = $props();
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
// App switcher items
const appItems = getPillAppItems('mail');
// Use theme store's isDark directly
let isDark = $derived(theme.isDark);
// Theme variant dropdown items
let themeVariantItems = $derived<PillDropdownItem[]>([
...theme.variants.map((variant) => ({
id: variant,
label: THEME_DEFINITIONS[variant].label,
icon: THEME_DEFINITIONS[variant].icon,
onClick: () => theme.setVariant(variant),
active: theme.variant === variant,
})),
{
id: 'all-themes',
label: 'Alle Themes',
icon: 'palette',
onClick: () => goto('/themes'),
active: false,
},
]);
// Current theme variant label
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
// Language selector items
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
// User email for user dropdown
let userEmail = $derived(authStore.user?.email || 'Menü');
// Navigation items for Mail
const navItems: PillNavItem[] = [
{ href: '/', label: 'Inbox', icon: 'inbox' },
{ href: '/sent', label: 'Gesendet', icon: 'send' },
{ href: '/drafts', label: 'Entwürfe', icon: 'file' },
{ href: '/starred', label: 'Markiert', icon: 'star' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{ href: '/feedback', label: 'Feedback', icon: 'chat' },
];
// Navigation shortcuts (Ctrl+1-6)
const navRoutes = navItems.map((item) => item.href);
function handleKeydown(event: KeyboardEvent) {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
const num = parseInt(event.key);
if (num >= 1 && num <= navRoutes.length) {
event.preventDefault();
const route = navRoutes[num - 1];
if (route) {
goto(route);
}
}
}
}
function handleModeChange(isSidebar: boolean) {
isSidebarMode = isSidebar;
sidebarModeStore.set(isSidebar);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('mail-nav-sidebar', String(isSidebar));
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
collapsedStore.set(collapsed);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('mail-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
theme.toggleMode();
}
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
theme.setMode(mode);
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
onMount(async () => {
// Redirect to login if not authenticated
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
// Load user settings
await userSettings.load();
// Load data
await accountsStore.fetchAccounts();
if (accountsStore.selectedAccountId) {
await foldersStore.fetchFolders(accountsStore.selectedAccountId);
}
// Initialize sidebar mode from localStorage
const savedSidebar = localStorage.getItem('mail-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
sidebarModeStore.set(true);
}
// Initialize collapsed state from localStorage
const savedCollapsed = localStorage.getItem('mail-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
collapsedStore.set(true);
}
});
</script>
<svelte:window onkeydown={handleKeydown} />
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Mail"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
desktopPosition={userSettings.nav.desktopPosition}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#6366f1"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
/>
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
>
<div class="content-wrapper">
{@render children()}
</div>
</main>
</div>
<style>
.layout-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
transition: all 300ms ease;
position: relative;
z-index: 0;
}
.main-content.floating-mode {
padding-top: 70px;
}
.main-content.sidebar-mode {
padding-left: 180px;
}
.content-wrapper {
max-width: 100%;
margin-left: auto;
margin-right: auto;
padding: 1rem;
position: relative;
z-index: 0;
}
@media (min-width: 640px) {
.content-wrapper {
padding: 1.5rem;
}
}
@media (min-width: 1024px) {
.content-wrapper {
padding: 2rem;
}
}
</style>

View file

@ -1,249 +1,36 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { PillNavigation } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { THEME_DEFINITIONS, type ThemeVariant } from '@manacore/shared-theme';
import { getPillAppItems } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import { accountsStore } from '$lib/stores/accounts.svelte';
import { foldersStore } from '$lib/stores/folders.svelte';
import '$lib/i18n';
import '../app.css';
import '$lib/i18n';
import { onMount } from 'svelte';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
let { children } = $props();
let loading = $state(true);
let isDark = $state(false);
let themeMode = $state<'light' | 'dark' | 'system'>('system');
let themeVariant = $state<ThemeVariant>('ocean');
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
// App switcher items
const appItems = getPillAppItems('mail');
// Theme variant dropdown items
let themeVariantItems = $derived<PillDropdownItem[]>(
Object.entries(THEME_DEFINITIONS).map(([key, def]) => ({
id: key,
label: def.label,
icon: def.icon,
onClick: () => setThemeVariant(key as ThemeVariant),
active: themeVariant === key,
}))
);
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[themeVariant]?.label || 'Ocean');
// User email for user dropdown
let userEmail = $derived(authStore.user?.email || 'Menu');
// Check if current route is an auth route
let isAuthRoute = $derived(
$page.url.pathname.startsWith('/login') ||
$page.url.pathname.startsWith('/register') ||
$page.url.pathname.startsWith('/forgot-password')
);
// Navigation items for Mail
const navItems: PillNavItem[] = [
{ href: '/', label: 'Inbox', icon: 'inbox' },
{ href: '/sent', label: 'Sent', icon: 'send' },
{ href: '/drafts', label: 'Drafts', icon: 'file' },
{ href: '/starred', label: 'Starred', icon: 'star' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
];
function handleModeChange(isSidebar: boolean) {
isSidebarMode = isSidebar;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('mail-nav-sidebar', String(isSidebar));
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('mail-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
isDark = !isDark;
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', isDark);
localStorage.setItem('mail-theme-dark', String(isDark));
}
}
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
themeMode = mode;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('mail-theme-mode', mode);
}
updateTheme();
}
function setThemeVariant(variant: ThemeVariant) {
themeVariant = variant;
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', variant);
localStorage.setItem('mail-theme-variant', variant);
}
}
function updateTheme() {
if (typeof window === 'undefined') return;
let shouldBeDark = false;
if (themeMode === 'dark') {
shouldBeDark = true;
} else if (themeMode === 'system') {
shouldBeDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
isDark = shouldBeDark;
document.documentElement.classList.toggle('dark', isDark);
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
onMount(async () => {
// Initialize theme
const savedMode = localStorage.getItem('mail-theme-mode') as 'light' | 'dark' | 'system' | null;
if (savedMode) themeMode = savedMode;
const savedVariant = localStorage.getItem('mail-theme-variant') as ThemeVariant | null;
if (savedVariant && savedVariant in THEME_DEFINITIONS) {
themeVariant = savedVariant;
document.documentElement.setAttribute('data-theme', savedVariant);
}
updateTheme();
theme.initialize();
// Initialize auth
await authStore.initialize();
// Load data if authenticated
if (authStore.isAuthenticated) {
await accountsStore.fetchAccounts();
if (accountsStore.selectedAccountId) {
await foldersStore.fetchFolders(accountsStore.selectedAccountId);
}
}
// Initialize sidebar mode from localStorage
const savedSidebar = localStorage.getItem('mail-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
}
const savedCollapsed = localStorage.getItem('mail-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
}
loading = false;
});
</script>
{#if isAuthRoute}
{@render children()}
{:else if loading}
{#if loading}
<div class="flex min-h-screen items-center justify-center bg-background">
<div class="text-center">
<div
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
></div>
<p class="text-muted-foreground">Loading...</p>
<p class="text-muted-foreground">Laden...</p>
</div>
</div>
{:else}
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Mail"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
{themeMode}
onThemeModeChange={handleThemeModeChange}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#6366f1"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
profileHref="/profile"
/>
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
>
<div class="content-wrapper">
{@render children()}
</div>
</main>
<div class="min-h-screen bg-background text-foreground">
{@render children()}
</div>
{/if}
<style>
.layout-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
transition: all 300ms ease;
position: relative;
z-index: 0;
}
.main-content.floating-mode {
padding-top: 70px;
}
.main-content.sidebar-mode {
padding-left: 180px;
}
.content-wrapper {
max-width: 100%;
margin-left: auto;
margin-right: auto;
padding: 1rem;
position: relative;
z-index: 0;
}
@media (min-width: 640px) {
.content-wrapper {
padding: 1.5rem;
}
}
@media (min-width: 1024px) {
.content-wrapper {
padding: 2rem;
}
}
</style>