From aa117c51cdbdb8b14e04b8d4a3a17f00ca1a22e2 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:06:22 +0100 Subject: [PATCH] feat(command-bar): match priority highlight colors to UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../web/src/lib/components/TaskItem.svelte | 126 +++++++++----- .../web/src/lib/components/TaskList.svelte | 142 +++++++++++++-- .../apps/web/src/lib/stores/tasks.svelte.ts | 45 +++-- packages/shared-theme/src/app-routes.ts | 45 +++++ packages/shared-theme/src/index.ts | 10 +- packages/shared-theme/src/types.ts | 10 +- .../src/user-settings-store.svelte.ts | 43 +++++ .../src/command-bar/CommandBar.svelte | 31 +++- .../src/settings/GlobalSettingsSection.svelte | 24 ++- .../src/settings/NavVisibilitySettings.svelte | 161 ++++++++++++++++++ packages/shared-ui/src/settings/index.ts | 1 + .../mana-core-auth/src/settings/dto/index.ts | 5 + 12 files changed, 551 insertions(+), 92 deletions(-) create mode 100644 packages/shared-ui/src/settings/NavVisibilitySettings.svelte diff --git a/apps/todo/apps/web/src/lib/components/TaskItem.svelte b/apps/todo/apps/web/src/lib/components/TaskItem.svelte index 3ddaabe60..f32a1a264 100644 --- a/apps/todo/apps/web/src/lib/components/TaskItem.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskItem.svelte @@ -59,6 +59,13 @@
+ +
+ + + +
+
- - {#if dueDateText() || subtaskProgress() || (task.labels && task.labels.length > 0)} + + {#if subtaskProgress() || (task.labels && task.labels.length > 0)}
- {#if dueDateText()} - - - - - {dueDateText()} - - {/if} - {#if subtaskProgress()} @@ -129,6 +118,17 @@ {/if} + + {#if dueDateText()} + + {dueDateText()} + + {/if} + {#if projectColor()}
@@ -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; diff --git a/apps/todo/apps/web/src/lib/components/TaskList.svelte b/apps/todo/apps/web/src/lib/components/TaskList.svelte index 0e7c66c65..1ce2fba42 100644 --- a/apps/todo/apps/web/src/lib/components/TaskList.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskList.svelte @@ -1,4 +1,5 @@ -
- {#each tasks as task (task.id)} - handleToggleComplete(task)} - onDelete={() => handleDelete(task.id)} - onEdit={onEditTask ? () => onEditTask(task) : undefined} - /> - {/each} -
+{#if enableDragDrop} +
+ {#each items.filter((t) => t.id !== SHADOW_PLACEHOLDER_ITEM_ID) as task (task.id)} + handleToggleComplete(task)} + onDelete={() => handleDelete(task.id)} + onEdit={onEditTask ? () => onEditTask(task) : undefined} + /> + {/each} + {#if items.length === 0} +
+ Aufgabe hierher ziehen +
+ {/if} +
+{:else} +
+ {#each tasks as task (task.id)} + handleToggleComplete(task)} + onDelete={() => handleDelete(task.id)} + onEdit={onEditTask ? () => onEditTask(task) : undefined} + /> + {/each} +
+{/if} + + diff --git a/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts b/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts index 435057133..429a2ac93 100644 --- a/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts @@ -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)); } }, diff --git a/packages/shared-theme/src/app-routes.ts b/packages/shared-theme/src/app-routes.ts index dcfa573c1..4b9063691 100644 --- a/packages/shared-theme/src/app-routes.ts +++ b/packages/shared-theme/src/app-routes.ts @@ -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( + appId: string, + items: T[], + hiddenNavItems: Record = {} +): 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 = {} +): boolean { + const hidden = hiddenNavItems[appId] || []; + return hidden.includes(path); +} diff --git a/packages/shared-theme/src/index.ts b/packages/shared-theme/src/index.ts index 090d9659b..9a5e51864 100644 --- a/packages/shared-theme/src/index.ts +++ b/packages/shared-theme/src/index.ts @@ -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'; diff --git a/packages/shared-theme/src/types.ts b/packages/shared-theme/src/types.ts index d8eed66a7..964771bec 100644 --- a/packages/shared-theme/src/types.ts +++ b/packages/shared-theme/src/types.ts @@ -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; } /** @@ -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; /** Update general settings */ updateGeneral: (settings: Partial) => Promise; + /** Get hidden nav items for a specific app */ + getHiddenNavItemsForApp: (appId: string) => string[]; + /** Toggle visibility of a navigation item */ + toggleNavItemVisibility: (appId: string, href: string) => Promise; + /** Set hidden nav items for an app */ + setHiddenNavItems: (appId: string, hiddenHrefs: string[]) => Promise; } /** diff --git a/packages/shared-theme/src/user-settings-store.svelte.ts b/packages/shared-theme/src/user-settings-store.svelte.ts index 7a9ee94f6..c2ecabd16 100644 --- a/packages/shared-theme/src/user-settings-store.svelte.ts +++ b/packages/shared-theme/src/user-settings-store.svelte.ts @@ -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 { + 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 { + const newHiddenNavItems = { + ...globalSettings.nav.hiddenNavItems, + [targetAppId]: hiddenHrefs, + }; + + // Remove empty arrays + if (hiddenHrefs.length === 0) { + delete newHiddenNavItems[targetAppId]; + } + + await updateGlobal({ + nav: { + hiddenNavItems: newHiddenNavItems, + }, + } as Partial); + } + return { get nav() { return nav; @@ -349,5 +389,8 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe removeAppOverride, setStartPage, updateGeneral, + getHiddenNavItemsForApp, + toggleNavItemVisibility, + setHiddenNavItems, }; } diff --git a/packages/shared-ui/src/command-bar/CommandBar.svelte b/packages/shared-ui/src/command-bar/CommandBar.svelte index b28d2c4e7..623e1f3c0 100644 --- a/packages/shared-ui/src/command-bar/CommandBar.svelte +++ b/packages/shared-ui/src/command-bar/CommandBar.svelte @@ -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; } diff --git a/packages/shared-ui/src/settings/GlobalSettingsSection.svelte b/packages/shared-ui/src/settings/GlobalSettingsSection.svelte index 21abe64f9..20aefb85a 100644 --- a/packages/shared-ui/src/settings/GlobalSettingsSection.svelte +++ b/packages/shared-ui/src/settings/GlobalSettingsSection.svelte @@ -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 @@
{/if} + {#if showNavVisibility && appId} + +
+ +
+ {/if} + {#if showTheme}

@@ -303,7 +318,10 @@ {#if showGeneral}
diff --git a/packages/shared-ui/src/settings/NavVisibilitySettings.svelte b/packages/shared-ui/src/settings/NavVisibilitySettings.svelte new file mode 100644 index 000000000..a6930f4d8 --- /dev/null +++ b/packages/shared-ui/src/settings/NavVisibilitySettings.svelte @@ -0,0 +1,161 @@ + + +
+
+

+ Navigation anpassen +

+

+ Versteckte Seiten bleiben über die URL erreichbar +

+
+ +
+ {#each sortedApps as app (app.id)} +
+ + + + + {#if expandedApps[app.id]} +
+ {#each app.routes as route (route.path)} + {@const hidden = isRouteHidden(app.id, route.path)} + + {/each} +
+ {/if} +
+ {/each} +
+
diff --git a/packages/shared-ui/src/settings/index.ts b/packages/shared-ui/src/settings/index.ts index f7daa394c..06db8bce2 100644 --- a/packages/shared-ui/src/settings/index.ts +++ b/packages/shared-ui/src/settings/index.ts @@ -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'; diff --git a/services/mana-core-auth/src/settings/dto/index.ts b/services/mana-core-auth/src/settings/dto/index.ts index bc8ee0e3d..2f315c1fe 100644 --- a/services/mana-core-auth/src/settings/dto/index.ts +++ b/services/mana-core-auth/src/settings/dto/index.ts @@ -18,6 +18,10 @@ export class NavSettingsDto { @IsOptional() @IsBoolean() sidebarCollapsed?: boolean; + + @IsOptional() + @IsObject() + hiddenNavItems?: Record; } // Theme settings @@ -70,6 +74,7 @@ export class UpdateAppOverrideDto { export interface NavSettings { desktopPosition: 'top' | 'bottom'; sidebarCollapsed: boolean; + hiddenNavItems?: Record; } export interface ThemeSettings {