fix: hover card, preferences modal, timeline scroll, multi-drag
- ResourceHoverCard: add isInitialLoading to useEffect deps so mouseover/mouseout listeners attach after canvas mounts - PreferencesModal: lift prefsOpen state to AppShell, render modal outside sidebar's backdrop-blur stacking context - Timeline page: constrain to max-h-[100dvh] overflow-hidden so horizontal scrollbar is accessible without scrolling to bottom - Multi-drag: pass selectedAllocationIds from ref at drag completion to prevent stale closure in onMultiDragComplete callback Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -15,7 +15,7 @@ const TimelineView = dynamic(
|
||||
|
||||
export default function TimelinePage() {
|
||||
return (
|
||||
<div className="app-page flex h-full flex-col gap-5 pb-6">
|
||||
<div className="app-page flex max-h-[100dvh] flex-col gap-5 overflow-hidden pb-6">
|
||||
<div className="app-page-header">
|
||||
<div>
|
||||
<h1 className="app-page-title">Timeline</h1>
|
||||
|
||||
@@ -295,15 +295,16 @@ function SidebarContent({
|
||||
sidebarCollapsed,
|
||||
onToggleCollapse,
|
||||
onNavClick,
|
||||
onPrefsOpen,
|
||||
}: {
|
||||
userRole: string;
|
||||
onChatOpen: () => void;
|
||||
sidebarCollapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
onNavClick?: () => void;
|
||||
onPrefsOpen: () => void;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const [prefsOpen, setPrefsOpen] = useState(false);
|
||||
|
||||
const activeHrefSet = useMemo(() => {
|
||||
const set = new Set<string>();
|
||||
@@ -594,7 +595,7 @@ function SidebarContent({
|
||||
<NavTooltip label="Preferences" show={sidebarCollapsed}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPrefsOpen(true)}
|
||||
onClick={onPrefsOpen}
|
||||
className={clsx(
|
||||
"flex w-full items-center rounded-2xl text-sm text-gray-700 transition-colors hover:bg-gray-100/90 dark:text-gray-300 dark:hover:bg-slate-900",
|
||||
sidebarCollapsed ? "justify-center px-2 py-2.5" : "gap-3 px-3 py-2.5",
|
||||
@@ -646,7 +647,6 @@ function SidebarContent({
|
||||
</NavTooltip>
|
||||
</div>
|
||||
|
||||
{prefsOpen && <PreferencesModal onClose={() => setPrefsOpen(false)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -660,11 +660,13 @@ function DesktopSidebar({
|
||||
onChatOpen,
|
||||
sidebarCollapsed,
|
||||
onToggleCollapse,
|
||||
onPrefsOpen,
|
||||
}: {
|
||||
userRole: string;
|
||||
onChatOpen: () => void;
|
||||
sidebarCollapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
onPrefsOpen: () => void;
|
||||
}) {
|
||||
return (
|
||||
<nav
|
||||
@@ -679,6 +681,7 @@ function DesktopSidebar({
|
||||
onChatOpen={onChatOpen}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
onPrefsOpen={onPrefsOpen}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
@@ -693,11 +696,13 @@ function MobileSidebar({
|
||||
onClose,
|
||||
userRole,
|
||||
onChatOpen,
|
||||
onPrefsOpen,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
userRole: string;
|
||||
onChatOpen: () => void;
|
||||
onPrefsOpen: () => void;
|
||||
}) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -738,6 +743,10 @@ function MobileSidebar({
|
||||
sidebarCollapsed={false}
|
||||
onToggleCollapse={() => {}}
|
||||
onNavClick={onClose}
|
||||
onPrefsOpen={() => {
|
||||
onPrefsOpen();
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</motion.nav>
|
||||
</>
|
||||
@@ -754,6 +763,7 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
||||
const [chatOpen, setChatOpen] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [prefsOpen, setPrefsOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const contentRef = useRef<HTMLElement>(null);
|
||||
|
||||
@@ -802,6 +812,7 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
||||
onChatOpen={() => setChatOpen(true)}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
onToggleCollapse={handleToggleCollapse}
|
||||
onPrefsOpen={() => setPrefsOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Mobile sidebar overlay */}
|
||||
@@ -810,6 +821,7 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
||||
onClose={() => setMobileOpen(false)}
|
||||
userRole={userRole}
|
||||
onChatOpen={() => setChatOpen(true)}
|
||||
onPrefsOpen={() => setPrefsOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Main content area */}
|
||||
@@ -844,6 +856,7 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{prefsOpen && <PreferencesModal onClose={() => setPrefsOpen(false)} />}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,8 +135,8 @@ export function TimelineView() {
|
||||
return { ...prev, isSelecting: false, selectedAllocationIds: [...ids] };
|
||||
});
|
||||
},
|
||||
onMultiDragComplete: (daysDelta, mode) => {
|
||||
const ids = multiSelectState.selectedAllocationIds;
|
||||
onMultiDragComplete: (daysDelta, mode, selectedIds) => {
|
||||
const ids = selectedIds ?? multiSelectState.selectedAllocationIds;
|
||||
if (ids.length > 0 && daysDelta !== 0) {
|
||||
pushBatchHistoryRef.current(ids, daysDelta, mode);
|
||||
batchShiftMutationOuter.mutate({ allocationIds: ids, daysDelta, mode });
|
||||
@@ -547,7 +547,7 @@ function TimelineViewContent({
|
||||
resourceHoverTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [resourceHover?.resourceId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [resourceHover?.resourceId, isInitialLoading]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// ─── Lazy-extend date range on scroll ─────────────────────────────────────
|
||||
function handleContainerScroll() {
|
||||
|
||||
@@ -178,7 +178,7 @@ export function useTimelineDrag({
|
||||
onRangeSelected?: (info: RangeSelectedInfo) => void;
|
||||
onAllocationMoved?: (snapshot: AllocationMovedSnapshot) => void;
|
||||
onShiftClickAlloc?: (allocationId: string) => void;
|
||||
onMultiDragComplete?: (daysDelta: number, mode: AllocDragMode) => void;
|
||||
onMultiDragComplete?: (daysDelta: number, mode: AllocDragMode, selectedIds?: string[]) => void;
|
||||
}) {
|
||||
const [dragState, setDragState] = useState<DragState>(INITIAL_DRAG_STATE);
|
||||
const [allocDragState, setAllocDragState] = useState<AllocDragState>(INITIAL_ALLOC_DRAG);
|
||||
@@ -394,7 +394,9 @@ export function useTimelineDrag({
|
||||
multiSelectRef.current = { ...multiSelectRef.current, isMultiDragging: false, multiDragDaysDelta: 0 };
|
||||
|
||||
if (finalDelta !== 0) {
|
||||
onMultiDragCompleteRef.current?.(finalDelta, dragMode);
|
||||
// Pass IDs from ref to avoid stale closure in the callback
|
||||
const ids = multiSelectRef.current.selectedAllocationIds;
|
||||
onMultiDragCompleteRef.current?.(finalDelta, dragMode, ids);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user