mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
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:
parent
3edb65c2c3
commit
a6d439360f
3 changed files with 307 additions and 100 deletions
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue