forked from farhoodlabs/paperclip
33d549db13
Add PWA meta tags for iOS home screen. Fix mobile properties drawer with safe area insets. Add image attachment button to comment thread. Improve sidebar with collapsible sections, project grouping, and mobile bottom nav. Show token and billing type breakdown on costs page. Fix inbox loading state to show content progressively. Various mobile overflow and layout fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
172 lines
5.7 KiB
TypeScript
172 lines
5.7 KiB
TypeScript
import { useCallback, useEffect, useRef, useState, type UIEvent } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { BookOpen } 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 { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
|
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
|
|
import { healthApi } from "../api/health";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { cn } from "../lib/utils";
|
|
|
|
export function Layout() {
|
|
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
|
|
const { openNewIssue, openOnboarding } = useDialog();
|
|
const { panelContent, closePanel } = usePanel();
|
|
const { companies, loading: companiesLoading, setSelectedCompanyId } = useCompany();
|
|
const onboardingTriggered = useRef(false);
|
|
const lastMainScrollTop = useRef(0);
|
|
const [mobileNavVisible, setMobileNavVisible] = useState(true);
|
|
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]);
|
|
|
|
const handleMainScroll = useCallback(
|
|
(event: UIEvent<HTMLElement>) => {
|
|
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 (
|
|
<div className="flex h-dvh bg-background text-foreground overflow-hidden pt-[env(safe-area-inset-top)]">
|
|
{/* Mobile backdrop */}
|
|
{isMobile && sidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-black/50"
|
|
onClick={() => setSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Combined sidebar area: company rail + inner sidebar + docs bar */}
|
|
{isMobile ? (
|
|
<div
|
|
className={cn(
|
|
"fixed inset-y-0 left-0 z-50 flex flex-col overflow-hidden pt-[env(safe-area-inset-top)] transition-transform duration-200 ease-in-out",
|
|
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
)}
|
|
>
|
|
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
<CompanyRail />
|
|
<Sidebar />
|
|
</div>
|
|
<div className="border-t border-r border-border px-3 py-2 bg-background">
|
|
<SidebarNavItem to="/docs" label="Documentation" icon={BookOpen} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col shrink-0 h-full">
|
|
<div className="flex flex-1 min-h-0">
|
|
<CompanyRail />
|
|
<div
|
|
className={cn(
|
|
"overflow-hidden transition-all duration-200 ease-in-out",
|
|
sidebarOpen ? "w-60" : "w-0"
|
|
)}
|
|
>
|
|
<Sidebar />
|
|
</div>
|
|
</div>
|
|
<div className="border-t border-r border-border px-3 py-2">
|
|
<SidebarNavItem to="/docs" label="Documentation" icon={BookOpen} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main content */}
|
|
<div className="flex-1 flex flex-col min-w-0 h-full">
|
|
<BreadcrumbBar />
|
|
<div className="flex flex-1 min-h-0">
|
|
<main
|
|
className={cn("flex-1 overflow-auto p-4 md:p-6", isMobile && "pb-[calc(5rem+env(safe-area-inset-bottom))]")}
|
|
onScroll={handleMainScroll}
|
|
>
|
|
<Outlet />
|
|
</main>
|
|
<PropertiesPanel />
|
|
</div>
|
|
</div>
|
|
{isMobile && <MobileBottomNav visible={mobileNavVisible} />}
|
|
<CommandPalette />
|
|
<NewIssueDialog />
|
|
<NewProjectDialog />
|
|
<NewGoalDialog />
|
|
<NewAgentDialog />
|
|
<OnboardingWizard />
|
|
<ToastViewport />
|
|
</div>
|
|
);
|
|
}
|