import { useCallback, useEffect, useRef, useState, type UIEvent } from "react"; import { useQuery } from "@tanstack/react-query"; import { BookOpen, Moon, Sun } from "lucide-react"; import { Outlet } from "react-router-dom"; import { CompanyRail } from "./CompanyRail"; import { Sidebar } from "./Sidebar"; import { SidebarNavItem } from "./SidebarNavItem"; import { BreadcrumbBar } from "./BreadcrumbBar"; import { PropertiesPanel } from "./PropertiesPanel"; import { CommandPalette } from "./CommandPalette"; import { NewIssueDialog } from "./NewIssueDialog"; import { NewProjectDialog } from "./NewProjectDialog"; import { NewGoalDialog } from "./NewGoalDialog"; import { NewAgentDialog } from "./NewAgentDialog"; import { OnboardingWizard } from "./OnboardingWizard"; import { ToastViewport } from "./ToastViewport"; import { MobileBottomNav } from "./MobileBottomNav"; import { useDialog } from "../context/DialogContext"; import { usePanel } from "../context/PanelContext"; import { useCompany } from "../context/CompanyContext"; import { useSidebar } from "../context/SidebarContext"; import { useTheme } from "../context/ThemeContext"; import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts"; import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory"; import { healthApi } from "../api/health"; import { queryKeys } from "../lib/queryKeys"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; export function Layout() { const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar(); const { openNewIssue, openOnboarding } = useDialog(); const { panelContent, closePanel } = usePanel(); const { companies, loading: companiesLoading, setSelectedCompanyId } = useCompany(); const { theme, toggleTheme } = useTheme(); const onboardingTriggered = useRef(false); const lastMainScrollTop = useRef(0); const [mobileNavVisible, setMobileNavVisible] = useState(true); const nextTheme = theme === "dark" ? "light" : "dark"; const { data: health } = useQuery({ queryKey: queryKeys.health, queryFn: () => healthApi.get(), retry: false, }); useEffect(() => { if (companiesLoading || onboardingTriggered.current) return; if (health?.deploymentMode === "authenticated") return; if (companies.length === 0) { onboardingTriggered.current = true; openOnboarding(); } }, [companies, companiesLoading, openOnboarding, health?.deploymentMode]); const togglePanel = useCallback(() => { if (panelContent) closePanel(); }, [panelContent, closePanel]); // Cmd+1..9 to switch companies const switchCompany = useCallback( (index: number) => { if (index < companies.length) { setSelectedCompanyId(companies[index]!.id); } }, [companies, setSelectedCompanyId], ); useCompanyPageMemory(); useKeyboardShortcuts({ onNewIssue: () => openNewIssue(), onToggleSidebar: toggleSidebar, onTogglePanel: togglePanel, onSwitchCompany: switchCompany, }); useEffect(() => { if (!isMobile) { setMobileNavVisible(true); return; } lastMainScrollTop.current = 0; setMobileNavVisible(true); }, [isMobile]); // Swipe gesture to open/close sidebar on mobile useEffect(() => { if (!isMobile) return; const EDGE_ZONE = 30; // px from left edge to start open-swipe const MIN_DISTANCE = 50; // minimum horizontal swipe distance const MAX_VERTICAL = 75; // max vertical drift before we ignore let startX = 0; let startY = 0; const onTouchStart = (e: TouchEvent) => { const t = e.touches[0]!; startX = t.clientX; startY = t.clientY; }; const onTouchEnd = (e: TouchEvent) => { const t = e.changedTouches[0]!; const dx = t.clientX - startX; const dy = Math.abs(t.clientY - startY); if (dy > MAX_VERTICAL) return; // vertical scroll, ignore // Swipe right from left edge → open if (!sidebarOpen && startX < EDGE_ZONE && dx > MIN_DISTANCE) { setSidebarOpen(true); return; } // Swipe left when open → close if (sidebarOpen && dx < -MIN_DISTANCE) { setSidebarOpen(false); } }; document.addEventListener("touchstart", onTouchStart, { passive: true }); document.addEventListener("touchend", onTouchEnd, { passive: true }); return () => { document.removeEventListener("touchstart", onTouchStart); document.removeEventListener("touchend", onTouchEnd); }; }, [isMobile, sidebarOpen, setSidebarOpen]); const handleMainScroll = useCallback( (event: UIEvent) => { if (!isMobile) return; const currentTop = event.currentTarget.scrollTop; const delta = currentTop - lastMainScrollTop.current; if (currentTop <= 24) { setMobileNavVisible(true); } else if (delta > 8) { setMobileNavVisible(false); } else if (delta < -8) { setMobileNavVisible(true); } lastMainScrollTop.current = currentTop; }, [isMobile], ); return (
{/* Mobile backdrop */} {isMobile && sidebarOpen && (
setSidebarOpen(false)} /> )} {/* Combined sidebar area: company rail + inner sidebar + docs bar */} {isMobile ? (
) : (
)} {/* Main content */}
{isMobile && }
); }