mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
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:
parent
a6d439360f
commit
893c6ef0fb
7 changed files with 246 additions and 121 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,23 @@
|
|||
<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) */
|
||||
/** Whether immersive mode is currently enabled */
|
||||
isImmersive: boolean;
|
||||
/** Callback to toggle immersive mode */
|
||||
onToggle: () => void;
|
||||
/** Whether to show the toggle (e.g., only on main page) */
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
let { visible = true }: Props = $props();
|
||||
|
||||
let isImmersive = $derived(settingsStore.immersiveModeEnabled);
|
||||
|
||||
function toggle() {
|
||||
settingsStore.toggleImmersiveMode();
|
||||
}
|
||||
let { isImmersive, onToggle, visible = true }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<button
|
||||
class="immersive-toggle"
|
||||
class:immersive={isImmersive}
|
||||
onclick={toggle}
|
||||
onclick={onToggle}
|
||||
title={isImmersive ? 'UI anzeigen (F)' : 'UI verstecken (F)'}
|
||||
>
|
||||
{#if isImmersive}
|
||||
|
|
@ -192,3 +192,6 @@ export type {
|
|||
SyntaxGroup,
|
||||
HelpModalConfig,
|
||||
} from './help';
|
||||
|
||||
// Immersive Mode
|
||||
export { default as ImmersiveModeToggle } from './components/ImmersiveModeToggle.svelte';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue