feat: implement sidebar resizing functionality across calendar, contacts, and settings pages
This commit is contained in:
@@ -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()}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"); }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user