mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 21:01:25 +02:00
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:
parent
f51708d75a
commit
f2ac3e245e
27 changed files with 2770 additions and 531 deletions
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue