feat(calendar): add immersive/fullscreen mode toggle

- Add ImmersiveModeToggle component with glass-pill styling
- Toggle hides all UI elements (PillNav, DateStrip, TagStrip, InputBar)
- Press F key to toggle immersive mode
- Button positioned at bottom center, transparent until hovered
- Semi-transparent in immersive mode for minimal distraction

🤖 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-15 03:22:30 +01:00
parent 3edb65c2c3
commit a6d439360f
3 changed files with 307 additions and 100 deletions

View file

@ -0,0 +1,116 @@
<script lang="ts">
import { settingsStore } from '$lib/stores/settings.svelte';
import { ChevronDown, ChevronUp } from 'lucide-svelte';
interface Props {
/** Whether to show the toggle (only on main calendar page) */
visible?: boolean;
}
let { visible = true }: Props = $props();
let isImmersive = $derived(settingsStore.immersiveModeEnabled);
function toggle() {
settingsStore.toggleImmersiveMode();
}
</script>
{#if visible}
<button
class="immersive-toggle"
class:immersive={isImmersive}
onclick={toggle}
title={isImmersive ? 'UI anzeigen (F)' : 'UI verstecken (F)'}
>
{#if isImmersive}
<ChevronUp size={20} />
{:else}
<ChevronDown size={20} />
{/if}
</button>
{/if}
<style>
.immersive-toggle {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 24px;
border-radius: 8px 8px 0 0;
border: none;
background: transparent;
color: hsl(var(--color-muted-foreground));
cursor: pointer;
pointer-events: auto;
transition:
opacity 300ms ease,
background 150ms ease,
color 150ms ease;
}
.immersive-toggle:hover {
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);
color: #374151;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
.immersive-toggle:active {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
/* Dark mode hover */
:global(.dark) .immersive-toggle:hover {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #f3f4f6;
}
:global(.dark) .immersive-toggle:active {
background: rgba(255, 255, 255, 0.2);
}
/* Immersive mode: even more subtle, full opacity on hover */
.immersive-toggle.immersive {
opacity: 0.2;
color: hsl(var(--color-muted-foreground));
}
.immersive-toggle.immersive:hover {
opacity: 1;
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);
color: #374151;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
:global(.dark) .immersive-toggle.immersive:hover {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #f3f4f6;
}
/* Mobile adjustments */
@media (max-width: 640px) {
.immersive-toggle {
bottom: env(safe-area-inset-bottom);
}
}
</style>

View file

@ -46,6 +46,9 @@ export interface CalendarAppSettings {
// TagStrip settings
tagStripCollapsed: boolean; // Whether TagStrip is hidden
// Immersive Mode settings
immersiveModeEnabled: boolean; // Fullscreen mode - hides all UI elements
// Birthday settings (cross-app integration with Contacts)
showBirthdays: boolean; // Show contact birthdays in calendar
showBirthdayAge: boolean; // Show age in birthday events
@ -88,6 +91,8 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
dateStripCollapsed: false,
// TagStrip defaults
tagStripCollapsed: true, // Hidden by default
// Immersive Mode defaults
immersiveModeEnabled: false,
// Birthday defaults
showBirthdays: true,
showBirthdayAge: true,
@ -236,6 +241,10 @@ export const settingsStore = {
get tagStripCollapsed() {
return settings.tagStripCollapsed;
},
// Immersive Mode settings
get immersiveModeEnabled() {
return settings.immersiveModeEnabled;
},
// Birthday settings
get showBirthdays() {
return settings.showBirthdays;
@ -307,6 +316,15 @@ export const settingsStore = {
syncToCloud();
},
/**
* Toggle Immersive Mode (fullscreen, hide all UI)
*/
toggleImmersiveMode() {
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
saveSettings(settings);
syncToCloud();
},
/**
* Initialize settings from localStorage
*/

View file

@ -58,6 +58,7 @@
import TagStrip from '$lib/components/calendar/TagStrip.svelte';
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
import ViewModePillContextMenu from '$lib/components/calendar/ViewModePillContextMenu.svelte';
import ImmersiveModeToggle from '$lib/components/calendar/ImmersiveModeToggle.svelte';
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
import type { CalendarViewType } from '@calendar/shared';
@ -413,6 +414,54 @@
}
}
}
// F = Toggle Immersive Mode (no modifier keys)
if (
(event.key === 'f' || event.key === 'F') &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey &&
!event.altKey
) {
event.preventDefault();
settingsStore.toggleImmersiveMode();
}
// Arrow keys for calendar navigation (only on main calendar page, no modifiers)
if (
showCalendarToolbar &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey &&
!event.altKey
) {
if (event.key === 'ArrowLeft') {
event.preventDefault();
viewStore.goToPrevious();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
viewStore.goToNext();
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
// Scroll calendar view up/down - scroll to top/bottom
const scrollContainer = document.querySelector('.carousel-page.current');
if (scrollContainer) {
event.preventDefault();
if (event.key === 'ArrowDown') {
// Scroll to bottom
scrollContainer.scrollTo({
top: scrollContainer.scrollHeight,
behavior: 'smooth',
});
} else {
// Scroll to top
scrollContainer.scrollTo({
top: 0,
behavior: 'smooth',
});
}
}
}
}
}
function handleModeChange(isSidebar: boolean) {
@ -525,128 +574,136 @@
<SplitPaneContainer>
<div class="layout-container">
<PillNavigation
items={navItems}
{prependElements}
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}
>
{#snippet toolbarContent()}
{#if showCalendarToolbar}
<CalendarToolbarContent vertical={true} />
{/if}
{/snippet}
</PillNavigation>
<!-- UI Elements (hidden in immersive mode) -->
{#if !settingsStore.immersiveModeEnabled}
<PillNavigation
items={navItems}
{prependElements}
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}
>
{#snippet toolbarContent()}
{#if showCalendarToolbar}
<CalendarToolbarContent vertical={true} />
{/if}
{/snippet}
</PillNavigation>
<!-- Date strip (only on main calendar page) -->
{#if showCalendarToolbar}
{#if settingsStore.dateStripCollapsed}
<DateStripFab
<!-- Date strip (only on main calendar page) -->
{#if showCalendarToolbar}
{#if settingsStore.dateStripCollapsed}
<DateStripFab
{isSidebarMode}
isToolbarExpanded={!isToolbarCollapsed}
{isMobile}
hasTagStrip={!settingsStore.tagStripCollapsed}
/>
{:else}
<DateStrip
{isSidebarMode}
isToolbarExpanded={!isToolbarCollapsed}
hasTagStrip={!settingsStore.tagStripCollapsed}
/>
{/if}
{/if}
<!-- Tag strip (only on main calendar page, when not collapsed) - directly above PillNav -->
{#if showCalendarToolbar && !settingsStore.tagStripCollapsed}
<TagStrip {isSidebarMode} />
{/if}
<!-- Calendar toolbar (only on main calendar page, not in sidebar mode) -->
{#if showCalendarToolbar && !isSidebarMode}
<CalendarToolbar
{isSidebarMode}
isToolbarExpanded={!isToolbarCollapsed}
isCollapsed={isToolbarCollapsed}
{isMobile}
hasTagStrip={!settingsStore.tagStripCollapsed}
/>
{:else}
<DateStrip
{isSidebarMode}
isToolbarExpanded={!isToolbarCollapsed}
hasTagStrip={!settingsStore.tagStripCollapsed}
bottomOffset={settingsStore.tagStripCollapsed ? '70px' : '140px'}
onModeChange={handleToolbarModeChange}
onCollapsedChange={handleToolbarCollapsedChange}
/>
{/if}
{/if}
<!-- Tag strip (only on main calendar page, when not collapsed) - directly above PillNav -->
{#if showCalendarToolbar && !settingsStore.tagStripCollapsed}
<TagStrip {isSidebarMode} />
{/if}
<!-- Calendar toolbar (only on main calendar page, not in sidebar mode) -->
{#if showCalendarToolbar && !isSidebarMode}
<CalendarToolbar
{isSidebarMode}
isCollapsed={isToolbarCollapsed}
{isMobile}
bottomOffset={settingsStore.tagStripCollapsed ? '70px' : '140px'}
onModeChange={handleToolbarModeChange}
onCollapsedChange={handleToolbarCollapsedChange}
<!-- 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"
bottomOffset={isMobile
? `${70 + tagStripOffset}px`
: isSidebarMode
? `${tagStripOffset}px`
: showCalendarToolbar && !isToolbarCollapsed
? `${140 + tagStripOffset}px`
: `${70 + tagStripOffset}px`}
hasFabRight={showCalendarToolbar && !isSidebarMode}
hasFabLeft={!isMobile &&
showCalendarToolbar &&
!isSidebarMode &&
settingsStore.dateStripCollapsed}
defaultOptions={calendarOptions}
selectedDefaultId={selectedDefaultCalendarId}
defaultOptionLabel="Standard-Kalender"
onDefaultChange={handleDefaultCalendarChange}
onShowShortcuts={handleShowShortcuts}
onShowSyntaxHelp={handleShowSyntaxHelp}
/>
{/if}
<!-- Immersive Mode Toggle (always visible on main calendar page) -->
<ImmersiveModeToggle visible={showCalendarToolbar} />
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
class:has-toolbar={showCalendarToolbar}
class:immersive={settingsStore.immersiveModeEnabled}
>
<div
class="content-wrapper"
class:calendar-expanded={settingsStore.sidebarCollapsed && $page.url.pathname === '/'}
class:immersive={settingsStore.immersiveModeEnabled}
>
{@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"
bottomOffset={isMobile
? `${70 + tagStripOffset}px`
: isSidebarMode
? `${tagStripOffset}px`
: showCalendarToolbar && !isToolbarCollapsed
? `${140 + tagStripOffset}px`
: `${70 + tagStripOffset}px`}
hasFabRight={showCalendarToolbar && !isSidebarMode}
hasFabLeft={!isMobile &&
showCalendarToolbar &&
!isSidebarMode &&
settingsStore.dateStripCollapsed}
defaultOptions={calendarOptions}
selectedDefaultId={selectedDefaultCalendarId}
defaultOptionLabel="Standard-Kalender"
onDefaultChange={handleDefaultCalendarChange}
onShowShortcuts={handleShowShortcuts}
onShowSyntaxHelp={handleShowSyntaxHelp}
/>
</div>
</SplitPaneContainer>
@ -773,6 +830,22 @@
min-height: 0;
}
/* Immersive Mode - fullscreen, no UI elements visible */
.main-content.immersive {
padding: 0 !important;
height: 100vh !important;
width: 100vw;
transition: padding 300ms ease;
}
.content-wrapper.immersive {
padding: 0 !important;
margin: 0;
height: 100vh;
width: 100vw;
max-width: 100vw;
}
/* Adjust InputBar when FABs are visible (toolbar FAB on right, DateStripFab on left) */
/* For a centered InputBar with max-width 450px, left edge is at 50% - 225px */
/* DateStripFab is positioned at: 50% - 225px - 8px gap - 54px fab width */