App Shell 4

Starter shell on the shadcn Sidebar primitive (inset variant): workspace switcher and ⌘K search in the rail, icon-collapsible nav with tooltips, an inset header with trigger, notification count and avatar, and a welcome heading over dashed placeholder slots.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/app-shell-04.json

Code

components/blocks/app-shell-04.tsx
"use client";

import * as React from "react";
import {
  Bell,
  CalendarRange,
  ChartNoAxesColumn,
  ChevronsUpDown,
  LayoutDashboard,
  LifeBuoy,
  Megaphone,
  Plug,
  Search,
  Settings,
  Users,
  type LucideIcon,
} from "lucide-react";

import { Badge } from "@/registry/hirael/ui/badge";
import { Button } from "@/registry/hirael/ui/button";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/registry/hirael/ui/input-group";
import { KbdDisplay, KbdGroup } from "@/registry/hirael/components/kbd";
import { Separator } from "@/registry/hirael/ui/separator";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@/registry/hirael/ui/sidebar";
import { cn } from "@/lib/utils";

const NAV: { icon: LucideIcon; label: string; active?: boolean }[] = [
  { icon: LayoutDashboard, label: "Overview", active: true },
  { icon: CalendarRange, label: "Planner" },
  { icon: Megaphone, label: "Campaigns" },
  { icon: ChartNoAxesColumn, label: "Reports" },
  { icon: Users, label: "Audience" },
  { icon: Plug, label: "Connections" },
];

const FOOTER_NAV: { icon: LucideIcon; label: string }[] = [
  { icon: LifeBuoy, label: "Support" },
  { icon: Settings, label: "Settings" },
];

function BrandMark({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 80 100"
      fill="none"
      stroke="currentColor"
      strokeWidth="2.2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden
      className={className}
    >
      <path d="M16 78 V40 a24 24 0 0 1 48 0 V78" />
      <path d="M40 44 L43.2 52 L51 55 L43.2 58 L40 66 L36.8 58 L29 55 L36.8 52 Z" />
      <path d="M22 86 H58" opacity="0.7" />
      <path d="M28 92 H52" opacity="0.45" />
      <path d="M34 96 H46" opacity="0.25" />
    </svg>
  );
}

function Slot({ label, className }: { label: string; className?: string }) {
  return (
    <div
      className={cn(
        "flex items-center justify-center rounded-md border border-dashed border-border bg-card/30",
        className,
      )}
    >
      <span className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground/70">
        {label}
      </span>
    </div>
  );
}

export default function AppShell04() {
  return (
    <SidebarProvider>
      <Sidebar variant="inset" collapsible="icon">
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="lg" tooltip="Plinth Labs">
                <span className="flex aspect-square size-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground">
                  <BrandMark className="size-5" />
                </span>
                <div className="grid flex-1 text-start leading-tight">
                  <span className="truncate text-sm font-semibold tracking-[-0.01em]">
                    Plinth Labs
                  </span>
                  <span className="truncate font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground">
                    Pro workspace
                  </span>
                </div>
                <ChevronsUpDown className="size-3.5 text-muted-foreground" />
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
          <InputGroup className="h-8 group-data-[collapsible=icon]:hidden">
            <InputGroupAddon align="inline-start">
              <Search className="size-3.5" />
            </InputGroupAddon>
            <InputGroupInput
              placeholder="Search…"
              aria-label="Search"
              className="text-sm"
            />
            <InputGroupAddon align="inline-end">
              <KbdGroup>
                <KbdDisplay></KbdDisplay>
                <KbdDisplay>K</KbdDisplay>
              </KbdGroup>
            </InputGroupAddon>
          </InputGroup>
        </SidebarHeader>

        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupContent>
              <SidebarMenu>
                {NAV.map((item) => (
                  <SidebarMenuItem key={item.label}>
                    <SidebarMenuButton
                      asChild
                      isActive={item.active}
                      tooltip={item.label}
                    >
                      <a href="#">
                        <item.icon />
                        <span>{item.label}</span>
                      </a>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>

        <SidebarFooter>
          <SidebarMenu>
            {FOOTER_NAV.map((item) => (
              <SidebarMenuItem key={item.label}>
                <SidebarMenuButton asChild tooltip={item.label} className="h-7">
                  <a href="#">
                    <item.icon />
                    <span>{item.label}</span>
                  </a>
                </SidebarMenuButton>
              </SidebarMenuItem>
            ))}
          </SidebarMenu>
          <span className="px-2 pb-1 font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground group-data-[collapsible=icon]:hidden">
            © Plinth Labs
          </span>
        </SidebarFooter>
      </Sidebar>

      <SidebarInset className="min-h-[640px]">
        <header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-border px-4">
          <div className="flex min-w-0 items-center gap-2">
            <SidebarTrigger />
            <Separator orientation="vertical" className="h-4" />
            <span className="truncate text-sm font-medium tracking-[-0.01em]">
              Overview
            </span>
          </div>
          <div className="flex shrink-0 items-center gap-2">
            <Button
              variant="ghost"
              size="icon"
              className="relative size-8"
              aria-label="Notifications · 3 unread"
            >
              <Bell className="size-4" />
              <Badge className="absolute -end-1 -top-1 size-4 justify-center rounded-full p-0 font-mono text-[9px]">
                3
              </Badge>
            </Button>
            <Separator orientation="vertical" className="h-4" />
            <button
              type="button"
              aria-label="Account menu"
              className="inline-flex size-8 items-center justify-center rounded-full border border-border bg-card font-mono text-[11px] font-medium transition-colors hover:border-foreground"
            >
              MS
            </button>
          </div>
        </header>

        <div className="flex flex-1 flex-col gap-6 p-4 sm:p-6">
          <div className="flex flex-col gap-1">
            <h1 className="font-serif text-2xl font-medium tracking-tight sm:text-3xl">
              Good morning, Maya.
            </h1>
            <p className="text-sm text-muted-foreground">
              Your workspace is ready. Drop content into the slots below.
            </p>
          </div>

          <div className="grid flex-1 grid-cols-2 gap-4 md:grid-cols-4">
            {["slot · 01", "slot · 02", "slot · 03", "slot · 04"].map(
              (label) => (
                <Slot key={label} label={label} className="min-h-28" />
              ),
            )}
            <Slot
              label="slot · 05"
              className="col-span-2 min-h-56 md:col-span-3"
            />
            <Slot
              label="slot · 06"
              className="col-span-2 min-h-56 md:col-span-1"
            />
            <Slot
              label="slot · 07"
              className="col-span-2 min-h-56 md:col-span-1"
            />
            <Slot
              label="slot · 08"
              className="col-span-2 min-h-56 md:col-span-3"
            />
          </div>
        </div>
      </SidebarInset>
    </SidebarProvider>
  );
}

Dependencies

shadcn registry

badgebuttoninput-groupkbdseparatorsidebar

npm

lucide-react