feat(shared-ui): unify ImmersiveModeToggle across Calendar, Contacts, and Todo apps

- Move ImmersiveModeToggle component from Calendar to shared-ui package
- Add immersiveModeEnabled setting to Contacts and Todo settings stores
- Update all three app layouts with F-key shortcut and conditional UI rendering
- Consistent glass-pill styling on hover for toggle button

🤖 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:34:15 +01:00
parent a6d439360f
commit 893c6ef0fb
7 changed files with 246 additions and 121 deletions

View file

@ -1,116 +0,0 @@
<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

@ -3,7 +3,12 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar, InputBarHelpModal } from '@manacore/shared-ui';
import {
PillNavigation,
QuickInputBar,
InputBarHelpModal,
ImmersiveModeToggle,
} from '@manacore/shared-ui';
import {
SplitPaneContainer,
setSplitPanelContext,
@ -58,7 +63,6 @@
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';
@ -687,7 +691,11 @@
{/if}
<!-- Immersive Mode Toggle (always visible on main calendar page) -->
<ImmersiveModeToggle visible={showCalendarToolbar} />
<ImmersiveModeToggle
isImmersive={settingsStore.immersiveModeEnabled}
onToggle={() => settingsStore.toggleImmersiveMode()}
visible={showCalendarToolbar}
/>
<main
class="main-content bg-background"

View file

@ -65,6 +65,10 @@ export interface ContactsAppSettings {
alphabetNavReverseOrder: boolean;
/** Show # symbol for non-letter names */
alphabetNavShowHash: boolean;
// Immersive Mode
/** Fullscreen mode - hides all UI elements */
immersiveModeEnabled: boolean;
}
const DEFAULT_SETTINGS: ContactsAppSettings = {
@ -100,6 +104,9 @@ const DEFAULT_SETTINGS: ContactsAppSettings = {
alphabetNavCompact: false,
alphabetNavReverseOrder: false,
alphabetNavShowHash: true,
// Immersive Mode
immersiveModeEnabled: false,
};
const STORAGE_KEY = 'contacts-settings';
@ -217,6 +224,19 @@ export const contactsSettings = {
return settings.alphabetNavShowHash;
},
// Immersive Mode
get immersiveModeEnabled() {
return settings.immersiveModeEnabled;
},
/**
* Toggle Immersive Mode (fullscreen, hide all UI)
*/
toggleImmersiveMode() {
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
saveSettings(settings);
},
/**
* Initialize settings from localStorage
*/

View file

@ -3,7 +3,7 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
import {
SplitPaneContainer,
setSplitPanelContext,
@ -168,6 +168,18 @@
}
}
}
// F = Toggle Immersive Mode (no modifier keys)
if (
(event.key === 'f' || event.key === 'F') &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey &&
!event.altKey
) {
event.preventDefault();
contactsSettings.toggleImmersiveMode();
}
}
function handleModeChange(isSidebar: boolean) {
@ -305,40 +317,70 @@
<SplitPaneContainer>
<!-- Navigation Layout -->
<div class="layout-container">
<!-- Floating/Sidebar Pill Navigation (at bottom) -->
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Contacts"
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}
<!-- UI Elements (hidden in immersive mode) -->
{#if !contactsSettings.immersiveModeEnabled}
<!-- Floating/Sidebar Pill Navigation (at bottom) -->
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Contacts"
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}
/>
<!-- Global Quick Input Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
onSearchChange={(query) => contactsFilterStore.setSearchQuery(query)}
placeholder="Neuer Kontakt oder suchen..."
emptyText="Keine Kontakte gefunden"
searchingText="Suche..."
onCreate={handleCreate}
onParseCreate={handleParseCreate}
createText="Erstellen"
appIcon="contacts"
bottomOffset={inputBarBottomOffset}
hasFabRight={showContactsToolbar}
/>
<!-- Contacts Toolbar (FAB + expandable bar) - only on main page -->
{#if showContactsToolbar}
<ContactsToolbar {isSidebarMode} contacts={contactsStore.contacts} />
{/if}
{/if}
<!-- Immersive Mode Toggle (always visible) -->
<ImmersiveModeToggle
isImmersive={contactsSettings.immersiveModeEnabled}
onToggle={() => contactsSettings.toggleImmersiveMode()}
/>
<!-- Main Content with dynamic padding based on nav mode -->
@ -346,8 +388,9 @@
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode}
class:immersive={contactsSettings.immersiveModeEnabled}
>
<div class="content-wrapper">
<div class="content-wrapper" class:immersive={contactsSettings.immersiveModeEnabled}>
{@render children()}
</div>
</main>
@ -361,27 +404,6 @@
{#if newContactModalStore.isOpen}
<NewContactModal onClose={() => newContactModalStore.close()} />
{/if}
<!-- Global Quick Input Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
onSearchChange={(query) => contactsFilterStore.setSearchQuery(query)}
placeholder="Neuer Kontakt oder suchen..."
emptyText="Keine Kontakte gefunden"
searchingText="Suche..."
onCreate={handleCreate}
onParseCreate={handleParseCreate}
createText="Erstellen"
appIcon="contacts"
bottomOffset={inputBarBottomOffset}
hasFabRight={showContactsToolbar}
/>
<!-- Contacts Toolbar (FAB + expandable bar) - only on main page -->
{#if showContactsToolbar}
<ContactsToolbar {isSidebarMode} contacts={contactsStore.contacts} />
{/if}
</div>
</SplitPaneContainer>
@ -416,6 +438,18 @@
padding-left: 180px;
}
/* Immersive mode - fullscreen, no padding */
.main-content.immersive {
padding: 0 !important;
height: 100vh;
width: 100vw;
}
.content-wrapper.immersive {
padding: 0;
height: 100%;
}
.content-wrapper {
/* No max-width - let individual views control their own width */
padding: 1rem;

View file

@ -58,6 +58,10 @@ export interface TodoAppSettings {
dailyGoal: number | null;
/** Show productivity streak */
showStreak: boolean;
// Immersive Mode
/** Fullscreen mode - hides all UI elements */
immersiveModeEnabled: boolean;
}
const DEFAULT_SETTINGS: TodoAppSettings = {
@ -89,6 +93,9 @@ const DEFAULT_SETTINGS: TodoAppSettings = {
pomodoroEnabled: false,
dailyGoal: null,
showStreak: false,
// Immersive Mode
immersiveModeEnabled: false,
};
const STORAGE_KEY = 'todo-settings';
@ -198,6 +205,19 @@ export const todoSettings = {
return settings.showStreak;
},
// Immersive Mode
get immersiveModeEnabled() {
return settings.immersiveModeEnabled;
},
/**
* Toggle Immersive Mode (fullscreen, hide all UI)
*/
toggleImmersiveMode() {
settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled };
saveSettings(settings);
},
/**
* Initialize settings from localStorage
*/

View file

@ -3,7 +3,7 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
import {
SplitPaneContainer,
setSplitPanelContext,
@ -18,6 +18,7 @@
} from '@manacore/shared-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { todoSettings } from '$lib/stores/settings.svelte';
import { projectsStore } from '$lib/stores/projects.svelte';
import { labelsStore } from '$lib/stores/labels.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
@ -215,6 +216,18 @@
}
}
}
// F = Toggle Immersive Mode (no modifier keys)
if (
(event.key === 'f' || event.key === 'F') &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey &&
!event.altKey
) {
event.preventDefault();
todoSettings.toggleImmersiveMode();
}
}
function handleModeChange(isSidebar: boolean) {
@ -262,6 +275,9 @@
// Initialize split-panel from URL/localStorage
splitPanel.initialize();
// Initialize todo settings
todoSettings.initialize();
// Load data
await Promise.all([
projectsStore.fetchProjects(),
@ -328,66 +344,80 @@
<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}
<!-- UI Elements (hidden in immersive mode) -->
{#if !todoSettings.immersiveModeEnabled}
<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}
/>
<!-- 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}
/>
{/if}
<!-- Immersive Mode Toggle (always visible) -->
<ImmersiveModeToggle
isImmersive={todoSettings.immersiveModeEnabled}
onToggle={() => todoSettings.toggleImmersiveMode()}
/>
<main
class="main-content bg-background"
class:sidebar-mode={isSidebarMode && !isCollapsed}
class:floating-mode={!isSidebarMode && !isCollapsed}
class:immersive={todoSettings.immersiveModeEnabled}
>
<div class="content-wrapper" class:full-width={$page.url.pathname === '/kanban'}>
<div
class="content-wrapper"
class:full-width={$page.url.pathname === '/kanban'}
class:immersive={todoSettings.immersiveModeEnabled}
>
{@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>
</SplitPaneContainer>
@ -414,6 +444,19 @@
padding-left: 180px;
}
/* Immersive mode - fullscreen, no padding */
.main-content.immersive {
padding: 0 !important;
height: 100vh;
width: 100vw;
}
.content-wrapper.immersive {
padding: 0;
max-width: none;
height: 100%;
}
.content-wrapper {
max-width: 900px;
margin-left: auto;