import { lazy, Suspense, useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { Link } from "@tanstack/react-router"; import { Plus, Search, ChevronsLeft, ChevronsRight, Loader2, Settings, Archive, ArrowLeft, ChevronDown, FileText, PenTool, } from "lucide-react"; import { DropdownPortal } from "@/client/components/ui/dropdown-portal"; import { useCurrentWorkspace, useWorkspaceRole } from "@/client/components/workspace/use-workspace-view"; import { useCreatePage } from "@/client/hooks/use-create-page"; import { WorkspaceSwitcher } from "./workspace-switcher"; import { PageTree } from "./page-tree"; import { searchShortcutLabel } from "./search-shortcut"; import { useOnline } from "@/client/hooks/use-online"; import { deriveSidebarBaseAffordance, type SidebarBaseAffordance } from "@/client/lib/affordance/sidebar"; import { isActionEnabled, isActionVisible } from "@/client/lib/affordance/action-state"; import { toast } from "@/client/components/toast-store"; import { EmojiIcon } from "@/client/components/ui/emoji-icon"; import { MobileDrawer } from "@/client/components/ui/mobile-drawer"; const SearchDialog = lazy(() => import("./search-dialog").then((mod) => ({ default: mod.SearchDialog }))); interface SidebarProps { collapsed: boolean; onCollapsedChange: (next: boolean) => void; mobileOpen?: boolean; onMobileClose?: () => void; } import type { WorkspaceRole } from "@/shared/types"; type SidebarWorkspaceRole = WorkspaceRole | "none"; type CreatePageAction = SidebarBaseAffordance["createPage"]; type CreatePageFn = ReturnType["createPage"]; export function Sidebar({ collapsed, onCollapsedChange, mobileOpen, onMobileClose }: SidebarProps) { const currentWorkspace = useCurrentWorkspace(); const { createPage, isCreating } = useCreatePage(); const online = useOnline(); const workspaceRole: SidebarWorkspaceRole = useWorkspaceRole() ?? "none"; // "Shared mode" is role-defined, not access_mode-defined: guests belong in // the authenticated shell (so they can reach Leave Workspace), but still // need the sidebar to hide writer-only affordances. Anything outside the // owner/admin/member/guest set is shared-surface. const isSharedMode = workspaceRole === "none"; const sidebarAffordance = deriveSidebarBaseAffordance({ workspaceRole, online }); const { searchOpen, openSearch, closeSearch } = useSidebarSearch(online); const showCollapsed = collapsed && !mobileOpen; const sidebarMenuZIndex = mobileOpen ? 90 : undefined; const expandSidebar = useCallback(() => onCollapsedChange(false), [onCollapsedChange]); const collapseSidebar = useCallback(() => onCollapsedChange(true), [onCollapsedChange]); return ( {showCollapsed ? ( ) : ( )} {searchOpen && ( )} ); } function useSidebarSearch(online: boolean) { const [searchOpen, setSearchOpen] = useState(false); const openSearch = useCallback(() => { if (!online) { toast.info("Search requires a connection"); return; } setSearchOpen(true); }, [online]); const closeSearch = useCallback(() => { setSearchOpen(false); }, []); useEffect(() => { const handleKeydown = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key === "k") { event.preventDefault(); if (!online) { toast.info("Search requires a connection"); return; } setSearchOpen((open) => !open); } }; document.addEventListener("keydown", handleKeydown); return () => document.removeEventListener("keydown", handleKeydown); }, [online]); return { searchOpen, openSearch, closeSearch }; } function SidebarFrame({ collapsed, children }: { collapsed: boolean; children: ReactNode }) { return ( ); } interface CollapsedSidebarRailProps { createPageAction: CreatePageAction; createPage: CreatePageFn; openSearch: () => void; onExpand: () => void; } function CollapsedSidebarRail({ createPageAction, createPage, openSearch, onExpand }: CollapsedSidebarRailProps) { return (
{isActionVisible(createPageAction) && ( )}
); } interface ExpandedSidebarContentProps { currentWorkspace: ReturnType; isSharedMode: boolean; workspaceRole: SidebarWorkspaceRole; createPageAction: CreatePageAction; createPage: CreatePageFn; isCreating: boolean; openSearch: () => void; menuZIndex?: number; mobileOpen: boolean; onCollapse: () => void; } function ExpandedSidebarContent({ currentWorkspace, isSharedMode, workspaceRole, createPageAction, createPage, isCreating, openSearch, menuZIndex, mobileOpen, onCollapse, }: ExpandedSidebarContentProps) { return ( <> {isSharedMode ? : } ); } function SharedWorkspaceHeader({ currentWorkspace }: { currentWorkspace: ReturnType }) { return (
{currentWorkspace?.icon && ( )} {currentWorkspace?.name ?? "Workspace"}
); } interface SidebarActionBarProps { createPageAction: CreatePageAction; createPage: CreatePageFn; isCreating: boolean; openSearch: () => void; menuZIndex?: number; } function SidebarActionBar({ createPageAction, createPage, isCreating, openSearch, menuZIndex }: SidebarActionBarProps) { return (
); } interface CreatePageControlsProps { createPageAction: CreatePageAction; createPage: CreatePageFn; isCreating: boolean; menuZIndex?: number; } function CreatePageControls({ createPageAction, createPage, isCreating, menuZIndex }: CreatePageControlsProps) { const [menuOpen, setMenuOpen] = useState(false); const triggerRef = useRef(null); if (!isActionVisible(createPageAction)) return null; const disabled = isCreating || !isActionEnabled(createPageAction); const disabledReason = createPageAction.kind === "disabled" ? createPageAction.reason : undefined; return (
{menuOpen && ( setMenuOpen(false)} >
)}
); } interface SidebarFooterProps { currentWorkspace: ReturnType; isSharedMode: boolean; workspaceRole: SidebarWorkspaceRole; mobileOpen: boolean; onCollapse: () => void; } function SidebarFooter({ currentWorkspace, isSharedMode, workspaceRole, mobileOpen, onCollapse }: SidebarFooterProps) { // Settings link is available to any membership (including guest, so they // can reach Leave Workspace). Shared-surface viewers do not see it because // they cannot act on workspace settings. const showSettings = !!currentWorkspace && !isSharedMode; // Archive is writer-surface only: guests get Settings (for Leave Workspace) // but cannot manage archived pages. const showArchive = !!currentWorkspace && !isSharedMode && workspaceRole !== "guest"; return (
{showSettings && ( )} {showArchive && ( )}
{!mobileOpen && ( )}
); } function NOOP() {}