diff --git a/app/(main)/[locale]/calendar/page.tsx b/app/(main)/[locale]/calendar/page.tsx index 138c8717..aae1f10e 100644 --- a/app/(main)/[locale]/calendar/page.tsx +++ b/app/(main)/[locale]/calendar/page.tsx @@ -17,7 +17,7 @@ import { useSettingsStore } from "@/stores/settings-store"; import { useIdentityStore } from "@/stores/identity-store"; import { useAccountStore } from "@/stores/account-store"; import { toast } from "@/stores/toast-store"; -import { useIsMobile } from "@/hooks/use-media-query"; +import { useIsDesktop, useIsMobile } from "@/hooks/use-media-query"; import { Button } from "@/components/ui/button"; import { CalendarToolbar } from "@/components/calendar/calendar-toolbar"; import { CalendarMonthView } from "@/components/calendar/calendar-month-view"; @@ -76,7 +76,13 @@ export default function CalendarPage() { const t = useTranslations("calendar"); const tWebcalAction = useTranslations("calendar.webcal_action"); const isMobile = useIsMobile(); + const isDesktop = useIsDesktop(); const isEmbedded = useIsEmbedded(); + // When the pane (Pro shell) or window is narrower than `lg`, the sidebar + // collapses into a burger-toggled overlay instead of taking inline space. + const isNarrow = !isDesktop; + const [narrowSidebarOpen, setNarrowSidebarOpen] = useState(false); + useEffect(() => { if (!isNarrow) setNarrowSidebarOpen(false); }, [isNarrow]); const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps(); const { client, isAuthenticated, logout, checkAuth, switchAccount, activeAccountId, isLoading: authLoading } = useAuthStore(); const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client); @@ -406,6 +412,8 @@ export default function CalendarPage() { setMobileReturnToMonth(true); setViewMode("day"); } + // Close the narrow-pane sidebar overlay after the user picks a date. + setNarrowSidebarOpen(false); }, [setSelectedDate, isMobile, normalizedViewMode, setViewMode]); const navigateBackToMonth = useCallback(() => { @@ -1221,7 +1229,7 @@ export default function CalendarPage() { return (
-
+
{/* Left Navigation Rail (hidden when embedded in Pro shell) */} {!isMobile && !isEmbedded && (
@@ -1242,15 +1250,31 @@ export default function CalendarPage() { )} - {/* Sidebar - full height */} - {!isMobile && !inlineApp && ( + {/* Narrow-pane backdrop: dim and close overlay sidebar */} + {isNarrow && narrowSidebarOpen && !inlineApp && ( +
setNarrowSidebarOpen(false)} + /> + )} + + {/* Sidebar - in-flow when desktop pane, overlay when narrow */} + {!inlineApp && ( <>
- { dragStartWidth.current = calSidebarWidth; setIsResizing(true); }} - onResize={(delta) => setCalSidebarWidth(Math.max(180, Math.min(400, dragStartWidth.current + delta)))} - onResizeEnd={() => { - setIsResizing(false); - localStorage.setItem("calendar-sidebar-width", String(calSidebarWidth)); - }} - onDoubleClick={() => { setCalSidebarWidth(256); localStorage.setItem("calendar-sidebar-width", "256"); }} - /> + {!isNarrow && ( + { dragStartWidth.current = calSidebarWidth; setIsResizing(true); }} + onResize={(delta) => setCalSidebarWidth(Math.max(180, Math.min(400, dragStartWidth.current + delta)))} + onResizeEnd={() => { + setIsResizing(false); + localStorage.setItem("calendar-sidebar-width", String(calSidebarWidth)); + }} + onDoubleClick={() => { setCalSidebarWidth(256); localStorage.setItem("calendar-sidebar-width", "256"); }} + /> + )} )} @@ -1343,6 +1369,7 @@ export default function CalendarPage() { selectedCalendarIds={selectedCalendarIds} onToggleVisibility={toggleCalendarVisibility} enableCalendarTasks={enableCalendarTasks} + onMenuClick={isNarrow ? () => setNarrowSidebarOpen(true) : undefined} />
{ if (!isNarrow) setNarrowSidebarOpen(false); }, [isNarrow]); // Panel resize state - sidebar (categories) const [sidebarWidth, setSidebarWidth] = useState(() => { @@ -198,6 +204,7 @@ export default function ContactsPage() { } else { setSelectedGroupId(null); } + setNarrowSidebarOpen(false); }, [clearSelection]); const handleDropContacts = useCallback(async (contactIds: string[], addressBook: AddressBook) => { @@ -689,18 +696,33 @@ export default function ContactsPage() { {inlineApp && ( )} -
+
+ {/* Narrow-pane backdrop for the overlay categories sidebar */} + {isNarrow && narrowSidebarOpen && ( +
setNarrowSidebarOpen(false)} + /> + )} {showListPanel && ( <> - {/* Panel 1: Categories sidebar */} - {!isMobile && ( + {/* Panel 1: Categories sidebar (in-flow on desktop, overlay on narrow) */} + {(!isMobile || isNarrow) && ( <>
setRenamingKeyword(kw)} />
- { sidebarDragStartWidth.current = sidebarWidth; setIsSidebarResizing(true); }} - onResize={(delta) => setSidebarWidth(Math.max(180, Math.min(400, sidebarDragStartWidth.current + delta)))} - onResizeEnd={() => { - setIsSidebarResizing(false); - localStorage.setItem("contacts-sidebar-width", String(sidebarWidth)); - }} - onDoubleClick={() => { setSidebarWidth(256); localStorage.setItem("contacts-sidebar-width", "256"); }} - /> + {!isNarrow && ( + { sidebarDragStartWidth.current = sidebarWidth; setIsSidebarResizing(true); }} + onResize={(delta) => setSidebarWidth(Math.max(180, Math.min(400, sidebarDragStartWidth.current + delta)))} + onResizeEnd={() => { + setIsSidebarResizing(false); + localStorage.setItem("contacts-sidebar-width", String(sidebarWidth)); + }} + onDoubleClick={() => { setSidebarWidth(256); localStorage.setItem("contacts-sidebar-width", "256"); }} + /> + )} )} @@ -780,6 +804,7 @@ export default function ContactsPage() { onEditContact={handleEditContact} onDeleteContact={handleDeleteContact} onAddContactToGroup={handleAddContactToGroup} + onMenuClick={isNarrow ? () => setNarrowSidebarOpen(true) : undefined} />
diff --git a/app/(main)/[locale]/files/page.tsx b/app/(main)/[locale]/files/page.tsx index 22a60880..7b8d22fa 100644 --- a/app/(main)/[locale]/files/page.tsx +++ b/app/(main)/[locale]/files/page.tsx @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; import { useAuthStore, redirectToLogin } from "@/stores/auth-store"; +import { useAccountStore } from "@/stores/account-store"; import { useEmailStore } from "@/stores/email-store"; import { useFileStore } from "@/stores/file-store"; import { toast } from "@/stores/toast-store"; @@ -33,6 +34,9 @@ export default function FilesPage() { const t = useTranslations("files"); const filesEnabled = usePolicyStore((s) => s.isFeatureEnabled('filesEnabled')); const { isAuthenticated, logout, checkAuth, isLoading: authLoading, client } = useAuthStore(); + const activeAccountId = useAuthStore((s) => s.activeAccountId); + const getClientForAccount = useAuthStore((s) => s.getClientForAccount); + const accounts = useAccountStore((s) => s.accounts); const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps(); const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client); const { quota, isPushConnected } = useEmailStore(); @@ -130,13 +134,18 @@ export default function FilesPage() { } }, [initialCheckDone, isAuthenticated, authLoading]); - // Initialize JMAP files client + // Initialize JMAP files client. In the Pro shell, all connected accounts + // are surfaced as top-level folders at the root, so we *don't* auto-attach + // to the active account — the user picks one explicitly. useEffect(() => { - if (isAuthenticated && client && !hasFetched.current) { - hasFetched.current = true; - initClient(client); + if (!isAuthenticated || !client || hasFetched.current) return; + hasFetched.current = true; + if (isEmbedded) { + useFileStore.getState().clearClient(); + } else { + initClient(client, activeAccountId); } - }, [isAuthenticated, client, initClient]); + }, [isAuthenticated, client, initClient, activeAccountId, isEmbedded]); // Intercept browser refresh gestures (F5, Ctrl/Cmd+R, pull-to-refresh) // and refresh files via JMAP instead of reloading the page. @@ -160,6 +169,17 @@ export default function FilesPage() { }, [storeClient, supportsFiles, checkSupport, navigate]); const handleNavigate = useCallback((path: string, resourceId?: string | null) => { + // Pro shell only: the Account breadcrumb segment signals "go to this + // account's filesystem root" via a sentinel, distinguishing it from a + // Home click (which detaches the account and returns to the picker). + if (resourceId === '__account_root__') { + void navigate(null); + return; + } + if (isEmbedded && path === '/' && resourceId === undefined) { + useFileStore.getState().clearClient(); + return; + } if (resourceId !== undefined) { // Direct ID-based navigation (directory click, breadcrumb dropdown folder) navigate(resourceId, path.split('/').pop() || ''); @@ -167,7 +187,7 @@ export default function FilesPage() { // Path-based navigation (breadcrumbs, favorites, recent files) navigateByPath(path); } - }, [navigate, navigateByPath]); + }, [navigate, navigateByPath, isEmbedded]); const handleCreateFolder = useCallback(async (name: string) => { try { @@ -374,6 +394,38 @@ export default function FilesPage() { setShowDetails(v => !v); }, []); + const currentFilesAccountId = useFileStore((s) => s.currentAccountId); + + // Pro shell only: all connected accounts are equal top-level entries at + // the root. The root path "/" itself is a cross-account picker — no + // account's files are shown until the user enters one. + const accountFolders = isEmbedded + ? accounts + .filter((a) => a.isConnected) + .map((a) => ({ + accountId: a.id, + label: a.label || a.email, + email: a.email, + avatarColor: a.avatarColor, + })) + : []; + const isAccountPicker = isEmbedded && currentFilesAccountId === null; + const currentAccountLabel = isEmbedded && currentFilesAccountId + ? (accounts.find((a) => a.id === currentFilesAccountId)?.label + || accounts.find((a) => a.id === currentFilesAccountId)?.email + || null) + : null; + + const handleSelectAccount = useCallback((accountId: string) => { + const nextClient = getClientForAccount(accountId); + if (!nextClient) return; + const store = useFileStore.getState(); + store.initClient(nextClient, accountId); + // Reset supportsFiles so the existing checkSupport effect re-runs for + // the freshly-attached client and triggers the initial navigate(null). + useFileStore.setState({ supportsFiles: null }); + }, [getClientForAccount]); + if (!isAuthenticated) return null; return ( @@ -479,6 +531,10 @@ export default function FilesPage() { showDetails={showDetails} onToggleDetails={handleToggleDetails} detailResource={detailResource} + accountFolders={accountFolders} + onSelectAccount={handleSelectAccount} + accountPickerMode={isAccountPicker} + accountLabel={currentAccountLabel} />
)} diff --git a/components/calendar/calendar-toolbar.tsx b/components/calendar/calendar-toolbar.tsx index 89710a93..c642bd79 100644 --- a/components/calendar/calendar-toolbar.tsx +++ b/components/calendar/calendar-toolbar.tsx @@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from "react"; import { useTranslations, useFormatter } from "next-intl"; import { Button } from "@/components/ui/button"; -import { ChevronLeft, ChevronRight, Plus, Upload, CalendarDays, Globe, ChevronDown, ArrowLeft } from "lucide-react"; +import { ChevronLeft, ChevronRight, Plus, Upload, CalendarDays, Globe, ChevronDown, ArrowLeft, Menu } from "lucide-react"; import { addDays, startOfWeek } from "date-fns"; import { cn } from "@/lib/utils"; import type { CalendarViewMode } from "@/stores/calendar-store"; @@ -26,6 +26,8 @@ interface CalendarToolbarProps { selectedCalendarIds?: string[]; onToggleVisibility?: (id: string) => void; enableCalendarTasks?: boolean; + /** Show a burger button at the start that opens the (overlay) sidebar. */ + onMenuClick?: () => void; } export function CalendarToolbar({ @@ -45,6 +47,7 @@ export function CalendarToolbar({ selectedCalendarIds, onToggleVisibility, enableCalendarTasks, + onMenuClick, }: CalendarToolbarProps) { const t = useTranslations("calendar"); const formatter = useFormatter(); @@ -115,11 +118,32 @@ export function CalendarToolbar({ return (
+ {/* Burger menu (rendered in pages that use a narrow overlay sidebar) */} + {onMenuClick && !isMobile && ( + + )} {/* ── MOBILE TOOLBAR ── */} {isMobile && (
{/* Row 1: Back / Date nav / Today */}
+ {onMenuClick && ( + + )} {onNavigateBack && ( + )} + )} {/* Breadcrumbs */}
+
+ ) : accountPickerMode ? ( +
+

{t("no_accounts")}

+
) : resources.length === 0 && !searchQuery && currentPath === '/' ? ( { diff --git a/components/files/folder-tree-sidebar.tsx b/components/files/folder-tree-sidebar.tsx index d464761d..48013f1c 100644 --- a/components/files/folder-tree-sidebar.tsx +++ b/components/files/folder-tree-sidebar.tsx @@ -130,7 +130,7 @@ export function FolderTreeSidebar({ currentPath, onNavigate, listByParentId, wid return (