feat(splitscreen): add split-screen feature for multi-app side-by-side view

Add new @manacore/shared-splitscreen package enabling iFrame-based
split-screen functionality across Calendar, Todo, and Contacts apps.

Features:
- SplitPaneContainer with CSS Grid layout
- AppPanel with iFrame sandbox permissions and loading/error states
- ResizeHandle with mouse, touch, and keyboard support (20-80% range)
- PanelControls for swap and close actions
- Svelte 5 runes-based store with Context API
- URL persistence (?panel=todo&split=60)
- localStorage persistence with versioning
- Mobile auto-disable (<1024px breakpoint)

Integration:
- PillNavigation: added onOpenInPanel prop and Ctrl/Cmd+click support
- PillDropdown: added split button per app item
- Calendar, Todo, Contacts layouts wrapped with SplitPaneContainer

Also fixes:
- WeekView.svelte: fixed {@const} placement error
- MultiDayView.svelte: fixed {@const} placement error

🤖 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-12 13:00:26 +01:00
parent f51708d75a
commit f2ac3e245e
27 changed files with 2770 additions and 531 deletions

View file

@ -32,6 +32,7 @@
"dependencies": {
"@calendar/shared": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-splitscreen": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",

View file

@ -136,7 +136,61 @@
let daysContainerEl: HTMLDivElement;
function getEventsForDay(day: Date) {
return eventsStore.getEventsForDay(day).filter((e) => !e.isAllDay);
const allEvents = eventsStore.getEventsForDay(day).filter((e) => !e.isAllDay);
// If hour filtering is enabled, only show events that overlap with visible range
if (settingsStore.filterHoursEnabled) {
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
return allEvents.filter((event) => {
const start =
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event overlaps with visible range
return eventStartMinutes < visibleEndMinutes && eventEndMinutes > visibleStartMinutes;
});
}
return allEvents;
}
// Get events that are completely outside the visible time range
function getOverflowEventsForDay(day: Date): { before: CalendarEvent[]; after: CalendarEvent[] } {
if (!settingsStore.filterHoursEnabled) {
return { before: [], after: [] };
}
const allEvents = eventsStore.getEventsForDay(day).filter((e) => !e.isAllDay);
const before: CalendarEvent[] = [];
const after: CalendarEvent[] = [];
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
for (const event of allEvents) {
const start =
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event ends before visible range starts
if (eventEndMinutes <= visibleStartMinutes) {
before.push(event);
}
// Event starts after visible range ends
else if (eventStartMinutes >= visibleEndMinutes) {
after.push(event);
}
}
return { before, after };
}
function getAllDayEventsForDay(day: Date) {
@ -961,6 +1015,36 @@
</div>
{/if}
<!-- Overflow indicators for events outside visible time range -->
{#if true}
{@const overflow = getOverflowEventsForDay(day)}
{#if overflow.before.length > 0}
<div class="overflow-indicator top" title="{overflow.before.length} Termin(e) früher">
{#each overflow.before as event}
<div
class="overflow-line"
style="background-color: {calendarsStore.getColor(event.calendarId)}"
title="{formatEventTime(event.startTime)} {event.title}"
></div>
{/each}
</div>
{/if}
{#if overflow.after.length > 0}
<div
class="overflow-indicator bottom"
title="{overflow.after.length} Termin(e) später"
>
{#each overflow.after as event}
<div
class="overflow-line"
style="background-color: {calendarsStore.getColor(event.calendarId)}"
title="{formatEventTime(event.startTime)} {event.title}"
></div>
{/each}
</div>
{/if}
{/if}
<!-- Current time indicator -->
{#if isToday(day)}
<div class="time-indicator" style="top: {currentTimePosition}%"></div>
@ -1290,27 +1374,6 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* Task drag ghost */
.task-drag-ghost {
position: absolute;
left: 2px;
right: 2px;
padding: 4px 6px;
background: hsl(var(--color-surface) / 0.8);
border: 2px dashed hsl(var(--color-primary));
border-radius: var(--radius-sm);
opacity: 0.7;
pointer-events: none;
z-index: 50;
overflow: hidden;
}
.task-drag-ghost .task-title {
font-size: 0.7rem;
font-weight: 500;
color: hsl(var(--color-foreground));
}
/* Sidebar task drop target */
.day-column.drop-target {
background: hsl(var(--color-primary) / 0.15);
@ -1408,4 +1471,49 @@
border-radius: 50%;
background: hsl(var(--color-error));
}
/* Overflow indicators for events outside visible time range */
.overflow-indicator {
position: absolute;
left: 2px;
right: 2px;
display: flex;
flex-direction: column;
gap: 2px;
z-index: 5;
padding: 2px;
}
.overflow-indicator.top {
top: 0;
}
.overflow-indicator.bottom {
bottom: 0;
}
.overflow-line {
height: 3px;
border-radius: 2px;
opacity: 0.7;
cursor: pointer;
transition:
opacity 0.15s ease,
height 0.15s ease;
}
.overflow-line:hover {
opacity: 1;
height: 5px;
}
.compact .overflow-line,
.very-compact .overflow-line {
height: 2px;
}
.compact .overflow-line:hover,
.very-compact .overflow-line:hover {
height: 4px;
}
</style>

View file

@ -135,7 +135,61 @@
let daysContainerEl: HTMLDivElement;
function getEventsForDay(day: Date) {
return eventsStore.getEventsForDay(day).filter((e) => !e.isAllDay);
const allEvents = eventsStore.getEventsForDay(day).filter((e) => !e.isAllDay);
// If hour filtering is enabled, only show events that overlap with visible range
if (settingsStore.filterHoursEnabled) {
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
return allEvents.filter((event) => {
const start =
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event overlaps with visible range
return eventStartMinutes < visibleEndMinutes && eventEndMinutes > visibleStartMinutes;
});
}
return allEvents;
}
// Get events that are completely outside the visible time range
function getOverflowEventsForDay(day: Date): { before: CalendarEvent[]; after: CalendarEvent[] } {
if (!settingsStore.filterHoursEnabled) {
return { before: [], after: [] };
}
const allEvents = eventsStore.getEventsForDay(day).filter((e) => !e.isAllDay);
const before: CalendarEvent[] = [];
const after: CalendarEvent[] = [];
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
for (const event of allEvents) {
const start =
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event ends before visible range starts
if (eventEndMinutes <= visibleStartMinutes) {
before.push(event);
}
// Event starts after visible range ends
else if (eventStartMinutes >= visibleEndMinutes) {
after.push(event);
}
}
return { before, after };
}
function getAllDayEventsForDay(day: Date) {
@ -992,6 +1046,36 @@
</div>
{/if}
<!-- Overflow indicators for events outside visible time range -->
{#if true}
{@const overflow = getOverflowEventsForDay(day)}
{#if overflow.before.length > 0}
<div class="overflow-indicator top" title="{overflow.before.length} Termin(e) früher">
{#each overflow.before as event}
<div
class="overflow-line"
style="background-color: {calendarsStore.getColor(event.calendarId)}"
title="{formatEventTime(event.startTime)} {event.title}"
></div>
{/each}
</div>
{/if}
{#if overflow.after.length > 0}
<div
class="overflow-indicator bottom"
title="{overflow.after.length} Termin(e) später"
>
{#each overflow.after as event}
<div
class="overflow-line"
style="background-color: {calendarsStore.getColor(event.calendarId)}"
title="{formatEventTime(event.startTime)} {event.title}"
></div>
{/each}
</div>
{/if}
{/if}
<!-- Current time indicator -->
{#if isToday(day)}
<div class="time-indicator" style="top: {currentTimePosition}%"></div>
@ -1272,27 +1356,6 @@
filter: grayscale(0.3);
}
/* Task drag ghost */
.task-drag-ghost {
position: absolute;
left: 2px;
right: 2px;
padding: 4px 6px;
background: hsl(var(--color-surface) / 0.8);
border: 2px dashed hsl(var(--color-primary));
border-radius: var(--radius-sm);
opacity: 0.7;
pointer-events: none;
z-index: 50;
overflow: hidden;
}
.task-drag-ghost .task-title {
font-size: 0.7rem;
font-weight: 500;
color: hsl(var(--color-foreground));
}
.event-card.draft {
outline: 2px solid hsl(var(--color-primary));
outline-offset: -1px;
@ -1374,4 +1437,39 @@
background: hsl(var(--color-error));
border-radius: 50%;
}
/* Overflow indicators for events outside visible time range */
.overflow-indicator {
position: absolute;
left: 2px;
right: 2px;
display: flex;
flex-direction: column;
gap: 2px;
z-index: 5;
padding: 2px;
}
.overflow-indicator.top {
top: 0;
}
.overflow-indicator.bottom {
bottom: 0;
}
.overflow-line {
height: 3px;
border-radius: 2px;
opacity: 0.7;
cursor: pointer;
transition:
opacity 0.15s ease,
height 0.15s ease;
}
.overflow-line:hover {
opacity: 1;
height: 5px;
}
</style>

View file

@ -4,6 +4,11 @@
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import {
SplitPaneContainer,
setSplitPanelContext,
DEFAULT_APPS,
} from '@manacore/shared-splitscreen';
import type {
PillNavItem,
PillDropdownItem,
@ -28,6 +33,7 @@
import {
isSidebarMode as sidebarModeStore,
isNavCollapsed as collapsedStore,
isToolbarCollapsed as toolbarCollapsedStore,
} from '$lib/stores/navigation';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems } from '@manacore/shared-branding';
@ -42,11 +48,20 @@
formatParsedEventPreview,
} from '$lib/utils/event-parser';
import CalendarToolbar from '$lib/components/calendar/CalendarToolbar.svelte';
import CalendarToolbarContent from '$lib/components/calendar/CalendarToolbarContent.svelte';
import DateStrip from '$lib/components/calendar/DateStrip.svelte';
// App switcher items
const appItems = getPillAppItems('calendar');
// Split-Panel Store für Split-Screen Feature
const splitPanel = setSplitPanelContext('calendar', DEFAULT_APPS);
// Handler für Split-Screen Panel-Öffnung
function handleOpenInPanel(appId: string, url: string) {
splitPanel.openPanel(appId);
}
let { children } = $props();
// InputBar search - search events
@ -128,6 +143,7 @@
let isSidebarMode = $state(false);
let isCollapsed = $state(false);
let isToolbarCollapsed = $state(false);
// Use theme store's isDark directly
let isDark = $derived(theme.isDark);
@ -234,6 +250,19 @@
}
}
function handleToolbarModeChange(isSidebar: boolean) {
// Sync toolbar mode with nav mode
handleModeChange(isSidebar);
}
function handleToolbarCollapsedChange(collapsed: boolean) {
isToolbarCollapsed = collapsed;
toolbarCollapsedStore.set(collapsed);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('calendar-toolbar-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
theme.toggleMode();
}
@ -254,6 +283,9 @@
return;
}
// Initialize split-panel from URL/localStorage
splitPanel.initialize();
// Initialize view state
viewStore.initialize();
@ -281,86 +313,114 @@
isCollapsed = true;
collapsedStore.set(true);
}
// Initialize toolbar collapsed state from localStorage
const savedToolbarCollapsed = localStorage.getItem('calendar-toolbar-collapsed');
if (savedToolbarCollapsed === 'true') {
isToolbarCollapsed = true;
toolbarCollapsedStore.set(true);
}
});
</script>
<svelte:window onkeydown={handleKeydown} />
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Kalender"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
desktopPosition="bottom"
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#3b82f6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
/>
<!-- Date strip (only on main calendar page) -->
{#if showCalendarToolbar}
<DateStrip />
{/if}
<!-- Calendar toolbar (only on main calendar page) -->
{#if showCalendarToolbar}
<CalendarToolbar />
{/if}
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
class:has-toolbar={showCalendarToolbar}
>
<div
class="content-wrapper"
class:calendar-expanded={settingsStore.sidebarCollapsed && $page.url.pathname === '/'}
<SplitPaneContainer>
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Kalender"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
desktopPosition="bottom"
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#3b82f6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
onOpenInPanel={handleOpenInPanel}
>
{@render children()}
</div>
</main>
{#snippet toolbarContent()}
{#if showCalendarToolbar}
<CalendarToolbarContent vertical={true} />
{/if}
{/snippet}
</PillNavigation>
<!-- Global Input Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
onSearchChange={handleSearchChange}
placeholder="Neuer Termin oder suchen..."
emptyText="Keine Termine gefunden"
searchingText="Suche..."
onCreate={handleCreate}
onParseCreate={handleParseCreate}
createText="Erstellen"
appIcon="calendar"
primaryColor="#3b82f6"
autoFocus={true}
/>
</div>
<!-- Date strip (only on main calendar page) -->
{#if showCalendarToolbar}
<DateStrip {isSidebarMode} />
{/if}
<!-- Calendar toolbar (only on main calendar page, not in sidebar mode) -->
{#if showCalendarToolbar && !isSidebarMode}
<CalendarToolbar
{isSidebarMode}
isCollapsed={isToolbarCollapsed}
onModeChange={handleToolbarModeChange}
onCollapsedChange={handleToolbarCollapsedChange}
/>
{/if}
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
class:has-toolbar={showCalendarToolbar}
>
<div
class="content-wrapper"
class:calendar-expanded={settingsStore.sidebarCollapsed && $page.url.pathname === '/'}
>
{@render children()}
</div>
</main>
<!-- Global Input Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
onSearchChange={handleSearchChange}
placeholder="Neuer Termin oder suchen..."
emptyText="Keine Termine gefunden"
searchingText="Suche..."
onCreate={handleCreate}
onParseCreate={handleParseCreate}
createText="Erstellen"
appIcon="calendar"
primaryColor="#3b82f6"
autoFocus={true}
bottomOffset={showCalendarToolbar
? isSidebarMode
? '0px'
: '130px'
: isSidebarMode
? '0px'
: '70px'}
/>
</div>
</SplitPaneContainer>
<style>
.layout-container {

View file

@ -31,6 +31,7 @@
},
"dependencies": {
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-splitscreen": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",

View file

@ -4,6 +4,11 @@
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import {
SplitPaneContainer,
setSplitPanelContext,
DEFAULT_APPS,
} from '@manacore/shared-splitscreen';
import type {
PillNavItem,
PillDropdownItem,
@ -50,6 +55,14 @@
// App switcher items
const appItems = getPillAppItems('contacts');
// Split-Panel Store für Split-Screen Feature
const splitPanel = setSplitPanelContext('contacts', DEFAULT_APPS);
// Handler für Split-Screen Panel-Öffnung
function handleOpenInPanel(appId: string, url: string) {
splitPanel.openPanel(appId);
}
let { children } = $props();
let isSidebarMode = $state(false);
@ -254,6 +267,9 @@
return;
}
// Initialize split-panel from URL/localStorage
splitPanel.initialize();
// Load user settings and tags
await userSettings.load();
@ -287,78 +303,81 @@
<svelte:window onkeydown={handleKeydown} />
<!-- Navigation Layout -->
<div class="layout-container">
<!-- Shadow gradient above navigation -->
<div class="nav-shadow-gradient"></div>
<SplitPaneContainer>
<!-- Navigation Layout -->
<div class="layout-container">
<!-- Shadow gradient above navigation -->
<div class="nav-shadow-gradient"></div>
<!-- Floating/Sidebar Pill Navigation -->
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Contacts"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
desktopPosition={userSettings.nav.desktopPosition}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#3b82f6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
/>
<!-- Floating/Sidebar Pill Navigation -->
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Contacts"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
desktopPosition={userSettings.nav.desktopPosition}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#3b82f6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
onOpenInPanel={handleOpenInPanel}
/>
<!-- Main Content with dynamic padding based on nav mode -->
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode}
>
<div class="content-wrapper">
{@render children()}
</div>
</main>
<!-- Main Content with dynamic padding based on nav mode -->
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode}
>
<div class="content-wrapper">
{@render children()}
</div>
</main>
<!-- Contact Detail Modal -->
{#if showContactModal && modalContactId}
<ContactDetailModal contactId={modalContactId} onClose={handleCloseContactModal} />
{/if}
<!-- Contact Detail Modal -->
{#if showContactModal && modalContactId}
<ContactDetailModal contactId={modalContactId} onClose={handleCloseContactModal} />
{/if}
<!-- Global Quick Input Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
{quickActions}
placeholder="Neuer Kontakt oder suchen..."
emptyText="Keine Kontakte gefunden"
searchingText="Suche..."
onCreate={handleCreate}
onParseCreate={handleParseCreate}
createText="Erstellen"
appIcon="contacts"
primaryColor="#3b82f6"
autoFocus={false}
/>
</div>
<!-- Global Quick Input Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
{quickActions}
placeholder="Neuer Kontakt oder suchen..."
emptyText="Keine Kontakte gefunden"
searchingText="Suche..."
onCreate={handleCreate}
onParseCreate={handleParseCreate}
createText="Erstellen"
appIcon="contacts"
primaryColor="#3b82f6"
autoFocus={false}
/>
</div>
</SplitPaneContainer>
<style>
.layout-container {

View file

@ -30,6 +30,7 @@
},
"dependencies": {
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-splitscreen": "workspace:*",
"@manacore/shared-types": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"@manacore/shared-tags": "workspace:*",

View file

@ -4,6 +4,11 @@
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import {
SplitPaneContainer,
setSplitPanelContext,
DEFAULT_APPS,
} from '@manacore/shared-splitscreen';
import type {
PillNavItem,
PillDropdownItem,
@ -36,6 +41,14 @@
// App switcher items
const appItems = getPillAppItems('todo');
// Split-Panel Store für Split-Screen Feature
const splitPanel = setSplitPanelContext('todo', DEFAULT_APPS);
// Handler für Split-Screen Panel-Öffnung
function handleOpenInPanel(appId: string, url: string) {
splitPanel.openPanel(appId);
}
let { children } = $props();
// QuickInputBar quick actions
@ -246,6 +259,9 @@
return;
}
// Initialize split-panel from URL/localStorage
splitPanel.initialize();
// Load data
await Promise.all([
projectsStore.fetchProjects(),
@ -310,67 +326,70 @@
<svelte:window onkeydown={handleKeydown} />
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Todo"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
desktopPosition={userSettings.nav.desktopPosition}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#8b5cf6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
/>
<SplitPaneContainer>
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Todo"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isSidebarMode}
onModeChange={handleModeChange}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
desktopPosition={userSettings.nav.desktopPosition}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#8b5cf6"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
allAppsHref="/apps"
onOpenInPanel={handleOpenInPanel}
/>
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
>
<div class="content-wrapper" class:full-width={$page.url.pathname === '/kanban'}>
{@render children()}
</div>
</main>
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
>
<div class="content-wrapper" class:full-width={$page.url.pathname === '/kanban'}>
{@render children()}
</div>
</main>
<!-- Global Quick Input Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
{quickActions}
placeholder="Neue Aufgabe oder suchen..."
emptyText="Keine Aufgaben gefunden"
searchingText="Suche..."
onCreate={handleCreate}
onParseCreate={handleParseCreate}
createText="Erstellen"
appIcon="todo"
primaryColor="#8b5cf6"
autoFocus={true}
/>
</div>
<!-- Global Quick Input Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
{quickActions}
placeholder="Neue Aufgabe oder suchen..."
emptyText="Keine Aufgaben gefunden"
searchingText="Suche..."
onCreate={handleCreate}
onParseCreate={handleParseCreate}
createText="Erstellen"
appIcon="todo"
primaryColor="#8b5cf6"
autoFocus={true}
/>
</div>
</SplitPaneContainer>
<style>
.layout-container {