diff --git a/apps/calendar/apps/web/src/routes/+layout.svelte b/apps/calendar/apps/web/src/routes/+layout.svelte index 691524162..d964f4348 100644 --- a/apps/calendar/apps/web/src/routes/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/+layout.svelte @@ -148,6 +148,12 @@ if (authStore.isAuthenticated) { await calendarsStore.fetchCalendars(); await userSettings.load(); + + // Redirect to start page if on root and a custom start page is set + const currentPath = window.location.pathname; + if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') { + goto(userSettings.startPage, { replaceState: true }); + } } // Initialize sidebar mode from localStorage @@ -225,7 +231,10 @@ class:sidebar-mode={isSidebarMode && !isCollapsed} class:floating-mode={!isSidebarMode && !isCollapsed} > -
+
{@render children()}
diff --git a/apps/chat/apps/web/src/routes/(protected)/+layout.svelte b/apps/chat/apps/web/src/routes/(protected)/+layout.svelte index b910e07a4..320275e76 100644 --- a/apps/chat/apps/web/src/routes/(protected)/+layout.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/+layout.svelte @@ -160,6 +160,12 @@ // Load user settings await userSettings.load(); + // Redirect to start page if on /chat and a custom start page is set + const currentPath = window.location.pathname; + if (currentPath === '/chat' && userSettings.startPage && userSettings.startPage !== '/chat') { + goto(userSettings.startPage, { replaceState: true }); + } + isChecking = false; }); diff --git a/apps/clock/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/clock/apps/web/src/lib/stores/user-settings.svelte.ts new file mode 100644 index 000000000..3b9727cdc --- /dev/null +++ b/apps/clock/apps/web/src/lib/stores/user-settings.svelte.ts @@ -0,0 +1,19 @@ +/** + * User Settings Store for Clock + * + * 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: 'clock', + authUrl: MANA_AUTH_URL, + getAccessToken: () => authStore.getAccessToken(), +}); diff --git a/apps/clock/apps/web/src/routes/settings/+page.svelte b/apps/clock/apps/web/src/routes/settings/+page.svelte index a1403bd24..6da6f40fe 100644 --- a/apps/clock/apps/web/src/routes/settings/+page.svelte +++ b/apps/clock/apps/web/src/routes/settings/+page.svelte @@ -1,8 +1,10 @@
@@ -160,4 +167,17 @@ Töne können für einzelne Wecker und Timer in deren Einstellungen angepasst werden.

+ + +
diff --git a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte index 4bd318707..f9998bf9f 100644 --- a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte @@ -148,6 +148,16 @@ // Load user settings from server if (authStore.isAuthenticated) { await userSettings.load(); + + // Redirect to start page if on /dashboard and a custom start page is set + const currentPath = window.location.pathname; + if ( + currentPath === '/dashboard' && + userSettings.startPage && + userSettings.startPage !== '/dashboard' + ) { + goto(userSettings.startPage, { replaceState: true }); + } } loading = false; diff --git a/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte b/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte index 97fe87661..bed7b8437 100644 --- a/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte @@ -139,6 +139,12 @@ // Load user settings await userSettings.load(); + // Redirect to start page if on root and a custom start page is set + const currentPath = window.location.pathname; + if (currentPath === '/decks' && userSettings.startPage && userSettings.startPage !== '/decks') { + goto(userSettings.startPage, { replaceState: true }); + } + // Initialize sidebar mode from localStorage const savedSidebar = localStorage.getItem('manadeck-nav-sidebar'); if (savedSidebar === 'true') { diff --git a/apps/picture/apps/web/src/routes/app/+layout.svelte b/apps/picture/apps/web/src/routes/app/+layout.svelte index 8f3c884a1..e19179eeb 100644 --- a/apps/picture/apps/web/src/routes/app/+layout.svelte +++ b/apps/picture/apps/web/src/routes/app/+layout.svelte @@ -68,7 +68,22 @@ // Load user settings when authenticated $effect(() => { if (authStore.initialized && authStore.user) { - userSettings.load(); + userSettings.load().then(() => { + // Redirect to start page if on /app and a custom start page is set + const currentPath = window.location.pathname; + if ( + currentPath === '/app' && + userSettings.startPage && + userSettings.startPage !== '/' && + userSettings.startPage !== '/app' + ) { + // Prepend /app if the start page doesn't include it + const targetPath = userSettings.startPage.startsWith('/app') + ? userSettings.startPage + : `/app${userSettings.startPage}`; + goto(targetPath, { replaceState: true }); + } + }); } }); diff --git a/apps/presi/apps/web/src/routes/+layout.svelte b/apps/presi/apps/web/src/routes/+layout.svelte index e2a5d3db7..549166b6b 100644 --- a/apps/presi/apps/web/src/routes/+layout.svelte +++ b/apps/presi/apps/web/src/routes/+layout.svelte @@ -136,6 +136,12 @@ // Load user settings await userSettings.load(); + // Redirect to start page if on root and a custom start page is set + const currentPath = window.location.pathname; + if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') { + goto(userSettings.startPage, { replaceState: true }); + } + // Initialize theme const cleanup = theme.initialize(); diff --git a/apps/todo/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/todo/apps/web/src/lib/stores/user-settings.svelte.ts new file mode 100644 index 000000000..18af057b7 --- /dev/null +++ b/apps/todo/apps/web/src/lib/stores/user-settings.svelte.ts @@ -0,0 +1,10 @@ +import { createUserSettingsStore } from '@manacore/shared-theme'; +import { authStore } from './auth.svelte'; + +const MANA_AUTH_URL = import.meta.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; + +export const userSettings = createUserSettingsStore({ + appId: 'todo', + authUrl: MANA_AUTH_URL, + getAccessToken: () => authStore.getAccessToken(), +}); diff --git a/apps/todo/apps/web/src/routes/+layout.svelte b/apps/todo/apps/web/src/routes/+layout.svelte index edf2bb891..a19fb25dd 100644 --- a/apps/todo/apps/web/src/routes/+layout.svelte +++ b/apps/todo/apps/web/src/routes/+layout.svelte @@ -6,6 +6,7 @@ import { PillNavigation } from '@manacore/shared-ui'; import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui'; import { authStore } from '$lib/stores/auth.svelte'; + import { userSettings } from '$lib/stores/user-settings.svelte'; import { projectsStore } from '$lib/stores/projects.svelte'; import { labelsStore } from '$lib/stores/labels.svelte'; import { theme } from '$lib/stores/theme'; @@ -137,7 +138,17 @@ // Load data if authenticated if (authStore.isAuthenticated) { - await Promise.all([projectsStore.fetchProjects(), labelsStore.fetchLabels()]); + await Promise.all([ + projectsStore.fetchProjects(), + labelsStore.fetchLabels(), + userSettings.load(), + ]); + + // Redirect to start page if on root and a custom start page is set + const currentPath = window.location.pathname; + if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') { + goto(userSettings.startPage, { replaceState: true }); + } } // Initialize sidebar mode from localStorage @@ -183,7 +194,7 @@ onModeChange={handleModeChange} {isCollapsed} onCollapsedChange={handleCollapsedChange} - desktopPosition="bottom" + desktopPosition={userSettings.nav.desktopPosition} showThemeToggle={true} showThemeVariants={true} {themeVariantItems} diff --git a/apps/zitare/apps/web/src/routes/+layout.svelte b/apps/zitare/apps/web/src/routes/+layout.svelte index dcd0251cb..4a33a368f 100644 --- a/apps/zitare/apps/web/src/routes/+layout.svelte +++ b/apps/zitare/apps/web/src/routes/+layout.svelte @@ -145,6 +145,12 @@ // Load user settings if authenticated if (authStore.isAuthenticated) { await userSettings.load(); + + // Redirect to start page if on root and a custom start page is set + const currentPath = window.location.pathname; + if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') { + goto(userSettings.startPage, { replaceState: true }); + } } // Initialize sidebar mode from localStorage diff --git a/packages/shared-theme/src/app-routes.ts b/packages/shared-theme/src/app-routes.ts new file mode 100644 index 000000000..dcfa573c1 --- /dev/null +++ b/packages/shared-theme/src/app-routes.ts @@ -0,0 +1,201 @@ +/** + * App Routes Configuration + * + * Defines available start pages for each app in the ecosystem. + * Used by the start page selector in global settings. + */ + +/** + * Route definition with i18n label + */ +export interface AppRoute { + /** Route path (e.g., '/stopwatch') */ + path: string; + /** i18n key for the label (e.g., 'nav.stopwatch') */ + labelKey: string; + /** Optional icon name */ + icon?: string; +} + +/** + * App route configuration + */ +export interface AppRouteConfig { + /** App identifier */ + appId: string; + /** Default start route (used when no preference set) */ + defaultRoute: string; + /** Available routes that can be set as start page */ + availableRoutes: AppRoute[]; +} + +/** + * Route configurations for all apps + */ +export const APP_ROUTES: Record = { + clock: { + appId: 'clock', + defaultRoute: '/', + availableRoutes: [ + { path: '/', labelKey: 'nav.dashboard', icon: 'home' }, + { path: '/alarms', labelKey: 'nav.alarms', icon: 'alarm' }, + { path: '/timers', labelKey: 'nav.timers', icon: 'timer' }, + { path: '/stopwatch', labelKey: 'nav.stopwatch', icon: 'stopwatch' }, + { path: '/pomodoro', labelKey: 'nav.pomodoro', icon: 'target' }, + { path: '/world-clock', labelKey: 'nav.worldClock', icon: 'globe' }, + { path: '/life', labelKey: 'nav.lifeClock', icon: 'heart' }, + ], + }, + + calendar: { + appId: 'calendar', + defaultRoute: '/', + availableRoutes: [ + { path: '/', labelKey: 'nav.month', icon: 'calendar' }, + { path: '/agenda', labelKey: 'nav.agenda', icon: 'list' }, + ], + }, + + contacts: { + appId: 'contacts', + defaultRoute: '/', + availableRoutes: [ + { path: '/', labelKey: 'nav.contacts', icon: 'users' }, + { path: '/groups', labelKey: 'nav.groups', icon: 'folder' }, + { path: '/favorites', labelKey: 'nav.favorites', icon: 'star' }, + ], + }, + + mail: { + appId: 'mail', + defaultRoute: '/', + availableRoutes: [ + { path: '/', labelKey: 'nav.inbox', icon: 'inbox' }, + { path: '/sent', labelKey: 'nav.sent', icon: 'send' }, + { path: '/drafts', labelKey: 'nav.drafts', icon: 'file' }, + { path: '/starred', labelKey: 'nav.starred', icon: 'star' }, + ], + }, + + todo: { + appId: 'todo', + defaultRoute: '/', + availableRoutes: [ + { path: '/', labelKey: 'nav.all', icon: 'list' }, + { path: '/today', labelKey: 'nav.today', icon: 'calendar' }, + { path: '/upcoming', labelKey: 'nav.upcoming', icon: 'clock' }, + { path: '/completed', labelKey: 'nav.completed', icon: 'check' }, + ], + }, + + storage: { + appId: 'storage', + defaultRoute: '/', + availableRoutes: [ + { path: '/', labelKey: 'nav.home', icon: 'home' }, + { path: '/files', labelKey: 'nav.files', icon: 'folder' }, + { path: '/favorites', labelKey: 'nav.favorites', icon: 'star' }, + { path: '/shared', labelKey: 'nav.shared', icon: 'share' }, + ], + }, + + chat: { + appId: 'chat', + defaultRoute: '/chat', + availableRoutes: [ + { path: '/chat', labelKey: 'nav.chat', icon: 'message' }, + { path: '/spaces', labelKey: 'nav.spaces', icon: 'folder' }, + { path: '/templates', labelKey: 'nav.templates', icon: 'file' }, + { path: '/documents', labelKey: 'nav.documents', icon: 'document' }, + ], + }, + + picture: { + appId: 'picture', + defaultRoute: '/app/gallery', + availableRoutes: [ + { path: '/app/gallery', labelKey: 'nav.gallery', icon: 'image' }, + { path: '/app/generate', labelKey: 'nav.generate', icon: 'sparkle' }, + { path: '/app/board', labelKey: 'nav.board', icon: 'grid' }, + { path: '/app/explore', labelKey: 'nav.explore', icon: 'compass' }, + ], + }, + + manadeck: { + appId: 'manadeck', + defaultRoute: '/decks', + availableRoutes: [ + { path: '/decks', labelKey: 'nav.decks', icon: 'layers' }, + { path: '/explore', labelKey: 'nav.explore', icon: 'compass' }, + { path: '/progress', labelKey: 'nav.progress', icon: 'trending' }, + ], + }, + + zitare: { + appId: 'zitare', + defaultRoute: '/', + availableRoutes: [ + { path: '/', labelKey: 'nav.home', icon: 'home' }, + { path: '/quotes', labelKey: 'nav.quotes', icon: 'quote' }, + { path: '/favorites', labelKey: 'nav.favorites', icon: 'star' }, + { path: '/authors', labelKey: 'nav.authors', icon: 'users' }, + { path: '/lists', labelKey: 'nav.lists', icon: 'list' }, + ], + }, + + presi: { + appId: 'presi', + defaultRoute: '/', + availableRoutes: [{ path: '/', labelKey: 'nav.home', icon: 'home' }], + }, + + manacore: { + appId: 'manacore', + defaultRoute: '/', + availableRoutes: [{ path: '/', labelKey: 'nav.dashboard', icon: 'home' }], + }, +}; + +/** + * Get the start page for a specific app + * @param appId The app identifier + * @param startPages User's start page preferences + * @returns The start page path (user preference or app default) + */ +export function getStartPage(appId: string, startPages: Record = {}): string { + const config = APP_ROUTES[appId]; + if (!config) { + return '/'; + } + + // Check if user has a preference for this app + const userPreference = startPages[appId]; + if (userPreference) { + // Validate that the route is available + const isValid = config.availableRoutes.some((r) => r.path === userPreference); + if (isValid) { + return userPreference; + } + } + + // Return app default + return config.defaultRoute; +} + +/** + * Get available routes for a specific app + * @param appId The app identifier + * @returns Array of available routes or empty array if app not found + */ +export function getAvailableRoutes(appId: string): AppRoute[] { + return APP_ROUTES[appId]?.availableRoutes ?? []; +} + +/** + * Get default route for a specific app + * @param appId The app identifier + * @returns The default route path or '/' if app not found + */ +export function getDefaultRoute(appId: string): string { + return APP_ROUTES[appId]?.defaultRoute ?? '/'; +} diff --git a/packages/shared-theme/src/index.ts b/packages/shared-theme/src/index.ts index cfbe3ac16..1719f7a70 100644 --- a/packages/shared-theme/src/index.ts +++ b/packages/shared-theme/src/index.ts @@ -24,10 +24,14 @@ export type { UserSettingsResponse, UserSettingsStore, UserSettingsStoreConfig, + // General Settings Types + StartPageConfig, + WeekStartDay, + GeneralSettings, } from './types'; // User Settings Constants -export { DEFAULT_GLOBAL_SETTINGS } from './types'; +export { DEFAULT_GLOBAL_SETTINGS, DEFAULT_GENERAL_SETTINGS } from './types'; // Constants export { @@ -89,3 +93,7 @@ export { loadA11yFromStorage, saveA11yToStorage, } from './a11y-utils'; + +// App Routes +export type { AppRoute, AppRouteConfig } from './app-routes'; +export { APP_ROUTES, getStartPage, getAvailableRoutes, getDefaultRoute } from './app-routes'; diff --git a/packages/shared-theme/src/types.ts b/packages/shared-theme/src/types.ts index db804c8f9..bbad90c10 100644 --- a/packages/shared-theme/src/types.ts +++ b/packages/shared-theme/src/types.ts @@ -223,6 +223,29 @@ export interface NavSettings { sidebarCollapsed: boolean; } +/** + * Start page configuration per app + * Keys are app IDs, values are route paths + */ +export type StartPageConfig = Record; + +/** + * Day of week for calendar/week starts + */ +export type WeekStartDay = 'monday' | 'sunday'; + +/** + * General settings (global preferences) + */ +export interface GeneralSettings { + /** Start page per app (e.g., { clock: '/stopwatch', calendar: '/week' }) */ + startPages: StartPageConfig; + /** First day of week */ + weekStartsOn: WeekStartDay; + /** Master toggle for all app sounds */ + soundsEnabled: boolean; +} + /** * Theme settings (synced to server) */ @@ -240,6 +263,8 @@ export interface GlobalSettings { nav: NavSettings; theme: ThemeSettings; locale: string; + /** General preferences (start pages, sounds, etc.) */ + general: GeneralSettings; } /** @@ -258,6 +283,15 @@ export interface UserSettingsResponse { appOverrides: Record; } +/** + * Default general settings + */ +export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = { + startPages: {}, // Empty = use app defaults + weekStartsOn: 'monday', + soundsEnabled: true, +}; + /** * Default global settings */ @@ -265,6 +299,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { nav: { desktopPosition: 'top', sidebarCollapsed: false }, theme: { mode: 'system', colorScheme: 'ocean' }, locale: 'de', + general: DEFAULT_GENERAL_SETTINGS, }; /** @@ -277,6 +312,10 @@ export interface UserSettingsStore { readonly theme: ThemeSettings; /** Current locale */ readonly locale: string; + /** Resolved general settings */ + readonly general: GeneralSettings; + /** Start page for current app (resolved from settings or default) */ + readonly startPage: string; /** Raw global settings */ readonly globalSettings: GlobalSettings; /** Whether current app has an override */ @@ -294,6 +333,10 @@ export interface UserSettingsStore { updateAppOverride: (settings: AppOverride) => Promise; /** Remove app override (revert to global) */ removeAppOverride: () => Promise; + /** Set start page for a specific app */ + setStartPage: (appId: string, path: string) => Promise; + /** Update general settings */ + updateGeneral: (settings: Partial) => Promise; } /** diff --git a/packages/shared-theme/src/user-settings-store.svelte.ts b/packages/shared-theme/src/user-settings-store.svelte.ts index 0f31692a0..7a9ee94f6 100644 --- a/packages/shared-theme/src/user-settings-store.svelte.ts +++ b/packages/shared-theme/src/user-settings-store.svelte.ts @@ -6,9 +6,11 @@ import type { NavSettings, ThemeSettings, UserSettingsResponse, + GeneralSettings, } from './types'; -import { DEFAULT_GLOBAL_SETTINGS } from './types'; +import { DEFAULT_GLOBAL_SETTINGS, DEFAULT_GENERAL_SETTINGS } from './types'; import { isBrowser } from './utils'; +import { getStartPage as getStartPageFromConfig } from './app-routes'; const STORAGE_KEY_PREFIX = 'manacore-user-settings'; @@ -66,6 +68,15 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe // Derived: whether this app has an override const hasAppOverride = $derived(!!appOverrides[appId]); + // Derived: resolved general settings (always from global) + const general = $derived({ + ...DEFAULT_GENERAL_SETTINGS, + ...globalSettings.general, + }); + + // Derived: start page for current app + const startPage = $derived(getStartPageFromConfig(appId, general.startPages)); + /** * Save current settings to localStorage (for offline fallback) */ @@ -111,11 +122,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe /** * Make an API request to the settings endpoint */ - async function apiRequest( - method: string, - path: string, - body?: object - ): Promise { + 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'); @@ -176,6 +183,14 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe nav: { ...globalSettings.nav, ...settings.nav }, theme: { ...globalSettings.theme, ...settings.theme }, locale: settings.locale ?? globalSettings.locale, + general: { + ...globalSettings.general, + ...settings.general, + startPages: { + ...globalSettings.general?.startPages, + ...settings.general?.startPages, + }, + }, }; saveToStorage(); @@ -238,6 +253,35 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe } } + /** + * Update start page for a specific app + */ + async function setStartPage(targetAppId: string, path: string): Promise { + await updateGlobal({ + general: { + startPages: { + [targetAppId]: path, + }, + }, + } as Partial); + } + + /** + * Update general settings + */ + async function updateGeneral(settings: Partial): Promise { + await updateGlobal({ + general: { + ...globalSettings.general, + ...settings, + startPages: { + ...globalSettings.general?.startPages, + ...settings.startPages, + }, + }, + }); + } + /** * Remove app override (revert to global settings) */ @@ -280,6 +324,12 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe get locale() { return locale; }, + get general() { + return general; + }, + get startPage() { + return startPage; + }, get globalSettings() { return globalSettings; }, @@ -297,5 +347,7 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe updateGlobal, updateAppOverride, removeAppOverride, + setStartPage, + updateGeneral, }; } diff --git a/packages/shared-ui/src/settings/GlobalSettingsSection.svelte b/packages/shared-ui/src/settings/GlobalSettingsSection.svelte index 90d21d80d..21abe64f9 100644 --- a/packages/shared-ui/src/settings/GlobalSettingsSection.svelte +++ b/packages/shared-ui/src/settings/GlobalSettingsSection.svelte @@ -1,32 +1,54 @@