feat: implement sidebar resizing functionality across calendar, contacts, and settings pages

This commit is contained in:
Linus Rath
2026-03-16 02:32:22 +01:00
parent e02477323f
commit 800499f54a
5 changed files with 105 additions and 29 deletions
+27 -1
View File
@@ -30,6 +30,8 @@ import { ICalImportModal } from "@/components/calendar/ical-import-modal";
import { ICalSubscriptionModal } from "@/components/calendar/ical-subscription-modal"; import { ICalSubscriptionModal } from "@/components/calendar/ical-subscription-modal";
import { RecurrenceScopeDialog, type RecurrenceEditScope } from "@/components/calendar/recurrence-scope-dialog"; import { RecurrenceScopeDialog, type RecurrenceEditScope } from "@/components/calendar/recurrence-scope-dialog";
import { NavigationRail } from "@/components/layout/navigation-rail"; import { NavigationRail } from "@/components/layout/navigation-rail";
import { ResizeHandle } from "@/components/layout/resize-handle";
import { cn } from "@/lib/utils";
import type { CalendarEvent, CalendarParticipant } from "@/lib/jmap/types"; import type { CalendarEvent, CalendarParticipant } from "@/lib/jmap/types";
import { getUserParticipantId } from "@/lib/calendar-participants"; import { getUserParticipantId } from "@/lib/calendar-participants";
import { debug } from "@/lib/debug"; import { debug } from "@/lib/debug";
@@ -76,6 +78,13 @@ export default function CalendarPage() {
const [detailAnchorRect, setDetailAnchorRect] = useState<DOMRect | null>(null); const [detailAnchorRect, setDetailAnchorRect] = useState<DOMRect | null>(null);
const hasFetched = useRef(false); const hasFetched = useRef(false);
// Sidebar resize state
const [calSidebarWidth, setCalSidebarWidth] = useState(() => {
try { const v = localStorage.getItem("calendar-sidebar-width"); return v ? Number(v) : 256; } catch { return 256; }
});
const [isResizing, setIsResizing] = useState(false);
const dragStartWidth = useRef(256);
// Swipe navigation ref (handlers defined after navigatePrev/navigateNext) // Swipe navigation ref (handlers defined after navigatePrev/navigateNext)
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null); const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);
@@ -721,7 +730,14 @@ export default function CalendarPage() {
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
> >
{!isMobile && ( {!isMobile && (
<div className="w-60 border-r border-border p-3 overflow-y-auto flex-shrink-0"> <>
<div
className={cn(
"border-r border-border bg-secondary overflow-y-auto flex-shrink-0 p-3",
!isResizing && "transition-[width] duration-300"
)}
style={{ width: `${calSidebarWidth}px` }}
>
<MiniCalendar <MiniCalendar
selectedDate={selectedDate} selectedDate={selectedDate}
displayMonth={miniMonth} displayMonth={miniMonth}
@@ -741,6 +757,16 @@ export default function CalendarPage() {
client={client} client={client}
/> />
</div> </div>
<ResizeHandle
onResizeStart={() => { 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"); }}
/>
</>
)} )}
{renderView()} {renderView()}
+29 -4
View File
@@ -20,6 +20,7 @@ import { useEmailStore } from "@/stores/email-store";
import { toast } from "@/stores/toast-store"; import { toast } from "@/stores/toast-store";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { NavigationRail } from "@/components/layout/navigation-rail"; import { NavigationRail } from "@/components/layout/navigation-rail";
import { ResizeHandle } from "@/components/layout/resize-handle";
import { useIsMobile } from "@/hooks/use-media-query"; import { useIsMobile } from "@/hooks/use-media-query";
import type { ContactCard } from "@/lib/jmap/types"; import type { ContactCard } from "@/lib/jmap/types";
@@ -75,6 +76,13 @@ export default function ContactsPage() {
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// Sidebar resize state
const [contactsSidebarWidth, setContactsSidebarWidth] = useState(() => {
try { const v = localStorage.getItem("contacts-sidebar-width"); return v ? Number(v) : 256; } catch { return 256; }
});
const [isResizing, setIsResizing] = useState(false);
const dragStartWidth = useRef(256);
// Check auth on mount // Check auth on mount
useEffect(() => { useEffect(() => {
checkAuth().finally(() => { checkAuth().finally(() => {
@@ -442,10 +450,15 @@ export default function ContactsPage() {
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0">
<div className="flex flex-1 min-h-0"> <div className="flex flex-1 min-h-0">
{showListPanel && ( {showListPanel && (
<div className={cn( <>
"border-r border-border flex flex-col flex-shrink-0", <div
isMobile ? "w-full" : "w-80" className={cn(
)}> "border-r border-border bg-secondary flex flex-col flex-shrink-0",
isMobile ? "w-full" : "",
!isResizing && !isMobile && "transition-[width] duration-300"
)}
style={!isMobile ? { width: `${contactsSidebarWidth}px` } : undefined}
>
<div className="flex border-b border-border"> <div className="flex border-b border-border">
<button <button
onClick={() => setActiveTab("all")} onClick={() => setActiveTab("all")}
@@ -507,6 +520,18 @@ export default function ContactsPage() {
/> />
)} )}
</div> </div>
{!isMobile && (
<ResizeHandle
onResizeStart={() => { dragStartWidth.current = contactsSidebarWidth; setIsResizing(true); }}
onResize={(delta) => setContactsSidebarWidth(Math.max(180, Math.min(400, dragStartWidth.current + delta)))}
onResizeEnd={() => {
setIsResizing(false);
localStorage.setItem("contacts-sidebar-width", String(contactsSidebarWidth));
}}
onDoubleClick={() => { setContactsSidebarWidth(256); localStorage.setItem("contacts-sidebar-width", "256"); }}
/>
)}
</>
)} )}
{showRightPanel && ( {showRightPanel && (
+27 -2
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useRouter } from '@/i18n/navigation'; import { useRouter } from '@/i18n/navigation';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { import {
@@ -44,6 +44,7 @@ import { useAuthStore } from '@/stores/auth-store';
import { useEmailStore } from '@/stores/email-store'; import { useEmailStore } from '@/stores/email-store';
import { useIsDesktop } from '@/hooks/use-media-query'; import { useIsDesktop } from '@/hooks/use-media-query';
import { NavigationRail } from '@/components/layout/navigation-rail'; import { NavigationRail } from '@/components/layout/navigation-rail';
import { ResizeHandle } from '@/components/layout/resize-handle';
import { useConfig } from '@/hooks/use-config'; import { useConfig } from '@/hooks/use-config';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -94,6 +95,13 @@ export default function SettingsPage() {
const [mobileShowContent, setMobileShowContent] = useState(false); const [mobileShowContent, setMobileShowContent] = useState(false);
const isDesktop = useIsDesktop(); const isDesktop = useIsDesktop();
// Sidebar resize state
const [settingsSidebarWidth, setSettingsSidebarWidth] = useState(() => {
try { const v = localStorage.getItem('settings-sidebar-width'); return v ? Number(v) : 256; } catch { return 256; }
});
const [isResizing, setIsResizing] = useState(false);
const dragStartWidth = useRef(256);
// Check auth on mount // Check auth on mount
useEffect(() => { useEffect(() => {
checkAuth().finally(() => { checkAuth().finally(() => {
@@ -286,7 +294,13 @@ export default function SettingsPage() {
</div> </div>
{/* Settings Sidebar */} {/* Settings Sidebar */}
<div className="w-64 border-r border-border bg-secondary flex flex-col"> <div
className={cn(
"border-r border-border bg-secondary flex flex-col",
!isResizing && "transition-[width] duration-300"
)}
style={{ width: `${settingsSidebarWidth}px` }}
>
{/* Header */} {/* Header */}
<div className="p-4 border-b border-border"> <div className="p-4 border-b border-border">
<Button <Button
@@ -338,6 +352,17 @@ export default function SettingsPage() {
</div> </div>
</div> </div>
{/* Sidebar resize handle */}
<ResizeHandle
onResizeStart={() => { dragStartWidth.current = settingsSidebarWidth; setIsResizing(true); }}
onResize={(delta) => setSettingsSidebarWidth(Math.max(180, Math.min(400, dragStartWidth.current + delta)))}
onResizeEnd={() => {
setIsResizing(false);
localStorage.setItem('settings-sidebar-width', String(settingsSidebarWidth));
}}
onDoubleClick={() => { setSettingsSidebarWidth(256); localStorage.setItem('settings-sidebar-width', '256'); }}
/>
{/* Settings Content */} {/* Settings Content */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<div className="max-w-3xl mx-auto p-8"> <div className="max-w-3xl mx-auto p-8">
+3 -3
View File
@@ -348,10 +348,10 @@ export function FileBrowser({
const saved = localStorage.getItem("files-sidebar-width"); const saved = localStorage.getItem("files-sidebar-width");
if (saved) return Math.max(180, Math.min(400, Number(saved))); if (saved) return Math.max(180, Math.min(400, Number(saved)));
} }
return 220; return 256;
}); });
const [isResizing, setIsResizing] = useState(false); const [isResizing, setIsResizing] = useState(false);
const dragStartWidth = useRef(220); const dragStartWidth = useRef(256);
const [dragTarget, setDragTarget] = useState<string | null>(null); const [dragTarget, setDragTarget] = useState<string | null>(null);
// Sync showThumbnails and folderLayout when settings change // Sync showThumbnails and folderLayout when settings change
@@ -1104,7 +1104,7 @@ export function FileBrowser({
setIsResizing(false); setIsResizing(false);
localStorage.setItem("files-sidebar-width", String(sidebarWidth)); localStorage.setItem("files-sidebar-width", String(sidebarWidth));
}} }}
onDoubleClick={() => { setSidebarWidth(220); localStorage.setItem("files-sidebar-width", "220"); }} onDoubleClick={() => { setSidebarWidth(256); localStorage.setItem("files-sidebar-width", "256"); }}
/> />
</> </>
)} )}
+1 -1
View File
@@ -26,7 +26,7 @@ interface FolderTreeSidebarProps {
isResizing?: boolean; isResizing?: boolean;
} }
export function FolderTreeSidebar({ currentPath, onNavigate, listByParentId, width = 220, isResizing }: FolderTreeSidebarProps) { export function FolderTreeSidebar({ currentPath, onNavigate, listByParentId, width = 256, isResizing }: FolderTreeSidebarProps) {
const t = useTranslations("files"); const t = useTranslations("files");
const client = useFileStore(s => s.client); const client = useFileStore(s => s.client);
const [rootChildren, setRootChildren] = useState<FolderNode[] | null>(null); const [rootChildren, setRootChildren] = useState<FolderNode[] | null>(null);