deno.land / x / netzo@0.5.16 / components / blocks / nav.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301import { signal } from "@preact/signals";import type { ComponentChildren, JSX } from "preact";import type { NetzoState } from "../../mod.ts";import { Avatar, AvatarFallback, AvatarImage } from "../avatar.tsx";import { buttonVariants } from "../button.tsx";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,} from "../dropdown-menu.tsx";import { Separator } from "../separator.tsx";import { Sheet, SheetContent, SheetTrigger } from "../sheet.tsx";import { Switch } from "../switch.tsx";import { useDarkMode } from "../use-dark-mode.ts";import { cn } from "../utils.ts";
export const open = signal<boolean>(false);
type NavRootProps = JSX.IntrinsicElements["nav"] & { state?: NetzoState; children?: ComponentChildren;};
export function NavRoot({ className, ...props }: NavRootProps) { const { denoJson } = props.state ?? {}; const darkMode = useDarkMode();
const logoSrc = darkMode.value ? `https://netzo.io/logos/built-with-netzo-dark.svg` : `https://netzo.io/logos/built-with-netzo-light.svg`;
const NavFooter = () => ( <footer className="w-full flex items-center justify-between p-3 b-t-1"> <a href="https://netzo.io/" target="_blank" className="mx-auto"> <img src={logoSrc} alt="Built with Netzo" className="h-[32px]" /> </a> </footer> );
const NavProjectVersion = () => { if (!denoJson) return null; // NOTE: uses style instead of className+unocss to avoid style flashing return ( <> <NavSeparator /> <div title={denoJson.description} style={{ textAlign: "center", fontSize: "10px", color: "hsl(var(--muted-foreground))", }} > {denoJson.name}@{denoJson.version} </div> </> ); };
return ( <> {/* MOBILE */} <Sheet className={cn("!md:hidden", open.value ? "w-[250px]" : "w-[28px]")} open={open.value} onOpenChange={(e) => open.value = e} > <SheetTrigger asChild> <div variant="outline" size="icon" className={cn( "h-full w-[28px]", "fixed top-0 left-0 z-1000", "hover:bg-background hover:bg-opacity-80 hover:cursor-pointer", "flex items-center justify-center", open.value ? "!hidden" : "!md:hidden", )} onClick={() => open.value = !open.value} > <i className="mdi-menu-open rotate-180 h-[1.2rem] w-[1.2rem]" /> <span className="sr-only">Toggle navigation</span> </div> </SheetTrigger> <SheetContent side="left" className="w-[250px] p-0"> <nav f-client-nav className={cn( "h-screen overflow-y-auto group flex flex-col bg-primary-foreground", className, )} > {props.children} <NavFooter /> <NavProjectVersion /> </nav> </SheetContent> </Sheet>
{/* DESKTOP */} <nav f-client-nav className={cn( "!hidden !md:flex md:b-r-1", "h-screen overflow-y-auto group flex flex-col bg-primary-foreground", className, )} > {props.children} <NavFooter /> <NavProjectVersion /> </nav> </> );}
type NavHeaderProps = JSX.IntrinsicElements["header"] & { /** A short title for the app at the navigation drawer. */ title?: string; /** An https or data URL of a cover image at the navigation drawer */ image?: string;};
export function NavHeader({ className, ...props }: NavHeaderProps) { return ( <header className={cn("flex items-center py-3 px-2", className)}> {props?.image && ( <img src={props?.image} className="w-auto h-9 my-auto mr-2" /> )} {props?.title && <h2 className="text-xl font-bold">{props?.title}</h2>} </header> );}
export function NavSeparator(props: JSX.IntrinsicElements["div"]) { return <Separator {...props} />;}
export function NavSpacer( { className, ...props }: JSX.IntrinsicElements["div"],) { return <div {...props} className={cn("flex-1", className)} />;}
type NavItemHeaderProps = JSX.IntrinsicElements["h3"] & { /** A short title for the app at the navigation drawer. */ text: string;};
export function NavItemHeader({ className, ...props }: NavItemHeaderProps) { return ( <h3 className={cn( "mt-3 mb-1 mx-4 text-xs font-medium text-muted-foreground", className, )} > {props.text} </h3> );}
type NavItemProps = JSX.IntrinsicElements["a"] & { /** An https or data URL of an icon to be shown at the navigation item. */ icon?: string; /** A short title for the app at the navigation drawer. */ text: string; /** An absolute or relative URL of the page to navigate to. */ href?: string; /** An optional target to open the link in. */ target?: string; /** Whether link active state should be exact or not (useful to disable ancestor link styles). */ exact?: boolean;};
export function NavItem({ className, ...props }: NavItemProps) { return ( <div> <a className={cn( buttonVariants({ variant: "ghost", className: "rounded-none" }), "flex w-full justify-start h-[40px]", "hover:cursor-pointer hover:bg-muted", // hover props.exact ? "" : `aria-[current='true']:bg-border`, // ancestor links `aria-[current='page']:bg-border`, // current page className, )} href={props.href} target={props.target} > {props.icon && (props.icon.startsWith("http") ? <img {...props} src={props.icon} className="w-4 h-4 mr-3" /> : <div {...props} className={cn("w-4 h-4 mr-3", props.icon)} />)} {props.text} </a> </div> );}
export function NavItemUser(props: { state: NetzoState }) { const { auth } = props.state ?? {};
const darkMode = useDarkMode();
const { name, authId, email } = auth?.sessionUser ?? {}; const initials = name || authId || email || "?"; const initial = initials?.[0]?.toUpperCase();
return ( <DropdownMenu> <DropdownMenuTrigger asChild> <div className={cn( buttonVariants({ variant: "ghost", className: "rounded-none" }), "flex w-full items-center justify-start h-[56px] pl-2 pr-3", "hover:cursor-pointer hover:bg-muted", // hover )} > {auth?.sessionUser ? ( <> <Avatar className="h-9 w-9 mr-2"> <AvatarImage src={auth?.sessionUser?.avatar} alt={`@${auth?.sessionUser?.authId}}`} /> <AvatarFallback>{initial}</AvatarFallback> </Avatar> <div className="flex flex-col space-y-1"> <p className="text-sm font-medium leading-none"> {auth?.sessionUser?.name} </p> {auth?.sessionUser?.email && ( <p className="text-xs leading-none text-muted-foreground"> {auth?.sessionUser?.email} </p> )} </div> </> ) : ( <> <i className="mdi-account-circle h-7 w-7 ml-0.5 mr-1.5" /> <div className="flex flex-col space-y-1"> <p className="text-sm font-medium leading-none"> User Settings </p> </div> </> )} <i className="mdi-unfold-more-horizontal h-6 w-6 ml-auto" /> </div> </DropdownMenuTrigger> <DropdownMenuContent side="right" align="end" forceMount> {auth?.sessionUser?.name && ( <> <DropdownMenuLabel className="font-normal"> <div className="flex flex-col space-y-1"> <p className="text-sm font-medium leading-none"> {auth?.sessionUser?.name} </p> {auth?.sessionUser?.email && ( <p className="text-xs leading-none text-muted-foreground"> {auth?.sessionUser?.email} </p> )} </div> </DropdownMenuLabel> <DropdownMenuSeparator /> </> )} <DropdownMenuItem> <div className="flex gap-4 w-full items-center justify-between"> Dark mode <Switch checked={darkMode.value} onCheckedChange={(value) => darkMode.value = value} /> </div> </DropdownMenuItem> {auth?.sessionUser?.name && ( <> <DropdownMenuSeparator /> <DropdownMenuItem> <a href="/auth/signout" title="Sign out" className="w-full"> Log out </a> </DropdownMenuItem> </> )} </DropdownMenuContent> </DropdownMenu> );}
Version Info