import { useLoaderData, useLocation } from '@tanstack/react-router'
import { type ReactNode, useEffect } from 'react'
import { cn } from './cn.ts'
import { Logo } from './ui/Logo.tsx'
import { SidebarToggle } from './ui/SidebarToggle.tsx'
import { useSidebar } from './useSidebar.ts'
interface Props {
/**
* The chats, whose sidebar is a column beside the panel that collapses off the window's edge, or
* settings and admin, whose nav sits inside the panel beside the section.
*/
kind: 'chats' | 'settings'
/** What the sidebar holds, such as the chat list or links to each section. */
nav?: ReactNode
/** The page's frame while its code loads, its nav a skeleton of what will fill it. */
frame?: boolean
children?: ReactNode
}
/**
* The sidebar, after Linear's: dense one-line rows of small text on the frame. On a narrow screen it
* is a drawer over the page, closed until opened, which closes again once a link in it is followed.
*/
export function SidebarLayout(props: Props) {
const sidebar = useSidebar()
const pathname = useLocation({ select: (location) => location.pathname })
const { closeDrawer } = sidebar
// biome-ignore lint/correctness/useExhaustiveDependencies: closing follows each navigation.
useEffect(() => closeDrawer(), [pathname])
const backdrop = sidebar.drawerOpen && (
)
if (props.kind === 'settings')
return (
// The panel fills the window, with the nav at its start and the section centered in the rest,
// so the nav stays put from section to section. The nav sits as far from the window's edge
// as the chat list does, with the chat's gutter on the other side.
{/* On a narrow screen the toggle opens the page, a gutter from the panel's corner like
the section under it, and stays there as the page scrolls, since a sticky element
keeps clear of its scroll container's padding. */}
{backdrop}
{/* A section pads its own foot, so its Save bar can stick to the panel's very bottom. */}
{props.children}
)
return (
// Whether the sidebar is collapsed is an attribute on the root, set before the first paint.
{backdrop}
{/* A size container, so the conversation's header can reach the panel's edges, and the block
its absolutely placed parts are placed in, so none of them can stretch the page past the
panel's scroll. */}
{props.children}
)
}
const sidebarClass = cn(
'flex flex-col gap-0.5 overscroll-contain bg-sidebar px-sidebar-pad pt-sidebar-pad pb-3 text-ui',
// On a narrow screen, a drawer over the page that stays visible until it has slid away.
'motion-reduce:transition-none max-wide:in-data-drawer-open:visible max-wide:invisible max-wide:fixed max-wide:inset-y-0 max-wide:start-0 max-wide:z-20 max-wide:w-drawer max-wide:-translate-x-full max-wide:in-data-drawer-open:translate-x-0 max-wide:shadow-float max-wide:transition-[translate,visibility] max-wide:duration-(--dur-slide) max-wide:ease-slide rtl:max-wide:in-data-drawer-open:translate-x-0 rtl:max-wide:translate-x-full',
)
/**
* The toggle in the panel, for when the sidebar is hidden: collapsed on a wide screen, or a closed
* drawer on a narrow one. On a wide screen it fades in once the sidebar, with its own toggle, has
* mostly slid away, and fades out at once as the sidebar comes back.
*/
function MainBar() {
return (
)
}
/** The app's logo, named for screen readers by the app's name. */
function AppLogo() {
const name = useLoaderData({ from: '__root__', select: (data) => data.appName })
return
}