
Client store header with a centered mega menu of link-only columns, search and bag count, collapsing to a left Sheet with accordion categories when narrow. No announcement bar.
blocks/store/store-header-02/index.tsx
"use client"
import {
BasketIcon,
CircleIcon,
ListIcon,
MagnifyingGlassIcon,
} from "@phosphor-icons/react"
import { cn } from "cn"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Button, buttonVariants } from "@/components/ui/button"
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
// The copy: change text here, not in the markup.
const COPY = {
brand: "Orbcore",
home: "Orbcore home",
navigation: "Store",
mobileNavigation: "Store menu",
menu: "Browse the shop",
openMenu: "Open menu",
pages: "Pages",
search: "Search",
cart: "items in bag, view bag",
moreEditions: "More editions",
shopAll: "Shop all",
} as const
const NAVIGATION = {
categories: [
{
id: "prints",
name: "Prints",
href: "#",
editions: {
id: "editions",
name: "Editions",
href: "#",
columns: [
[
{ name: "Color fields", href: "#" },
{ name: "Line work", href: "#" },
{ name: "Mezzotints", href: "#" },
{ name: "Monotypes", href: "#" },
{ name: "Posters", href: "#" },
{ name: "Small formats", href: "#" },
],
[
{ name: "Signed editions", href: "#" },
{ name: "Diptychs", href: "#" },
{ name: "Large formats", href: "#" },
{ name: "Sample packs", href: "#" },
{ name: "Browse all", href: "#" },
],
],
},
groups: [
{
id: "framing",
name: "Framing",
href: "#",
links: [
{ name: "Frames", href: "#" },
{ name: "Mounts", href: "#" },
{ name: "Glass", href: "#" },
{ name: "Hanging kits", href: "#" },
{ name: "Portfolios", href: "#" },
{ name: "Browse all", href: "#" },
],
},
{
id: "collections",
name: "Collections",
href: "#",
links: [
{ name: "New arrivals", href: "#" },
{ name: "Sale", href: "#" },
{ name: "Core", href: "#" },
{ name: "Signed editions", href: "#" },
],
},
],
},
{
id: "originals",
name: "Originals",
href: "#",
editions: {
id: "works",
name: "Works",
href: "#",
columns: [
[
{ name: "Drawings", href: "#" },
{ name: "Paintings", href: "#" },
{ name: "Studies", href: "#" },
{ name: "Diptychs", href: "#" },
{ name: "Sketchbooks", href: "#" },
{ name: "Collages", href: "#" },
],
[
{ name: "Commissions", href: "#" },
{ name: "Archive", href: "#" },
{ name: "Large formats", href: "#" },
{ name: "Browse all", href: "#" },
],
],
},
groups: [
{
id: "framing",
name: "Framing",
href: "#",
links: [
{ name: "Frames", href: "#" },
{ name: "Mounts", href: "#" },
{ name: "Glass", href: "#" },
{ name: "Hanging kits", href: "#" },
{ name: "Browse all", href: "#" },
],
},
{
id: "collections",
name: "Collections",
href: "#",
links: [
{ name: "Just added", href: "#" },
{ name: "Clearance", href: "#" },
{ name: "Studio picks", href: "#" },
],
},
],
},
],
pages: [
{ name: "Studio", href: "#" },
{ name: "Stockists", href: "#" },
],
} as const
type StoreCategory = (typeof NAVIGATION.categories)[number]
// The phone drawer lists each group's links in one column.
function groupsOf(category: StoreCategory) {
return [
{
id: category.editions.id,
name: category.editions.name,
href: category.editions.href,
links: category.editions.columns.flat(),
},
...category.groups,
]
}
const LINK =
"inline-flex min-h-6 items-center rounded-sm text-sm underline-offset-4 outline-none hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
const MUTED = cn(LINK, "text-muted-foreground hover:text-foreground")
const ROW = cn(LINK, "min-h-11 w-full text-base")
type Link = { name: string; href: string }
function LinkList({
links,
label,
}: {
links: readonly Link[]
label?: string
}) {
return (
<ul role="list" aria-label={label} className="grid content-start gap-2">
{links.map((link) => (
<li key={link.name}>
<a href={link.href} className={MUTED}>
{link.name}
</a>
</li>
))}
</ul>
)
}
export default function StoreHeader02({
cartCount = 0,
}: {
cartCount?: number
}) {
return (
<header className="@container/shop-nav relative w-full border-b border-border bg-background text-foreground">
<div className="mx-auto flex min-h-16 w-full max-w-7xl flex-wrap items-center gap-x-4 px-6 lg:px-10">
<div className="-ml-2 flex flex-1 items-center gap-x-1 @5xl/shop-nav:hidden">
<Sheet>
<SheetTrigger
render={
<Button
variant="ghost"
size="icon"
aria-label={COPY.openMenu}
/>
}
>
<ListIcon weight="light" aria-hidden="true" className="size-5" />
</SheetTrigger>
<SheetContent side="left" className="overflow-y-auto">
<SheetHeader>
<SheetTitle>{COPY.menu}</SheetTitle>
</SheetHeader>
<nav
aria-label={COPY.mobileNavigation}
className="grid gap-4 pb-6"
>
<Accordion className="mx-6 w-auto">
{NAVIGATION.categories.map((category) => (
<AccordionItem key={category.id} value={category.id}>
<AccordionTrigger className="focus-visible:ring-3 focus-visible:ring-ring/30 focus-visible:ring-inset">
{category.name}
</AccordionTrigger>
<AccordionContent className="grid gap-6">
<a href={category.href} className={ROW}>
{COPY.shopAll} {category.name}
</a>
{groupsOf(category).map((group) => (
<div
key={group.id}
className="grid min-w-0 content-start gap-4"
>
<a
href={group.href}
className={cn(LINK, "font-medium")}
>
{group.name}
</a>
<LinkList links={group.links} />
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
<ul
role="list"
aria-label={COPY.pages}
className="grid border-t border-border px-6 pt-4"
>
{NAVIGATION.pages.map((page) => (
<li key={page.name}>
<a href={page.href} className={ROW}>
{page.name}
</a>
</li>
))}
</ul>
</nav>
</SheetContent>
</Sheet>
</div>
<a
href="#"
aria-label={COPY.home}
className="inline-flex w-fit min-w-0 items-center gap-2 rounded-sm text-xl font-semibold focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring @5xl/shop-nav:flex-1"
>
<CircleIcon
weight="light"
aria-hidden="true"
className="size-5 shrink-0"
/>
<span className="hidden @md/shop-nav:inline">{COPY.brand}</span>
</a>
<NavigationMenu
aria-label={COPY.navigation}
delay={250}
closeDelay={150}
className="hidden @5xl/shop-nav:flex"
>
<NavigationMenuList>
{NAVIGATION.categories.map((category) => (
<NavigationMenuItem key={category.id}>
<NavigationMenuTrigger>{category.name}</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="grid w-4xl grid-cols-4 gap-x-8 gap-y-10 p-4">
<div className="col-span-2 grid min-w-0 content-start gap-4">
<a
href={category.editions.href}
className={cn(LINK, "font-medium")}
>
{category.editions.name}
</a>
<div className="grid grid-cols-2 gap-x-4">
<LinkList links={category.editions.columns[0]} />
<LinkList
links={category.editions.columns[1]}
label={COPY.moreEditions}
/>
</div>
</div>
{category.groups.map((group) => (
<div
key={group.id}
className="grid min-w-0 content-start gap-4"
>
<a
href={group.href}
className={cn(LINK, "font-medium")}
>
{group.name}
</a>
<LinkList links={group.links} />
</div>
))}
</div>
</NavigationMenuContent>
</NavigationMenuItem>
))}
{NAVIGATION.pages.map((page) => (
<NavigationMenuItem key={page.name}>
<NavigationMenuLink
href={page.href}
className={navigationMenuTriggerStyle()}
>
{page.name}
</NavigationMenuLink>
</NavigationMenuItem>
))}
</NavigationMenuList>
</NavigationMenu>
<div className="flex flex-1 items-center justify-end gap-x-1">
<a
href="#"
aria-label={COPY.search}
className={buttonVariants({ variant: "ghost", size: "icon" })}
>
<MagnifyingGlassIcon
weight="light"
aria-hidden="true"
className="size-5"
/>
</a>
<a
href="#"
className={cn(buttonVariants({ variant: "ghost" }), "gap-1.5 px-2")}
>
<BasketIcon weight="light" aria-hidden="true" className="size-5" />
<span className="text-sm tabular-nums">{cartCount}</span>
<span className="sr-only">{COPY.cart}</span>
</a>
</div>
</div>
</header>
)
}