feat(command-bar): match priority highlight colors to UI

Priority keywords now show their actual UI colors:
- Dringend (urgent): red #ef4444
- Wichtig (high): orange #f97316
- Normal (medium): yellow #eab308
- Später (low): green #22c55e

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-10 15:06:22 +01:00
parent d9626a9d8f
commit aa117c51cd
12 changed files with 551 additions and 92 deletions

View file

@ -59,6 +59,13 @@
</script>
<div class="task-item group" class:completed={task.isCompleted}>
<!-- Drag handle -->
<div class="drag-handle">
<svg class="drag-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
</svg>
</div>
<!-- Priority indicator -->
<div
class="priority-dot"
@ -80,27 +87,9 @@
{task.title}
</span>
<!-- Meta info inline -->
{#if dueDateText() || subtaskProgress() || (task.labels && task.labels.length > 0)}
<!-- Labels and subtasks below title -->
{#if subtaskProgress() || (task.labels && task.labels.length > 0)}
<div class="task-meta">
{#if dueDateText()}
<span
class="meta-item date"
class:overdue={isOverdue()}
class:today={isToday(new Date(task.dueDate || 0))}
>
<svg class="meta-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{dueDateText()}
</span>
{/if}
{#if subtaskProgress()}
<span class="meta-item">
<svg class="meta-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -129,6 +118,17 @@
{/if}
</button>
<!-- Due date (always on the right) -->
{#if dueDateText()}
<span
class="due-date"
class:overdue={isOverdue()}
class:today={task.dueDate && isToday(new Date(task.dueDate))}
>
{dueDateText()}
</span>
{/if}
<!-- Project indicator -->
{#if projectColor()}
<div class="project-dot" style="background-color: {projectColor()}"></div>
@ -151,18 +151,16 @@
.task-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 1rem;
border-radius: 9999px;
gap: 0.625rem;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
transition: all 0.2s;
margin-bottom: 0.5rem;
margin-bottom: 0.375rem;
}
:global(.dark) .task-item {
@ -172,11 +170,8 @@
.task-item:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.12);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
:global(.dark) .task-item:hover {
@ -188,6 +183,43 @@
opacity: 0.6;
}
/* Drag handle */
.drag-handle {
cursor: grab;
opacity: 0;
transition: opacity 0.15s;
flex-shrink: 0;
display: flex;
align-items: center;
padding: 0.125rem;
margin-left: -0.25rem;
}
.task-item:hover .drag-handle {
opacity: 0.4;
}
.drag-handle:hover {
opacity: 0.7 !important;
}
.drag-handle:active {
cursor: grabbing;
}
.drag-icon {
width: 1rem;
height: 1rem;
color: currentColor;
}
/* During drag, disable pointer events on interactive elements */
:global([aria-grabbed='true']) .task-checkbox,
:global([aria-grabbed='true']) .task-content,
:global([aria-grabbed='true']) .delete-btn {
pointer-events: none;
}
/* Priority dot */
.priority-dot {
width: 0.5rem;
@ -284,14 +316,6 @@
color: #9ca3af;
}
.meta-item.date.overdue {
color: #ef4444;
}
.meta-item.date.today {
color: #f97316;
}
.meta-icon {
width: 0.75rem;
height: 0.75rem;
@ -306,6 +330,26 @@
font-weight: 500;
}
/* Due date */
.due-date {
font-size: 0.75rem;
color: #6b7280;
flex-shrink: 0;
white-space: nowrap;
}
:global(.dark) .due-date {
color: #9ca3af;
}
.due-date.overdue {
color: #ef4444;
}
.due-date.today {
color: #f97316;
}
/* Project dot */
.project-dot {
width: 0.5rem;

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { Task } from '@todo/shared';
import TaskItem from './TaskItem.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
@ -6,10 +7,55 @@
interface Props {
tasks: Task[];
showCompleted?: boolean;
enableDragDrop?: boolean;
dropTargetDate?: Date | 'completed' | 'overdue';
onEditTask?: (task: Task) => void;
onTaskDrop?: (taskId: string, targetDate: Date | 'completed' | 'overdue') => void;
}
let { tasks, showCompleted = false, onEditTask }: Props = $props();
let {
tasks,
showCompleted = false,
enableDragDrop = false,
dropTargetDate,
onEditTask,
onTaskDrop,
}: Props = $props();
// Local mutable state for dnd-zone
let items = $state<Task[]>([]);
// Track last known tasks reference to detect parent updates
let lastTasksRef: Task[] | null = null;
// Sync items with tasks only when tasks array reference changes
$effect.pre(() => {
if (tasks !== lastTasksRef) {
items = [...tasks];
lastTasksRef = tasks;
}
});
const flipDurationMs = 200;
function handleDndConsider(e: CustomEvent<{ items: Task[] }>) {
items = e.detail.items;
}
function handleDndFinalize(e: CustomEvent<{ items: Task[]; info: { id: string } }>) {
const newItems = e.detail.items.filter((t) => t.id !== SHADOW_PLACEHOLDER_ITEM_ID);
const movedTaskId = e.detail.info.id;
// Check if this task came from another list (dropped INTO this list)
const wasInThisList = tasks.some((t) => t.id === movedTaskId);
if (!wasInThisList && dropTargetDate && onTaskDrop) {
// Task moved FROM another section TO this section
onTaskDrop(movedTaskId, dropTargetDate);
}
items = newItems;
}
async function handleToggleComplete(task: Task) {
if (task.isCompleted) {
@ -24,14 +70,86 @@
}
</script>
<div class="task-list">
{#each tasks as task (task.id)}
<TaskItem
{task}
{showCompleted}
onToggleComplete={() => handleToggleComplete(task)}
onDelete={() => handleDelete(task.id)}
onEdit={onEditTask ? () => onEditTask(task) : undefined}
/>
{/each}
</div>
{#if enableDragDrop}
<div
class="task-list"
class:empty={items.length === 0}
use:dndzone={{
items,
flipDurationMs,
dropTargetStyle: {},
dropTargetClasses: ['task-drop-target'],
type: 'homepage-tasks',
}}
onconsider={handleDndConsider}
onfinalize={handleDndFinalize}
>
{#each items.filter((t) => t.id !== SHADOW_PLACEHOLDER_ITEM_ID) as task (task.id)}
<TaskItem
{task}
{showCompleted}
onToggleComplete={() => handleToggleComplete(task)}
onDelete={() => handleDelete(task.id)}
onEdit={onEditTask ? () => onEditTask(task) : undefined}
/>
{/each}
{#if items.length === 0}
<div class="empty-placeholder">
<span>Aufgabe hierher ziehen</span>
</div>
{/if}
</div>
{:else}
<div class="task-list">
{#each tasks as task (task.id)}
<TaskItem
{task}
{showCompleted}
onToggleComplete={() => handleToggleComplete(task)}
onDelete={() => handleDelete(task.id)}
onEdit={onEditTask ? () => onEditTask(task) : undefined}
/>
{/each}
</div>
{/if}
<style>
.task-list {
min-height: 60px;
padding: 0.25rem;
border-radius: 0.5rem;
transition: background-color 0.15s ease;
}
.task-list.empty {
border: 2px dashed rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
}
:global(.dark) .task-list.empty {
border-color: rgba(255, 255, 255, 0.2);
}
.empty-placeholder {
color: var(--color-muted-foreground, #9ca3af);
font-size: 0.75rem;
padding: 1rem;
text-align: center;
}
:global(.task-drop-target) {
outline: 2px dashed #8b5cf6 !important;
outline-offset: -2px;
background: rgba(139, 92, 246, 0.08) !important;
}
:global(.task-drop-target) .empty-placeholder {
opacity: 0;
}
:global(.dark .task-drop-target) {
background: rgba(139, 92, 246, 0.15) !important;
}
</style>

View file

@ -242,7 +242,7 @@ export const tasksStore = {
* Update task optimistically (for drag and drop)
* Updates local state immediately, then syncs with server
*/
updateTaskOptimistic(
async updateTaskOptimistic(
id: string,
data: {
dueDate?: string | null;
@ -255,32 +255,25 @@ export const tasksStore = {
tasks = tasks.map((t) => (t.id === id ? { ...t, ...data } : t));
// Sync with server in background
if (data.isCompleted !== undefined) {
const apiCall = data.isCompleted ? tasksApi.completeTask(id) : tasksApi.uncompleteTask(id);
try {
// Handle completion state change first
if (data.isCompleted !== undefined && data.isCompleted !== originalTask.isCompleted) {
if (data.isCompleted) {
await tasksApi.completeTask(id);
} else {
await tasksApi.uncompleteTask(id);
}
}
apiCall
.then((updatedTask) => {
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
})
.catch((e) => {
// Rollback on error
console.error('Failed to update task:', e);
tasks = tasks.map((t) => (t.id === id ? originalTask : t));
});
}
if (data.dueDate !== undefined) {
tasksApi
.updateTask(id, { dueDate: data.dueDate })
.then((updatedTask) => {
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
})
.catch((e) => {
// Rollback on error
console.error('Failed to update task:', e);
tasks = tasks.map((t) => (t.id === id ? originalTask : t));
});
// Handle due date change
if (data.dueDate !== undefined) {
const updatedTask = await tasksApi.updateTask(id, { dueDate: data.dueDate });
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
}
} catch (e) {
// Rollback on error
console.error('Failed to update task:', e);
tasks = tasks.map((t) => (t.id === id ? originalTask : t));
}
},

View file

@ -15,6 +15,8 @@ export interface AppRoute {
labelKey: string;
/** Optional icon name */
icon?: string;
/** If true, this route cannot be hidden (e.g., Settings, Home) */
alwaysVisible?: boolean;
}
/**
@ -199,3 +201,46 @@ export function getAvailableRoutes(appId: string): AppRoute[] {
export function getDefaultRoute(appId: string): string {
return APP_ROUTES[appId]?.defaultRoute ?? '/';
}
/**
* Filter hidden navigation items from a list of nav items
* @param appId The app identifier
* @param items Array of nav items with href property
* @param hiddenNavItems Hidden items config (appId -> hidden paths)
* @returns Filtered array with hidden items removed
*/
export function filterHiddenNavItems<T extends { href: string }>(
appId: string,
items: T[],
hiddenNavItems: Record<string, string[]> = {}
): T[] {
const hidden = hiddenNavItems[appId] || [];
return items.filter((item) => !hidden.includes(item.href));
}
/**
* Get routes that can be hidden for a specific app
* (excludes routes marked as alwaysVisible)
* @param appId The app identifier
* @returns Array of routes that can be hidden
*/
export function getHideableRoutes(appId: string): AppRoute[] {
const config = APP_ROUTES[appId];
return config?.availableRoutes.filter((r) => !r.alwaysVisible) || [];
}
/**
* Check if a route is hidden for a specific app
* @param appId The app identifier
* @param path The route path
* @param hiddenNavItems Hidden items config
* @returns True if the route is hidden
*/
export function isRouteHidden(
appId: string,
path: string,
hiddenNavItems: Record<string, string[]> = {}
): boolean {
const hidden = hiddenNavItems[appId] || [];
return hidden.includes(path);
}

View file

@ -117,4 +117,12 @@ export {
// App Routes
export type { AppRoute, AppRouteConfig } from './app-routes';
export { APP_ROUTES, getStartPage, getAvailableRoutes, getDefaultRoute } from './app-routes';
export {
APP_ROUTES,
getStartPage,
getAvailableRoutes,
getDefaultRoute,
filterHiddenNavItems,
getHideableRoutes,
isRouteHidden,
} from './app-routes';

View file

@ -240,6 +240,8 @@ export interface NavSettings {
desktopPosition: NavPosition;
/** Whether sidebar is collapsed */
sidebarCollapsed: boolean;
/** Hidden navigation items per app (appId -> list of hidden paths) */
hiddenNavItems?: Record<string, string[]>;
}
/**
@ -323,7 +325,7 @@ export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = {
* Default global settings
*/
export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
nav: { desktopPosition: 'top', sidebarCollapsed: false },
nav: { desktopPosition: 'top', sidebarCollapsed: false, hiddenNavItems: {} },
theme: { mode: 'system', colorScheme: 'ocean', pinnedThemes: [] },
locale: 'de',
general: DEFAULT_GENERAL_SETTINGS,
@ -364,6 +366,12 @@ export interface UserSettingsStore {
setStartPage: (appId: string, path: string) => Promise<void>;
/** Update general settings */
updateGeneral: (settings: Partial<GeneralSettings>) => Promise<void>;
/** Get hidden nav items for a specific app */
getHiddenNavItemsForApp: (appId: string) => string[];
/** Toggle visibility of a navigation item */
toggleNavItemVisibility: (appId: string, href: string) => Promise<void>;
/** Set hidden nav items for an app */
setHiddenNavItems: (appId: string, hiddenHrefs: string[]) => Promise<void>;
}
/**

View file

@ -314,6 +314,46 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
}
}
/**
* Get hidden nav items for a specific app
*/
function getHiddenNavItemsForApp(targetAppId: string): string[] {
return globalSettings.nav.hiddenNavItems?.[targetAppId] || [];
}
/**
* Toggle visibility of a navigation item for an app
*/
async function toggleNavItemVisibility(targetAppId: string, href: string): Promise<void> {
const currentHidden = getHiddenNavItemsForApp(targetAppId);
const isHidden = currentHidden.includes(href);
const newHidden = isHidden ? currentHidden.filter((h) => h !== href) : [...currentHidden, href];
await setHiddenNavItems(targetAppId, newHidden);
}
/**
* Set hidden nav items for an app
*/
async function setHiddenNavItems(targetAppId: string, hiddenHrefs: string[]): Promise<void> {
const newHiddenNavItems = {
...globalSettings.nav.hiddenNavItems,
[targetAppId]: hiddenHrefs,
};
// Remove empty arrays
if (hiddenHrefs.length === 0) {
delete newHiddenNavItems[targetAppId];
}
await updateGlobal({
nav: {
hiddenNavItems: newHiddenNavItems,
},
} as Partial<GlobalSettings>);
}
return {
get nav() {
return nav;
@ -349,5 +389,8 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe
removeAppOverride,
setStartPage,
updateGeneral,
getHiddenNavItemsForApp,
toggleNavItemVisibility,
setHiddenNavItems,
};
}

View file

@ -8,11 +8,11 @@
}
const HIGHLIGHT_PATTERNS: HighlightPattern[] = [
// Priority keywords (Todo)
{
pattern: /(!{1,3}|!?dringend|!?wichtig|!?normal|!?später|!?späer)\b/gi,
className: 'hl-priority',
},
// Priority keywords (Todo) - with specific colors per level
{ pattern: /(!{3,}|!?dringend)\b/gi, className: 'hl-priority-urgent' },
{ pattern: /(!{2}|!?wichtig)\b/gi, className: 'hl-priority-high' },
{ pattern: /!?normal\b/gi, className: 'hl-priority-medium' },
{ pattern: /!?sp[aä]ter\b/gi, className: 'hl-priority-low' },
// Tags
{ pattern: /#\w+/g, className: 'hl-tag' },
// Projects/Calendars/Companies (@reference)
@ -593,9 +593,24 @@
color: hsl(var(--color-muted-foreground));
}
/* Syntax highlighting colors */
.input-highlight-backdrop :global(.hl-priority) {
color: hsl(var(--color-warning, 38 92% 50%));
/* Syntax highlighting colors - Priority levels with matching UI colors */
.input-highlight-backdrop :global(.hl-priority-urgent) {
color: #ef4444; /* red - Dringend */
font-weight: 600;
}
.input-highlight-backdrop :global(.hl-priority-high) {
color: #f97316; /* orange - Wichtig */
font-weight: 600;
}
.input-highlight-backdrop :global(.hl-priority-medium) {
color: #eab308; /* yellow - Normal */
font-weight: 600;
}
.input-highlight-backdrop :global(.hl-priority-low) {
color: #22c55e; /* green - Später */
font-weight: 600;
}

View file

@ -8,6 +8,7 @@
import { getAvailableRoutes, getDefaultRoute } from '@manacore/shared-theme';
import SettingsSection from './SettingsSection.svelte';
import SettingsCard from './SettingsCard.svelte';
import NavVisibilitySettings from './NavVisibilitySettings.svelte';
interface Props {
/** User settings store instance */
@ -16,6 +17,8 @@
appId?: string;
/** Whether to show navigation settings */
showNavigation?: boolean;
/** Whether to show nav visibility settings */
showNavVisibility?: boolean;
/** Whether to show theme settings */
showTheme?: boolean;
/** Whether to show language settings */
@ -34,6 +37,7 @@
userSettings,
appId,
showNavigation = true,
showNavVisibility = true,
showTheme = true,
showLanguage = true,
showGeneral = true,
@ -205,10 +209,21 @@
</div>
{/if}
{#if showNavVisibility && appId}
<!-- Navigation Visibility Settings -->
<div
class="space-y-4 {showNavigation ? 'pt-4 border-t border-[hsl(var(--border))]' : ''}"
>
<NavVisibilitySettings {userSettings} {appId} {t} />
</div>
{/if}
{#if showTheme}
<!-- Theme Settings -->
<div
class="space-y-4 {showNavigation ? 'pt-4 border-t border-[hsl(var(--border))]' : ''}"
class="space-y-4 {showNavigation || (showNavVisibility && appId)
? 'pt-4 border-t border-[hsl(var(--border))]'
: ''}"
>
<h3
class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider"
@ -266,7 +281,7 @@
{#if showLanguage}
<!-- Language Settings -->
<div
class="space-y-4 {showTheme || showNavigation
class="space-y-4 {showTheme || showNavigation || (showNavVisibility && appId)
? 'pt-4 border-t border-[hsl(var(--border))]'
: ''}"
>
@ -303,7 +318,10 @@
{#if showGeneral}
<!-- General Settings -->
<div
class="space-y-4 {showLanguage || showTheme || showNavigation
class="space-y-4 {showLanguage ||
showTheme ||
showNavigation ||
(showNavVisibility && appId)
? 'pt-4 border-t border-[hsl(var(--border))]'
: ''}"
>

View file

@ -0,0 +1,161 @@
<script lang="ts">
import type { UserSettingsStore, AppRoute } from '@manacore/shared-theme';
import { getHideableRoutes, APP_ROUTES } from '@manacore/shared-theme';
interface Props {
/** User settings store instance */
userSettings: UserSettingsStore;
/** Current app ID */
appId: string;
/** Translation function (optional, falls back to German) */
t?: (key: string) => string;
}
let { userSettings, appId, t = (key: string) => key }: Props = $props();
// Get all apps that have configurable routes
const configurableApps = $derived(
Object.entries(APP_ROUTES)
.filter(([, config]) => {
const hideableRoutes = config.availableRoutes.filter((r) => !r.alwaysVisible);
return hideableRoutes.length > 0;
})
.map(([id, config]) => ({
id,
label: getAppLabel(id),
routes: config.availableRoutes.filter((r) => !r.alwaysVisible),
}))
);
// Sort so current app is first
const sortedApps = $derived(
[...configurableApps].sort((a, b) => {
if (a.id === appId) return -1;
if (b.id === appId) return 1;
return a.label.localeCompare(b.label);
})
);
function getAppLabel(id: string): string {
const labels: Record<string, string> = {
clock: 'Uhr',
calendar: 'Kalender',
contacts: 'Kontakte',
mail: 'Mail',
todo: 'Aufgaben',
storage: 'Speicher',
chat: 'Chat',
picture: 'Bilder',
manadeck: 'ManaDeck',
zitare: 'Zitare',
presi: 'Präsentation',
manacore: 'ManaCore',
};
return labels[id] || id;
}
function isRouteHidden(targetAppId: string, path: string): boolean {
const hidden = userSettings.getHiddenNavItemsForApp(targetAppId);
return hidden.includes(path);
}
async function handleToggle(targetAppId: string, path: string): Promise<void> {
await userSettings.toggleNavItemVisibility(targetAppId, path);
}
// Expanded state per app
let expandedApps = $state<Record<string, boolean>>({});
// Initialize with current app expanded
$effect(() => {
if (appId && expandedApps[appId] === undefined) {
expandedApps[appId] = true;
}
});
function toggleApp(id: string): void {
expandedApps[id] = !expandedApps[id];
}
</script>
<div class="space-y-4">
<div>
<h3 class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider">
Navigation anpassen
</h3>
<p class="text-sm text-[hsl(var(--muted-foreground))] mt-1">
Versteckte Seiten bleiben über die URL erreichbar
</p>
</div>
<div class="space-y-2">
{#each sortedApps as app (app.id)}
<div class="border border-[hsl(var(--border))] rounded-lg overflow-hidden">
<!-- App Header (collapsible) -->
<button
type="button"
class="w-full flex items-center justify-between px-4 py-3 bg-[hsl(var(--muted))] hover:bg-[hsl(var(--muted))]/80 transition-colors"
onclick={() => toggleApp(app.id)}
>
<span class="font-medium text-[hsl(var(--foreground))] flex items-center gap-2">
{app.label}
{#if app.id === appId}
<span
class="text-xs px-1.5 py-0.5 rounded bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]"
>
Aktuell
</span>
{/if}
</span>
<svg
class="w-5 h-5 text-[hsl(var(--muted-foreground))] transition-transform {expandedApps[
app.id
]
? 'rotate-180'
: ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"
></path>
</svg>
</button>
<!-- Routes List (collapsible) -->
{#if expandedApps[app.id]}
<div class="p-3 space-y-1 bg-[hsl(var(--background))]">
{#each app.routes as route (route.path)}
{@const hidden = isRouteHidden(app.id, route.path)}
<label
class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-[hsl(var(--muted))]/50 cursor-pointer transition-colors"
>
<span
class="text-sm {hidden
? 'text-[hsl(var(--muted-foreground))] line-through'
: 'text-[hsl(var(--foreground))]'}"
>
{t(route.labelKey)}
</span>
<button
type="button"
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors {!hidden
? 'bg-[hsl(var(--primary))]'
: 'bg-gray-200 dark:bg-gray-700'}"
onclick={() => handleToggle(app.id, route.path)}
aria-label={hidden ? 'Einblenden' : 'Ausblenden'}
>
<span
class="inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform shadow-sm {!hidden
? 'translate-x-5'
: 'translate-x-0.5'}"
></span>
</button>
</label>
{/each}
</div>
{/if}
</div>
{/each}
</div>
</div>

View file

@ -10,3 +10,4 @@ export { default as SettingsTimeInput } from './SettingsTimeInput.svelte';
export { default as SettingsDangerZone } from './SettingsDangerZone.svelte';
export { default as SettingsDangerButton } from './SettingsDangerButton.svelte';
export { default as GlobalSettingsSection } from './GlobalSettingsSection.svelte';
export { default as NavVisibilitySettings } from './NavVisibilitySettings.svelte';

View file

@ -18,6 +18,10 @@ export class NavSettingsDto {
@IsOptional()
@IsBoolean()
sidebarCollapsed?: boolean;
@IsOptional()
@IsObject()
hiddenNavItems?: Record<string, string[]>;
}
// Theme settings
@ -70,6 +74,7 @@ export class UpdateAppOverrideDto {
export interface NavSettings {
desktopPosition: 'top' | 'bottom';
sidebarCollapsed: boolean;
hiddenNavItems?: Record<string, string[]>;
}
export interface ThemeSettings {