~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
115 lines · 3.8 KB Raw History
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>
    </>
  );
}