diff --git a/.env.development b/.env.development index 92535d6e6..4784d4393 100644 --- a/.env.development +++ b/.env.development @@ -221,6 +221,7 @@ CLOCK_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/clock # ============================================ TODO_BACKEND_PORT=3018 +TODO_BACKEND_URL=http://localhost:3018 TODO_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/todo # ============================================ diff --git a/apps/calendar/apps/web/src/lib/api/todos.ts b/apps/calendar/apps/web/src/lib/api/todos.ts new file mode 100644 index 000000000..fa649d829 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/api/todos.ts @@ -0,0 +1,370 @@ +/** + * Cross-App API Client for Todo Backend + * Allows Calendar app to fetch/manage todos from the Todo service + */ + +import { browser } from '$app/environment'; +import { env } from '$env/dynamic/public'; + +const TODO_API_BASE = env.PUBLIC_TODO_BACKEND_URL || 'http://localhost:3018'; + +// ============================================ +// Types (mirrored from @todo/shared for cross-app use) +// ============================================ + +export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent'; +export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; + +export interface Subtask { + id: string; + title: string; + isCompleted: boolean; + completedAt?: string | null; + order: number; +} + +export interface Label { + id: string; + userId: string; + name: string; + color: string; + createdAt: string; + updatedAt: string; +} + +export interface Project { + id: string; + userId: string; + name: string; + description?: string | null; + color: string; + icon?: string | null; + order: number; + isArchived: boolean; + isDefault: boolean; + createdAt: string; + updatedAt: string; +} + +export interface TaskMetadata { + notes?: string; + attachments?: string[]; + linkedCalendarEventId?: string | null; + storyPoints?: number | null; + effectiveDuration?: { + value: number; + unit: 'minutes' | 'hours' | 'days'; + } | null; + funRating?: number | null; +} + +export interface Task { + id: string; + projectId?: string | null; + userId: string; + parentTaskId?: string | null; + title: string; + description?: string | null; + dueDate?: string | null; + dueTime?: string | null; + startDate?: string | null; + priority: TaskPriority; + status: TaskStatus; + isCompleted: boolean; + completedAt?: string | null; + order: number; + columnId?: string | null; + columnOrder?: number; + recurrenceRule?: string | null; + recurrenceEndDate?: string | null; + lastOccurrence?: string | null; + subtasks?: Subtask[] | null; + metadata?: TaskMetadata | null; + labels?: Label[]; + project?: Project | null; + createdAt: string; + updatedAt: string; +} + +export interface CreateTaskInput { + title: string; + description?: string; + projectId?: string | null; + dueDate?: string | null; + dueTime?: string | null; + priority?: TaskPriority; + labelIds?: string[]; + subtasks?: Omit[]; + recurrenceRule?: string | null; + metadata?: TaskMetadata; +} + +export interface UpdateTaskInput { + title?: string; + description?: string | null; + projectId?: string | null; + dueDate?: string | null; + dueTime?: string | null; + priority?: TaskPriority; + status?: TaskStatus; + isCompleted?: boolean; + subtasks?: Subtask[] | null; + recurrenceRule?: string | null; + metadata?: TaskMetadata | null; + labelIds?: string[]; +} + +export interface TaskQuery { + projectId?: string; + labelId?: string; + priority?: TaskPriority; + status?: TaskStatus; + isCompleted?: boolean; + dueDateFrom?: string; + dueDateTo?: string; + search?: string; + sortBy?: 'dueDate' | 'priority' | 'createdAt' | 'order'; + sortOrder?: 'asc' | 'desc'; + limit?: number; + offset?: number; +} + +// ============================================ +// API Response Types +// ============================================ + +interface TasksResponse { + tasks: Task[]; +} + +interface TaskResponse { + task: Task; +} + +interface ProjectsResponse { + projects: Project[]; +} + +interface LabelsResponse { + labels: Label[]; +} + +// ============================================ +// API Client +// ============================================ + +type FetchOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + body?: unknown; + token?: string; +}; + +async function fetchTodoApi( + endpoint: string, + options: FetchOptions = {} +): Promise<{ data: T | null; error: Error | null }> { + const { method = 'GET', body, token } = options; + + let authToken = token; + if (!authToken && browser) { + authToken = localStorage.getItem('@auth/appToken') || undefined; + } + + try { + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; + } + + const response = await fetch(`${TODO_API_BASE}/api/v1${endpoint}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + return { + data: null, + error: new Error(errorData.message || `Todo API error: ${response.status}`), + }; + } + + // Handle empty responses (204 No Content) + if (response.status === 204) { + return { data: null, error: null }; + } + + const data = await response.json(); + return { data, error: null }; + } catch (error) { + return { + data: null, + error: error instanceof Error ? error : new Error('Failed to connect to Todo service'), + }; + } +} + +// ============================================ +// Helper Functions +// ============================================ + +function buildQueryString(query: TaskQuery): string { + const params = new URLSearchParams(); + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.append(key, String(value)); + } + }); + const queryString = params.toString(); + return queryString ? `?${queryString}` : ''; +} + +// ============================================ +// Task API Functions +// ============================================ + +export async function getTasks( + query: TaskQuery = {} +): Promise<{ data: Task[] | null; error: Error | null }> { + const queryString = buildQueryString(query); + const result = await fetchTodoApi(`/tasks${queryString}`); + return { + data: result.data?.tasks || null, + error: result.error, + }; +} + +export async function getTask(id: string): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}`); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function createTask( + data: CreateTaskInput +): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi('/tasks', { + method: 'POST', + body: data, + }); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function updateTask( + id: string, + data: UpdateTaskInput +): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}`, { + method: 'PUT', + body: data, + }); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function deleteTask(id: string): Promise<{ error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}`, { + method: 'DELETE', + }); + return { error: result.error }; +} + +export async function completeTask( + id: string +): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}/complete`, { + method: 'POST', + }); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function uncompleteTask( + id: string +): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}/uncomplete`, { + method: 'POST', + }); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function getTodayTasks(): Promise<{ data: Task[] | null; error: Error | null }> { + const result = await fetchTodoApi('/tasks/today'); + return { + data: result.data?.tasks || null, + error: result.error, + }; +} + +export async function getUpcomingTasks(): Promise<{ data: Task[] | null; error: Error | null }> { + const result = await fetchTodoApi('/tasks/upcoming'); + return { + data: result.data?.tasks || null, + error: result.error, + }; +} + +// ============================================ +// Project API Functions +// ============================================ + +export async function getProjects(): Promise<{ data: Project[] | null; error: Error | null }> { + const result = await fetchTodoApi('/projects'); + return { + data: result.data?.projects || null, + error: result.error, + }; +} + +// ============================================ +// Label API Functions +// ============================================ + +export async function getLabels(): Promise<{ data: Label[] | null; error: Error | null }> { + const result = await fetchTodoApi('/labels'); + return { + data: result.data?.labels || null, + error: result.error, + }; +} + +// ============================================ +// Priority Colors Helper +// ============================================ + +export const PRIORITY_COLORS: Record = { + urgent: 'hsl(var(--color-danger))', + high: 'hsl(var(--color-warning))', + medium: 'hsl(var(--color-accent))', + low: 'hsl(var(--color-success))', +}; + +export const PRIORITY_LABELS: Record = { + urgent: 'Dringend', + high: 'Wichtig', + medium: 'Normal', + low: 'Später', +}; + +export const PRIORITY_ORDER: Record = { + urgent: 0, + high: 1, + medium: 2, + low: 3, +}; diff --git a/apps/calendar/apps/web/src/lib/components/todo/PriorityBadge.svelte b/apps/calendar/apps/web/src/lib/components/todo/PriorityBadge.svelte new file mode 100644 index 000000000..2ad0693ec --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/PriorityBadge.svelte @@ -0,0 +1,124 @@ + + +{#if variant === 'dot'} + +{:else if variant === 'badge'} + + {#if showLabel} + {label} + {:else} + {priority.charAt(0).toUpperCase()} + {/if} + +{:else if variant === 'pill'} + + + {#if showLabel} + {label} + {/if} + +{/if} + + diff --git a/apps/calendar/apps/web/src/lib/components/todo/QuickAddTodo.svelte b/apps/calendar/apps/web/src/lib/components/todo/QuickAddTodo.svelte new file mode 100644 index 000000000..573e3794a --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/QuickAddTodo.svelte @@ -0,0 +1,226 @@ + + +{#if showButton && !isExpanded} + +{:else} +
+ + + {#if showButton} + + {/if} + + +
+{/if} + + diff --git a/apps/calendar/apps/web/src/lib/components/todo/TodoCheckbox.svelte b/apps/calendar/apps/web/src/lib/components/todo/TodoCheckbox.svelte new file mode 100644 index 000000000..640043528 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/TodoCheckbox.svelte @@ -0,0 +1,130 @@ + + + + + diff --git a/apps/calendar/apps/web/src/lib/components/todo/TodoItem.svelte b/apps/calendar/apps/web/src/lib/components/todo/TodoItem.svelte new file mode 100644 index 000000000..0a24b386d --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/TodoItem.svelte @@ -0,0 +1,287 @@ + + +
+ + +
+
+ {#if showPriority && variant !== 'minimal'} + + {/if} + + {task.title} + + {#if subtaskProgress && variant === 'default'} + + {subtaskProgress.completed}/{subtaskProgress.total} + + {/if} +
+ + {#if variant !== 'minimal'} +
+ {#if showDueDate && dueDateLabel} + + {dueDateLabel} + + {/if} + + {#if showProject && task.project} + + {task.project.name} + + {/if} + + {#if task.labels && task.labels.length > 0 && variant === 'default'} +
+ {#each task.labels.slice(0, 2) as label} + + {label.name} + + {/each} + {#if task.labels.length > 2} + +{task.labels.length - 2} + {/if} +
+ {/if} +
+ {/if} +
+
+ + diff --git a/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts b/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts new file mode 100644 index 000000000..1311d0971 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts @@ -0,0 +1,405 @@ +/** + * Todos Store - Manages todos from Todo-App using Svelte 5 runes + * Cross-app integration with Todo Backend + */ + +import * as api from '$lib/api/todos'; +import type { + Task, + TaskPriority, + CreateTaskInput, + UpdateTaskInput, + TaskQuery, + Project, + Label, +} from '$lib/api/todos'; +import { PRIORITY_ORDER } from '$lib/api/todos'; +import { + format, + parseISO, + isSameDay, + isToday, + isBefore, + startOfDay, + addDays, + isWithinInterval, +} from 'date-fns'; + +// Re-export types for convenience +export type { Task, TaskPriority, CreateTaskInput, UpdateTaskInput, Project, Label }; + +// State +let todos = $state([]); +let projects = $state([]); +let labels = $state([]); +let loading = $state(false); +let error = $state(null); +let loadedRange = $state<{ start: Date; end: Date } | null>(null); +let serviceAvailable = $state(true); + +export const todosStore = { + // ========== Getters ========== + get todos() { + return todos ?? []; + }, + get projects() { + return projects ?? []; + }, + get labels() { + return labels ?? []; + }, + get loading() { + return loading; + }, + get error() { + return error; + }, + get serviceAvailable() { + return serviceAvailable; + }, + + // ========== Derived Getters ========== + + /** + * Get todos for a specific day + */ + getTodosForDay(date: Date): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos.filter((task) => { + if (!task.dueDate || task.isCompleted) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isSameDay(dueDate, date); + }); + }, + + /** + * Get todos within a date range + */ + getTodosInRange(start: Date, end: Date): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos.filter((task) => { + if (!task.dueDate) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isWithinInterval(dueDate, { start, end }); + }); + }, + + /** + * Get today's uncompleted todos + */ + get todaysTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos + .filter((task) => { + if (task.isCompleted) return false; + if (!task.dueDate) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isToday(dueDate); + }) + .sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]); + }, + + /** + * Get overdue todos (due before today, not completed) + */ + get overdueTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + const today = startOfDay(new Date()); + + return currentTodos + .filter((task) => { + if (task.isCompleted) return false; + if (!task.dueDate) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isBefore(startOfDay(dueDate), today); + }) + .sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]); + }, + + /** + * Get upcoming todos (next 7 days, not including today) + */ + get upcomingTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + const tomorrow = startOfDay(addDays(new Date(), 1)); + const weekFromNow = startOfDay(addDays(new Date(), 7)); + + return currentTodos + .filter((task) => { + if (task.isCompleted) return false; + if (!task.dueDate) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isWithinInterval(startOfDay(dueDate), { start: tomorrow, end: weekFromNow }); + }) + .sort((a, b) => { + // First sort by date + const dateA = a.dueDate ? parseISO(a.dueDate as string) : new Date(); + const dateB = b.dueDate ? parseISO(b.dueDate as string) : new Date(); + const dateDiff = dateA.getTime() - dateB.getTime(); + if (dateDiff !== 0) return dateDiff; + // Then by priority + return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]; + }); + }, + + /** + * Get todos without due date + */ + get unscheduledTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos + .filter((task) => !task.isCompleted && !task.dueDate) + .sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]); + }, + + /** + * Get completed todos + */ + get completedTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos.filter((task) => task.isCompleted); + }, + + /** + * Get combined sidebar todos (overdue + today, sorted by priority) + * Limited to show in sidebar + */ + getSidebarTodos(limit = 5): Task[] { + const overdue = this.overdueTodos; + const today = this.todaysTodos; + + // Combine and sort: overdue first, then today, both by priority + const combined = [...overdue, ...today]; + + return combined.slice(0, limit); + }, + + /** + * Get total count of active todos (not completed) + */ + get activeTodosCount(): number { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return 0; + + return currentTodos.filter((task) => !task.isCompleted).length; + }, + + // ========== API Methods ========== + + /** + * Fetch todos for a date range + */ + async fetchTodos(startDate?: Date, endDate?: Date) { + loading = true; + error = null; + + const query: TaskQuery = { + isCompleted: false, + }; + + if (startDate) { + query.dueDateFrom = format(startDate, 'yyyy-MM-dd'); + } + if (endDate) { + query.dueDateTo = format(endDate, 'yyyy-MM-dd'); + } + + const result = await api.getTasks(query); + + if (result.error) { + error = result.error.message; + serviceAvailable = false; + } else { + todos = result.data || []; + serviceAvailable = true; + if (startDate && endDate) { + loadedRange = { start: startDate, end: endDate }; + } + } + + loading = false; + return result; + }, + + /** + * Fetch today's todos (shortcut) + */ + async fetchTodayTodos() { + loading = true; + error = null; + + const result = await api.getTodayTasks(); + + if (result.error) { + error = result.error.message; + serviceAvailable = false; + } else { + // Merge with existing todos (avoid duplicates) + const newTodos = result.data || []; + const existingIds = new Set(todos.map((t) => t.id)); + const uniqueNew = newTodos.filter((t) => !existingIds.has(t.id)); + todos = [...todos, ...uniqueNew]; + serviceAvailable = true; + } + + loading = false; + return result; + }, + + /** + * Fetch upcoming todos (shortcut) + */ + async fetchUpcomingTodos() { + loading = true; + error = null; + + const result = await api.getUpcomingTasks(); + + if (result.error) { + error = result.error.message; + serviceAvailable = false; + } else { + // Merge with existing todos (avoid duplicates) + const newTodos = result.data || []; + const existingIds = new Set(todos.map((t) => t.id)); + const uniqueNew = newTodos.filter((t) => !existingIds.has(t.id)); + todos = [...todos, ...uniqueNew]; + serviceAvailable = true; + } + + loading = false; + return result; + }, + + /** + * Fetch projects + */ + async fetchProjects() { + const result = await api.getProjects(); + + if (!result.error && result.data) { + projects = result.data; + } + + return result; + }, + + /** + * Fetch labels + */ + async fetchLabels() { + const result = await api.getLabels(); + + if (!result.error && result.data) { + labels = result.data; + } + + return result; + }, + + /** + * Create a new todo + */ + async createTodo(data: CreateTaskInput) { + const result = await api.createTask(data); + + if (result.data) { + todos = [...todos, result.data]; + } + + return result; + }, + + /** + * Update a todo + */ + async updateTodo(id: string, data: UpdateTaskInput) { + const result = await api.updateTask(id, data); + + if (result.data) { + todos = todos.map((t) => (t.id === id ? result.data! : t)); + } + + return result; + }, + + /** + * Delete a todo + */ + async deleteTodo(id: string) { + const result = await api.deleteTask(id); + + if (!result.error) { + todos = todos.filter((t) => t.id !== id); + } + + return result; + }, + + /** + * Toggle todo completion + */ + async toggleComplete(id: string) { + const todo = todos.find((t) => t.id === id); + if (!todo) return { data: null, error: new Error('Todo not found') }; + + const result = todo.isCompleted ? await api.uncompleteTask(id) : await api.completeTask(id); + + if (result.data) { + todos = todos.map((t) => (t.id === id ? result.data! : t)); + } + + return result; + }, + + /** + * Get todo by ID + */ + getById(id: string): Task | undefined { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return undefined; + + return currentTodos.find((t) => t.id === id); + }, + + /** + * Get project by ID + */ + getProjectById(id: string): Project | undefined { + const currentProjects = projects ?? []; + if (!Array.isArray(currentProjects)) return undefined; + + return currentProjects.find((p) => p.id === id); + }, + + /** + * Clear todos cache + */ + clear() { + todos = []; + loadedRange = null; + }, + + /** + * Check if Todo service is available + */ + async checkServiceHealth(): Promise { + const result = await api.getTasks({ limit: 1 }); + serviceAvailable = !result.error; + return serviceAvailable; + }, +}; diff --git a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte index 50ab7b55a..62e8f1aab 100644 --- a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte @@ -25,6 +25,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -178,8 +179,8 @@ // User email for user dropdown let userEmail = $derived(authStore.user?.email || 'Menü'); - // Navigation items for Calendar - const navItems: PillNavItem[] = [ + // Base navigation items for Calendar + const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Kalender', icon: 'calendar' }, { href: '/agenda', label: 'Agenda', icon: 'list' }, { href: '/tags', label: 'Tags', icon: 'tag' }, @@ -189,8 +190,13 @@ { href: '/feedback', label: 'Feedback', icon: 'chat' }, ]; - // Navigation shortcuts (Ctrl+1-4) - const navRoutes = navItems.map((item) => item.href); + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('calendar', baseNavItems, userSettings.nav.hiddenNavItems) + ); + + // Navigation shortcuts (Ctrl+1-4) - use base items for consistent shortcuts + const navRoutes = baseNavItems.map((item) => item.href); function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; diff --git a/apps/chat/apps/web/src/routes/(protected)/+layout.svelte b/apps/chat/apps/web/src/routes/(protected)/+layout.svelte index 71202a1b3..275071d82 100644 --- a/apps/chat/apps/web/src/routes/(protected)/+layout.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/+layout.svelte @@ -12,6 +12,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -78,8 +79,8 @@ ); let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale)); - // Navigation items for Chat (settings moved to user dropdown) - const navItems: PillNavItem[] = [ + // Base navigation items for Chat (settings moved to user dropdown) + const baseNavItems: PillNavItem[] = [ { href: '/chat', label: 'Chat', icon: 'home' }, { href: '/templates', label: 'Templates', icon: 'document' }, { href: '/spaces', label: 'Spaces', icon: 'building' }, @@ -88,14 +89,19 @@ { href: '/feedback', label: 'Feedback', icon: 'chat' }, ]; + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('chat', baseNavItems, userSettings.nav.hiddenNavItems) + ); + // User email for user dropdown let userEmail = $derived(authStore.user?.email); // Check if current page is a chat page (needs full-width layout) let isChatPage = $derived($page.url.pathname.startsWith('/chat')); - // Navigation shortcuts (Ctrl+1-5) - const navRoutes = navItems.map((item) => item.href); + // Navigation shortcuts (Ctrl+1-5) - use base items for consistent shortcuts + const navRoutes = baseNavItems.map((item) => item.href); function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; diff --git a/apps/clock/apps/web/src/routes/(app)/+layout.svelte b/apps/clock/apps/web/src/routes/(app)/+layout.svelte index 46e5273ba..d58c91855 100644 --- a/apps/clock/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/clock/apps/web/src/routes/(app)/+layout.svelte @@ -19,6 +19,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -161,8 +162,8 @@ // User email for user dropdown let userEmail = $derived(authStore.user?.email || 'Menü'); - // Navigation items for Clock - const navItems: PillNavItem[] = [ + // Base navigation items for Clock + const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Übersicht', icon: 'home' }, { href: '/alarms', label: 'Wecker', icon: 'bell' }, { href: '/timers', label: 'Timer', icon: 'timer' }, @@ -174,8 +175,13 @@ { href: '/feedback', label: 'Feedback', icon: 'chat' }, ]; - // Navigation shortcuts (Ctrl+1-9) - const navRoutes = navItems.map((item) => item.href); + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('clock', baseNavItems, userSettings.nav.hiddenNavItems) + ); + + // Navigation shortcuts (Ctrl+1-9) - use base items for consistent shortcuts + const navRoutes = baseNavItems.map((item) => item.href); function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; diff --git a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte index 5ac9ef360..0a8871350 100644 --- a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte @@ -20,6 +20,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -106,8 +107,8 @@ // User email for user dropdown (fallback to 'Menü' when not logged in) let userEmail = $derived(authStore.user?.email || 'Menü'); - // Navigation items for Contacts - const navItems: PillNavItem[] = [ + // Base navigation items for Contacts + const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Kontakte', icon: 'users' }, { href: '/tags', label: 'Tags', icon: 'tag' }, { href: '/favorites', label: 'Favoriten', icon: 'heart' }, @@ -118,8 +119,13 @@ { href: '/help', label: 'Hilfe', icon: 'help-circle' }, ]; - // Navigation shortcuts (Ctrl+1-5) - const navRoutes = navItems.map((item) => item.href); + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('contacts', baseNavItems, userSettings.nav.hiddenNavItems) + ); + + // Navigation shortcuts (Ctrl+1-5) - use base items for consistent shortcuts + const navRoutes = baseNavItems.map((item) => item.href); function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; diff --git a/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte b/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte index c4f82f189..a199c8d44 100644 --- a/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte @@ -18,6 +18,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n'; import { getPillAppItems } from '@manacore/shared-branding'; import { setLocale, supportedLocales } from '$lib/i18n'; @@ -33,13 +34,18 @@ // Get theme state let isDark = $derived(theme.isDark); - // Navigation items for ManaDeck (Mana and Profile are in user dropdown) - const navItems: PillNavItem[] = [ + // Base navigation items for ManaDeck (Mana and Profile are in user dropdown) + const baseNavItems: PillNavItem[] = [ { href: '/decks', label: 'Decks', icon: 'archive' }, { href: '/explore', label: 'Explore', icon: 'search' }, { href: '/progress', label: 'Progress', icon: 'chart' }, ]; + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('manadeck', baseNavItems, userSettings.nav.hiddenNavItems) + ); + // Get pinned themes from user settings (extended themes only) let pinnedThemes = $derived( (userSettings.theme?.pinnedThemes || []).filter((t): t is ThemeVariant => diff --git a/apps/picture/apps/web/src/routes/app/+layout.svelte b/apps/picture/apps/web/src/routes/app/+layout.svelte index 99db8a558..447101cca 100644 --- a/apps/picture/apps/web/src/routes/app/+layout.svelte +++ b/apps/picture/apps/web/src/routes/app/+layout.svelte @@ -12,6 +12,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n'; import { getPillAppItems } from '@manacore/shared-branding'; import { setLocale, supportedLocales } from '$lib/i18n'; @@ -93,8 +94,8 @@ } }); - // Navigation items (Mana is in user dropdown via manaHref) - const navItems: PillNavItem[] = [ + // Base navigation items (Mana is in user dropdown via manaHref) + const baseNavItems: PillNavItem[] = [ { href: '/app/gallery', label: 'Galerie', icon: 'home' }, { href: '/app/board', label: 'Moodboards', icon: 'grid' }, { href: '/app/explore', label: 'Entdecken', icon: 'search' }, @@ -104,6 +105,11 @@ { href: '/app/archive', label: 'Archiv', icon: 'archive' }, ]; + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('picture', baseNavItems, userSettings.nav.hiddenNavItems) + ); + // View mode options for tab group const viewModeOptions = [ { id: 'single', icon: 'list', title: 'Liste (1)' }, diff --git a/apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte b/apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte index e7ac1bb11..ad8dc1014 100644 --- a/apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte +++ b/apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte @@ -39,7 +39,7 @@