chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { Dialog } from '@base-ui/react/dialog';
import { Avatar } from '#/components/atoms/avatar.jsx';
import { Button } from '#/components/atoms/button.jsx';
import { Link } from '#/components/atoms/link.jsx';
import { cn } from '#/lib/utils.js';
/**
* The account rail: a branded header, the section links with their counts, and
* a sign-out. Always present on desktop, and a sheet on small screens driven
* by `open` / `onOpenChange`.
*/
export function Sidebar({
account,
sections,
current,
onNavigate,
onSignOut,
open,
onOpenChange,
}) {
const initial = (account.displayName || account.handle || account.did)
.slice(0, 1)
.toUpperCase();
const rail = (
<div className="flex h-full flex-col gap-1">
<div className="flex shrink-0 items-center gap-3 p-3">
<Avatar
className="size-10 border-0 bg-gradient-to-br from-beam-from to-beam-to shadow-[inset_0_1px_0_rgba(255,255,255,.35)]"
src={account.avatarUrl}
fallback={
<span className="font-extrabold text-[17px] text-white">
{initial}
</span>
}
/>
<div className="min-w-0">
<div className="truncate font-extrabold text-[15px] text-foreground">
{account.handle ? `@${account.handle}` : account.did}
</div>
</div>
</div>
<nav className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto overscroll-contain px-2 pb-1">
{sections.map((section) => {
if (section.heading) {
return (
<div
key={`heading-${section.heading}`}
className="px-3.5 pt-5 pb-1.5 font-bold text-[11px] text-muted-foreground/70 uppercase tracking-wide"
>
{section.heading}
</div>
);
}
const active = section.path === current;
return (
<Link
key={section.path}
href={section.path}
onNavigate={(next) => {
onNavigate(next);
onOpenChange?.(false);
}}
aria-current={active ? 'page' : undefined}
className={cn(
'flex items-center gap-2.5 rounded-2xl px-3.5 py-3 font-bold text-[14.5px] outline-none transition-colors',
'focus-visible:ring-[3px] focus-visible:ring-ring/50',
active
? 'bg-accent text-foreground shadow-[inset_0_0_0_1px_var(--color-border)]'
: 'text-muted-foreground hover:text-foreground',
)}
>
<span className="truncate">{section.title}</span>
{section.count != null && section.count !== '' && (
<span className="ml-auto text-[12px] opacity-50">
{section.count}
</span>
)}
</Link>
);
})}
</nav>
<div className="shrink-0 border-border/60 border-t p-2">
<Button
variant="ghost"
onClick={onSignOut}
className="h-auto w-full justify-start rounded-2xl px-3.5 py-2.5 font-bold text-[13.5px] text-faint hover:bg-transparent hover:text-foreground"
>
Sign out
</Button>
</div>
</div>
);
return (
<>
<aside className="sticky top-0 hidden h-svh w-[250px] shrink-0 border-border border-r bg-background md:block">
{rail}
</aside>
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 z-40 bg-black/60 md:hidden" />
<Dialog.Popup
aria-label="Sections"
className="fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-border border-r bg-card md:hidden"
>
{rail}
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
</>
);
}