diff --git a/apps/calendar/apps/web/src/lib/components/calendar/CalendarToolbar.svelte b/apps/calendar/apps/web/src/lib/components/calendar/CalendarToolbar.svelte new file mode 100644 index 000000000..658bfd49f --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/calendar/CalendarToolbar.svelte @@ -0,0 +1,125 @@ + + + + + viewStore.goToToday()} title="Zum heutigen Tag springen"> + Heute + + + + + + viewStore.goToPrevious()} title="Zurück" iconOnly> + + + + + viewStore.goToNext()} title="Weiter" iconOnly> + + + + + + + + + settingsStore.set('showOnlyWeekdays', !settingsStore.showOnlyWeekdays)} + active={settingsStore.showOnlyWeekdays} + title="Nur Wochentage anzeigen (Mo-Fr)" + > + Mo-Fr + + + + settingsStore.set('filterHoursEnabled', !settingsStore.filterHoursEnabled)} + active={settingsStore.filterHoursEnabled} + title="Stundenfilter ein/aus" + iconOnly + > + + + + + + + + + + + + + diff --git a/apps/calendar/apps/web/src/lib/components/calendar/DateStrip.svelte b/apps/calendar/apps/web/src/lib/components/calendar/DateStrip.svelte new file mode 100644 index 000000000..f357b7a8d --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/calendar/DateStrip.svelte @@ -0,0 +1,438 @@ + + +
+ + {#if !isTodayVisible} + + {/if} + +
+ +
+ {#each days as day, index} + {@const monthLabel = getMonthLabel(day, index)} + {@const dayIsToday = isToday(day)} + {@const dayIsSelected = isSameDay(day, currentDate)} + {@const dayIsWeekend = day.getDay() === 0 || day.getDay() === 6} + {@const dayIsFirstOfMonth = day.getDate() === 1} + {@const dayInRange = isWithinInterval(day, { start: viewRange.start, end: viewRange.end })} + {@const dayIsRangeStart = isSameDay(day, viewRange.start)} + {@const dayIsRangeEnd = isSameDay(day, viewRange.end)} + {#if monthLabel} +
+ {monthLabel} +
+ {/if} + + {/each} +
+
+
+ + diff --git a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte index d321c5b31..9f4f39923 100644 --- a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte @@ -3,11 +3,11 @@ import { page } from '$app/stores'; import { onMount } from 'svelte'; import { locale } from 'svelte-i18n'; - import { PillNavigation, CommandBar } from '@manacore/shared-ui'; + import { PillNavigation, QuickInputBar } from '@manacore/shared-ui'; import type { PillNavItem, PillDropdownItem, - CommandBarItem, + QuickInputItem, QuickAction, CreatePreview, } from '@manacore/shared-ui'; @@ -41,31 +41,28 @@ resolveEventIds, formatParsedEventPreview, } from '$lib/utils/event-parser'; + import CalendarToolbar from '$lib/components/calendar/CalendarToolbar.svelte'; + import DateStrip from '$lib/components/calendar/DateStrip.svelte'; // App switcher items const appItems = getPillAppItems('calendar'); let { children } = $props(); - // CommandBar state - let commandBarOpen = $state(false); - - // CommandBar quick actions (no search for calendar yet) - const commandBarQuickActions: QuickAction[] = [ - { id: 'new', label: 'Neuen Termin erstellen', icon: 'plus', href: '/event/new', shortcut: 'N' }, + // QuickInputBar quick actions + const quickActions: QuickAction[] = [ { id: 'today', - label: 'Zu Heute springen', + label: 'Heute', icon: 'calendar', onclick: () => viewStore.goToToday(), }, - { id: 'agenda', label: 'Agenda anzeigen', icon: 'list', href: '/agenda' }, - { id: 'tasks', label: 'Aufgaben anzeigen', icon: 'check-square', href: '/tasks' }, + { id: 'agenda', label: 'Agenda', icon: 'list', href: '/agenda' }, { id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' }, ]; - // CommandBar search - search events - async function handleCommandBarSearch(query: string): Promise { + // QuickInputBar search - search events + async function handleSearch(query: string): Promise { if (!query.trim()) return []; const result = await searchEvents(query); @@ -78,24 +75,24 @@ })); } - function handleCommandBarSelect(item: CommandBarItem) { + function handleSelect(item: QuickInputItem) { goto(`/event/${item.id}`); } - // CommandBar Quick-Create handlers - function handleCommandBarParseCreate(query: string): CreatePreview | null { + // QuickInputBar Quick-Create handlers + function handleParseCreate(query: string): CreatePreview | null { if (!query.trim()) return null; const parsed = parseEventInput(query); if (!parsed.title) return null; return { - title: parsed.title, + title: `"${parsed.title}" erstellen`, subtitle: formatParsedEventPreview(parsed), }; } - async function handleCommandBarCreate(query: string): Promise { + async function handleCreate(query: string): Promise { const parsed = parseEventInput(query); if (!parsed.title) return; @@ -137,6 +134,9 @@ // Use theme store's isDark directly let isDark = $derived(theme.isDark); + // Show toolbar only on calendar main page + let showCalendarToolbar = $derived($page.url.pathname === '/'); + // Get pinned themes from user settings (extended themes only) let pinnedThemes = $derived( (userSettings.theme?.pinnedThemes || []).filter((t): t is ThemeVariant => @@ -204,13 +204,6 @@ function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; - // Cmd/Ctrl+K to open command bar (works even in inputs) - if ((event.ctrlKey || event.metaKey) && event.key === 'k') { - event.preventDefault(); - commandBarOpen = true; - return; - } - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return; } @@ -307,7 +300,7 @@ onModeChange={handleModeChange} {isCollapsed} onCollapsedChange={handleCollapsedChange} - desktopPosition={userSettings.nav.desktopPosition} + desktopPosition="bottom" showThemeToggle={true} showThemeVariants={true} {themeVariantItems} @@ -330,10 +323,21 @@ allAppsHref="/apps" /> + + {#if showCalendarToolbar} + + {/if} + + + {#if showCalendarToolbar} + + {/if} +
- - (commandBarOpen = false)} - onSearch={handleCommandBarSearch} - onSelect={handleCommandBarSelect} - quickActions={commandBarQuickActions} - placeholder="Termin suchen oder erstellen..." + + @@ -371,12 +375,37 @@ transition: all 300ms ease; position: relative; z-index: 0; + /* Space for QuickInputBar at bottom */ + padding-bottom: calc(80px + env(safe-area-inset-bottom)); } .main-content.floating-mode { padding-top: 70px; } + /* Extra padding when DateStrip + Toolbar are at bottom */ + .main-content.floating-mode.has-toolbar { + padding-top: 0; + padding-bottom: calc( + 280px + env(safe-area-inset-bottom) + ); /* DateStrip + Toolbar + PillNav + QuickInputBar */ + } + + @media (max-width: 768px) { + /* On mobile, toolbars are at bottom, extra padding at bottom instead */ + .main-content { + padding-bottom: calc(150px + env(safe-area-inset-bottom)); /* PillNav + QuickInputBar */ + } + .main-content.has-toolbar { + padding-bottom: calc( + 250px + env(safe-area-inset-bottom) + ); /* DateStrip + Toolbar + BottomNav + QuickInputBar */ + } + .main-content.floating-mode.has-toolbar { + padding-top: 70px; + } + } + .main-content.sidebar-mode { padding-left: 180px; } diff --git a/apps/contacts/apps/web/src/lib/components/ContactsToolbar.svelte b/apps/contacts/apps/web/src/lib/components/ContactsToolbar.svelte new file mode 100644 index 000000000..2e30f283e --- /dev/null +++ b/apps/contacts/apps/web/src/lib/components/ContactsToolbar.svelte @@ -0,0 +1,274 @@ + + + + + goto('/contacts/new')} title={$_('contacts.new')}> + + + + {$_('contacts.new')} + + + + + + + + + + + + + + + + + + + {#if favoritesCount > 0} + {favoritesCount} + {/if} + + + + + + + + + + + + + + + +
+ + + +
+
+ + diff --git a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte index 0a8871350..df28a0743 100644 --- a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte @@ -3,11 +3,11 @@ import { page } from '$app/stores'; import { onMount } from 'svelte'; import { locale } from 'svelte-i18n'; - import { PillNavigation, CommandBar } from '@manacore/shared-ui'; + import { PillNavigation, QuickInputBar } from '@manacore/shared-ui'; import type { PillNavItem, PillDropdownItem, - CommandBarItem, + QuickInputItem, QuickAction, CreatePreview, } from '@manacore/shared-ui'; @@ -39,9 +39,6 @@ formatParsedContactPreview, } from '$lib/utils/contact-parser'; - // Search modal state - let searchModalOpen = $state(false); - // Tags state for Quick-Create let availableTags = $state<{ id: string; name: string }[]>([]); @@ -130,13 +127,6 @@ function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; - // Cmd/Ctrl+K to open search (works even in inputs) - if ((event.ctrlKey || event.metaKey) && event.key === 'k') { - event.preventDefault(); - searchModalOpen = true; - return; - } - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return; } @@ -188,8 +178,8 @@ goto('/', { replaceState: false }); } - // CommandBar search function - async function handleCommandBarSearch(query: string): Promise { + // QuickInputBar search function + async function handleSearch(query: string): Promise { const response = await contactsApi.list({ search: query, limit: 10 }); return (response.contacts || []).map((contact: any) => ({ id: contact.id, @@ -204,25 +194,25 @@ })); } - // CommandBar item selection - function handleCommandBarSelect(item: CommandBarItem) { + // QuickInputBar item selection + function handleSelect(item: QuickInputItem) { goto(`/contacts/${item.id}`); } - // CommandBar Quick-Create handlers - function handleCommandBarParseCreate(query: string): CreatePreview | null { + // QuickInputBar Quick-Create handlers + function handleParseCreate(query: string): CreatePreview | null { if (!query.trim()) return null; const parsed = parseContactInput(query); if (!parsed.displayName) return null; return { - title: parsed.displayName, + title: `"${parsed.displayName}" erstellen`, subtitle: formatParsedContactPreview(parsed), }; } - async function handleCommandBarCreate(query: string): Promise { + async function handleCreate(query: string): Promise { const parsed = parseContactInput(query); if (!parsed.displayName) return; @@ -250,18 +240,11 @@ } } - // CommandBar quick actions - const commandBarQuickActions: QuickAction[] = [ - { - id: 'new', - label: 'Neuen Kontakt erstellen', - icon: 'plus', - href: '/contacts/new', - shortcut: 'N', - }, - { id: 'favorites', label: 'Favoriten anzeigen', icon: 'heart', href: '/favorites' }, - { id: 'tags', label: 'Tags verwalten', icon: 'tag', href: '/tags' }, - { id: 'import', label: 'Kontakte importieren', icon: 'upload', href: '/data?tab=import' }, + // QuickInputBar quick actions + const quickActions: QuickAction[] = [ + { id: 'favorites', label: 'Favoriten', icon: 'heart', href: '/favorites' }, + { id: 'tags', label: 'Tags', icon: 'tag', href: '/tags' }, + { id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' }, ]; onMount(async () => { @@ -360,20 +343,20 @@ {/if} - - (searchModalOpen = false)} - onSearch={handleCommandBarSearch} - onSelect={handleCommandBarSelect} - quickActions={commandBarQuickActions} - placeholder="Kontakt suchen oder erstellen..." + + diff --git a/apps/todo/apps/web/src/lib/components/TodoToolbar.svelte b/apps/todo/apps/web/src/lib/components/TodoToolbar.svelte new file mode 100644 index 000000000..01dce8b12 --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/TodoToolbar.svelte @@ -0,0 +1,848 @@ + + + + + + +
e.stopPropagation()}> +
+ + + + +
+ + + {#if showQuickAddOptions || inputValue.trim()} +
+ +
+ + + {#if showDatePicker} +
e.stopPropagation()}> + {#each dateOptions as option} + + {/each} +
+ {/if} +
+ + +
+ + + {#if showPriorityPicker} +
e.stopPropagation()}> + {#each PRIORITY_OPTIONS as priority} + + {/each} +
+ {/if} +
+ + +
+ + + {#if showProjectPicker} +
e.stopPropagation()}> + + {#each projectsStore.activeProjects as project} + + {/each} +
+ {/if} +
+ + + +
+ {/if} +
+ + + + + goto('/kanban')} title="Kanban-Ansicht"> + + + + + + + + +
e.stopPropagation()}> + { + showFilterDropdown = !showFilterDropdown; + closeAllDropdowns(); + }} + active={activeFilterCount > 0} + title="Filter" + > + + + + {#if activeFilterCount > 0} + {activeFilterCount} + {/if} + + + {#if showFilterDropdown} +
e.stopPropagation()}> +
+
Priorität
+
+ {#each priorities as priority} + + {/each} +
+
+ +
+
Projekt
+ +
+ + {#if activeFilterCount > 0} + + {/if} +
+ {/if} +
+ + + + + + + + + + + + + + +
+ + diff --git a/apps/todo/apps/web/src/routes/(app)/+layout.svelte b/apps/todo/apps/web/src/routes/(app)/+layout.svelte index 8b909f30a..dad2bb8c9 100644 --- a/apps/todo/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/todo/apps/web/src/routes/(app)/+layout.svelte @@ -3,11 +3,11 @@ import { page } from '$app/stores'; import { onMount } from 'svelte'; import { locale } from 'svelte-i18n'; - import { PillNavigation, CommandBar } from '@manacore/shared-ui'; + import { PillNavigation, QuickInputBar } from '@manacore/shared-ui'; import type { PillNavItem, PillDropdownItem, - CommandBarItem, + QuickInputItem, QuickAction, CreatePreview, } from '@manacore/shared-ui'; @@ -38,19 +38,15 @@ let { children } = $props(); - // CommandBar state - let commandBarOpen = $state(false); - - // CommandBar quick actions - const commandBarQuickActions: QuickAction[] = [ - { id: 'new', label: 'Neue Aufgabe erstellen', icon: 'plus', href: '/task/new', shortcut: 'N' }, - { id: 'kanban', label: 'Kanban-Board', icon: 'list', href: '/kanban' }, - { id: 'stats', label: 'Statistiken', icon: 'chart', href: '/statistics' }, + // QuickInputBar quick actions + const quickActions: QuickAction[] = [ + { id: 'kanban', label: 'Kanban', icon: 'kanban', href: '/kanban' }, + { id: 'stats', label: 'Statistik', icon: 'chart', href: '/statistics' }, { id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' }, ]; - // CommandBar search - search tasks - async function handleCommandBarSearch(query: string): Promise { + // QuickInputBar search - search tasks + async function handleSearch(query: string): Promise { if (!query.trim()) return []; try { @@ -69,25 +65,25 @@ } } - function handleCommandBarSelect(item: CommandBarItem) { + function handleSelect(item: QuickInputItem) { goto(`/task/${item.id}`); } - // CommandBar create - parse input and show preview - function handleCommandBarParseCreate(query: string): CreatePreview | null { + // QuickInputBar create - parse input and show preview + function handleParseCreate(query: string): CreatePreview | null { if (!query.trim()) return null; const parsed = parseTaskInput(query); const preview = formatParsedTaskPreview(parsed); return { - title: `"${parsed.title}" als Aufgabe erstellen`, + title: `"${parsed.title}" erstellen`, subtitle: preview || 'Neue Aufgabe', }; } - // CommandBar create - actually create the task - async function handleCommandBarCreate(query: string): Promise { + // QuickInputBar create - actually create the task + async function handleCreate(query: string): Promise { if (!query.trim()) return; const parsed = parseTaskInput(query); @@ -192,13 +188,6 @@ function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; - // Cmd/Ctrl+K to open command bar (works even in inputs) - if ((event.ctrlKey || event.metaKey) && event.key === 'k') { - event.preventDefault(); - commandBarOpen = true; - return; - } - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return; } @@ -366,20 +355,20 @@ - - (commandBarOpen = false)} - onSearch={handleCommandBarSearch} - onSelect={handleCommandBarSelect} - quickActions={commandBarQuickActions} - placeholder="Aufgabe suchen oder erstellen..." + + @@ -394,6 +383,8 @@ transition: all 300ms ease; position: relative; z-index: 0; + /* Space for QuickInputBar at bottom */ + padding-bottom: calc(80px + env(safe-area-inset-bottom)); } .main-content.floating-mode { @@ -438,4 +429,11 @@ padding-right: 0; } } + + /* Mobile: More space for QuickInputBar + PillNav */ + @media (max-width: 768px) { + .main-content { + padding-bottom: calc(150px + env(safe-area-inset-bottom)); + } + } diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index d8c032f24..71c8b18a4 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -79,6 +79,12 @@ export { SidebarSection, PillNavigation, PillDropdown, + PillTabGroup, + PillTimeRangeSelector, + PillViewSwitcher, + PillToolbar, + PillToolbarButton, + PillToolbarDivider, } from './navigation'; export type { NavItem, @@ -90,6 +96,7 @@ export type { PillDropdownItem, PillNavElement, PillNavigationProps, + PillTabOption, } from './navigation'; // Settings @@ -107,9 +114,13 @@ export { GlobalSettingsSection, } from './settings'; -// Command Bar +// Command Bar (deprecated - use QuickInputBar) export { CommandBar } from './command-bar'; -export type { CommandBarItem, QuickAction, CreatePreview } from './command-bar'; +export type { CommandBarItem } from './command-bar'; + +// Quick Input Bar +export { QuickInputBar } from './quick-input'; +export type { QuickInputItem, QuickAction, CreatePreview } from './quick-input'; // Pages export { default as AppsPage } from './pages/AppsPage.svelte'; diff --git a/packages/shared-ui/src/navigation/PillTimeRangeSelector.svelte b/packages/shared-ui/src/navigation/PillTimeRangeSelector.svelte new file mode 100644 index 000000000..806977d1f --- /dev/null +++ b/packages/shared-ui/src/navigation/PillTimeRangeSelector.svelte @@ -0,0 +1,471 @@ + + +
+ + + {#if isOpen} + + +
+
Zeitbereich
+ +
+
+ +
+ {#each startHours as hour} + + {/each} +
+
+ +
+ +
+ +
+ {#each endHours as hour} + + {/each} +
+
+
+ +
+ {formatHour(startHour)} - {formatHour(endHour)} +
+
+ {/if} +
+ + diff --git a/packages/shared-ui/src/navigation/PillToolbar.svelte b/packages/shared-ui/src/navigation/PillToolbar.svelte new file mode 100644 index 000000000..f702e3d18 --- /dev/null +++ b/packages/shared-ui/src/navigation/PillToolbar.svelte @@ -0,0 +1,94 @@ + + +
+
+ {@render children()} +
+
+ + diff --git a/packages/shared-ui/src/navigation/PillToolbarButton.svelte b/packages/shared-ui/src/navigation/PillToolbarButton.svelte new file mode 100644 index 000000000..6af067a8f --- /dev/null +++ b/packages/shared-ui/src/navigation/PillToolbarButton.svelte @@ -0,0 +1,91 @@ + + + + + diff --git a/packages/shared-ui/src/navigation/PillToolbarDivider.svelte b/packages/shared-ui/src/navigation/PillToolbarDivider.svelte new file mode 100644 index 000000000..0eaf28f9f --- /dev/null +++ b/packages/shared-ui/src/navigation/PillToolbarDivider.svelte @@ -0,0 +1,18 @@ + + +
+ + diff --git a/packages/shared-ui/src/navigation/PillViewSwitcher.svelte b/packages/shared-ui/src/navigation/PillViewSwitcher.svelte new file mode 100644 index 000000000..d462852b2 --- /dev/null +++ b/packages/shared-ui/src/navigation/PillViewSwitcher.svelte @@ -0,0 +1,223 @@ + + +
+ +
+ + + {#each options as option} + + {/each} +
+ + diff --git a/packages/shared-ui/src/navigation/index.ts b/packages/shared-ui/src/navigation/index.ts index 6695e0176..500d8fe2f 100644 --- a/packages/shared-ui/src/navigation/index.ts +++ b/packages/shared-ui/src/navigation/index.ts @@ -5,6 +5,11 @@ export { default as SidebarSection } from './SidebarSection.svelte'; export { default as PillNavigation } from './PillNavigation.svelte'; export { default as PillDropdown } from './PillDropdown.svelte'; export { default as PillTabGroup } from './PillTabGroup.svelte'; +export { default as PillTimeRangeSelector } from './PillTimeRangeSelector.svelte'; +export { default as PillViewSwitcher } from './PillViewSwitcher.svelte'; +export { default as PillToolbar } from './PillToolbar.svelte'; +export { default as PillToolbarButton } from './PillToolbarButton.svelte'; +export { default as PillToolbarDivider } from './PillToolbarDivider.svelte'; export type { NavItem, NavbarProps, diff --git a/packages/shared-ui/src/quick-input/QuickInputBar.svelte b/packages/shared-ui/src/quick-input/QuickInputBar.svelte new file mode 100644 index 000000000..06c37a1fe --- /dev/null +++ b/packages/shared-ui/src/quick-input/QuickInputBar.svelte @@ -0,0 +1,944 @@ + + +
+ + {#if showPanel} +
+ {#if !searchQuery.trim() && quickActions.length > 0} + +
+ {#each quickActions as action, index (action.id)} + + {/each} +
+ {:else if searchQuery.trim()} + + {#if createPreview && onCreate} + + {/if} + + {#if loading} +
+
+ {searchingText} +
+ {:else if results.length === 0 && !createPreview} +
+ {emptyText} +
+ {:else if results.length > 0} +
+ Suchergebnisse +
+ {#each results as item, index (item.id)} + {@const adjustedIndex = createPreview ? index + 1 : index} + + {/each} + {/if} + {/if} +
+ {/if} + + +
+
+ + {#if appIcon === 'check-square' || appIcon === 'todo'} + + {:else if appIcon === 'calendar'} + + {:else if appIcon === 'users' || appIcon === 'contacts'} + + {:else} + + + {/if} + +
+ +
+ +
+ {@html highlightedQuery}  +
+ + +
+ + {#if searchQuery.trim() && onCreate} + + {/if} +
+
+ + diff --git a/packages/shared-ui/src/quick-input/index.ts b/packages/shared-ui/src/quick-input/index.ts new file mode 100644 index 000000000..10c8b448a --- /dev/null +++ b/packages/shared-ui/src/quick-input/index.ts @@ -0,0 +1,2 @@ +export { default as QuickInputBar } from './QuickInputBar.svelte'; +export type { QuickInputItem, QuickAction, CreatePreview } from './types'; diff --git a/packages/shared-ui/src/quick-input/types.ts b/packages/shared-ui/src/quick-input/types.ts new file mode 100644 index 000000000..642a7959d --- /dev/null +++ b/packages/shared-ui/src/quick-input/types.ts @@ -0,0 +1,22 @@ +export interface QuickInputItem { + id: string; + title: string; + subtitle?: string; + icon?: string; + imageUrl?: string; + isFavorite?: boolean; +} + +export interface QuickAction { + id: string; + label: string; + href?: string; + icon: string; + shortcut?: string; + onclick?: () => void; +} + +export interface CreatePreview { + title: string; + subtitle: string; +}