Resizable calendar sidebar with localStorage persistence
Sidebar width adjustable via click-and-drag (180–400px range, default 224px). Width persists to localStorage across sessions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1bc1e37518
commit
59c89c904c
@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -47,6 +47,51 @@ export default function CalendarPage() {
|
|||||||
const [visibleSharedIds, setVisibleSharedIds] = useState<Set<number>>(new Set());
|
const [visibleSharedIds, setVisibleSharedIds] = useState<Set<number>>(new Set());
|
||||||
const calendarContainerRef = useRef<HTMLDivElement>(null);
|
const calendarContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Resizable sidebar
|
||||||
|
const SIDEBAR_STORAGE_KEY = 'umbra-calendar-sidebar-width';
|
||||||
|
const SIDEBAR_MIN = 180;
|
||||||
|
const SIDEBAR_MAX = 400;
|
||||||
|
const SIDEBAR_DEFAULT = 224; // w-56
|
||||||
|
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||||
|
const saved = localStorage.getItem(SIDEBAR_STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
const n = parseInt(saved, 10);
|
||||||
|
if (!isNaN(n) && n >= SIDEBAR_MIN && n <= SIDEBAR_MAX) return n;
|
||||||
|
}
|
||||||
|
return SIDEBAR_DEFAULT;
|
||||||
|
});
|
||||||
|
const isResizingRef = useRef(false);
|
||||||
|
|
||||||
|
const handleSidebarMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
isResizingRef.current = true;
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startWidth = sidebarWidth;
|
||||||
|
|
||||||
|
const onMouseMove = (ev: MouseEvent) => {
|
||||||
|
const newWidth = Math.min(SIDEBAR_MAX, Math.max(SIDEBAR_MIN, startWidth + (ev.clientX - startX)));
|
||||||
|
setSidebarWidth(newWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isResizingRef.current = false;
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
}, [sidebarWidth]);
|
||||||
|
|
||||||
|
// Persist sidebar width on change
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(sidebarWidth));
|
||||||
|
}, [sidebarWidth]);
|
||||||
|
|
||||||
// Location data for event panel
|
// Location data for event panel
|
||||||
const { data: locations = [] } = useQuery({
|
const { data: locations = [] } = useQuery({
|
||||||
queryKey: ['locations'],
|
queryKey: ['locations'],
|
||||||
@ -423,7 +468,11 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full overflow-hidden animate-fade-in">
|
<div className="flex h-full overflow-hidden animate-fade-in">
|
||||||
<CalendarSidebar onUseTemplate={handleUseTemplate} onSharedVisibilityChange={setVisibleSharedIds} />
|
<CalendarSidebar onUseTemplate={handleUseTemplate} onSharedVisibilityChange={setVisibleSharedIds} width={sidebarWidth} />
|
||||||
|
<div
|
||||||
|
onMouseDown={handleSidebarMouseDown}
|
||||||
|
className="w-1 shrink-0 cursor-col-resize hover:bg-accent/30 active:bg-accent/50 transition-colors duration-150"
|
||||||
|
/>
|
||||||
|
|
||||||
<div ref={calendarContainerRef} className="flex-1 flex flex-col overflow-hidden">
|
<div ref={calendarContainerRef} className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Custom toolbar */}
|
{/* Custom toolbar */}
|
||||||
|
|||||||
@ -14,9 +14,10 @@ import SharedCalendarSection, { loadVisibility, saveVisibility } from './SharedC
|
|||||||
interface CalendarSidebarProps {
|
interface CalendarSidebarProps {
|
||||||
onUseTemplate?: (template: EventTemplate) => void;
|
onUseTemplate?: (template: EventTemplate) => void;
|
||||||
onSharedVisibilityChange?: (visibleIds: Set<number>) => void;
|
onSharedVisibilityChange?: (visibleIds: Set<number>) => void;
|
||||||
|
width: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CalendarSidebar({ onUseTemplate, onSharedVisibilityChange }: CalendarSidebarProps) {
|
export default function CalendarSidebar({ onUseTemplate, onSharedVisibilityChange, width }: CalendarSidebarProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: calendars = [], sharedData: sharedCalendars = [] } = useCalendars();
|
const { data: calendars = [], sharedData: sharedCalendars = [] } = useCalendars();
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
@ -93,7 +94,7 @@ export default function CalendarSidebar({ onUseTemplate, onSharedVisibilityChang
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-56 shrink-0 border-r bg-card flex flex-col">
|
<div className="shrink-0 border-r bg-card flex flex-col" style={{ width }}>
|
||||||
<div className="h-16 px-4 border-b flex items-center justify-between shrink-0">
|
<div className="h-16 px-4 border-b flex items-center justify-between shrink-0">
|
||||||
<span className="text-sm font-semibold font-heading text-foreground">Calendars</span>
|
<span className="text-sm font-semibold font-heading text-foreground">Calendars</span>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user