import { useEffect, useRef, useState } from "react" const SIDEBAR_KEY = "crema.shell.sidebar" import { NavLink, useNavigate } from "react-router" import { Bell, LayoutDashboard, Boxes, Activity, Sparkles, Bot, BookOpen, Settings, Search, PanelLeftClose, PanelLeftOpen, User as UserIcon, LogOut, HelpCircle, Menu, Play, // CREMA:NAV-ICONS } from "lucide-react" import { useBrand, useUser, type Brand, type User, } from "~/lib/identity" import { Appbar, AppbarActions, AppbarSpacer, AppbarTitle, } from "~/components/layout/appbar" import { ThemeToggle } from "~/components/layout/theme-toggle" import { BackgroundPicker } from "~/components/layout/background-picker" import { FontSizePicker } from "~/components/layout/font-size-picker" import { SurfacePicker } from "~/components/layout/surface-picker" import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar" import { Popover, PopoverContent, PopoverTrigger, } from "~/components/ui/popover" import { profileInitials, useProfile } from "~/lib/profile" import { signOut, useSession } from "~/lib/session" import { addNotification, dismiss, dismissAll, markAllRead, markRead, seedIfEmpty, unreadCount, useNotifications, } from "~/lib/notifications" import { Button } from "~/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu" import { Input } from "~/components/ui/input" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, } from "~/components/ui/sheet" import { ScriptsDialog, useScriptsHotkey } from "~/components/scripts-dialog" type NavItem = { to: string icon: React.ComponentType<{ className?: string }> label: string end?: boolean } const navItems: NavItem[] = [ { to: "/", icon: LayoutDashboard, label: "Overview", end: true }, { to: "/resources", icon: Boxes, label: "Resources" }, { to: "/activity", icon: Activity, label: "Activity" }, { to: "/assistant", icon: Sparkles, label: "Assistant" }, { to: "/ai", icon: Bot, label: "AI" }, { to: "/library", icon: BookOpen, label: "Library" }, { to: "/settings", icon: Settings, label: "Settings" }, // CREMA:NAV-ITEMS ] type AppShellProps = { title: string children: React.ReactNode brand?: Brand user?: User /** * Optional theme name. When set, the shell wraps itself in * `data-theme={theme}` so a route can opt into an alternate theme without * the caller having to add an extra wrapping div. */ theme?: string } export function AppShell({ title, children, brand: brandOverride, user: userOverride, theme, }: AppShellProps) { const defaultBrand = useBrand() const defaultUser = useUser() const profile = useProfile() const session = useSession() const navigate = useNavigate() const brand = brandOverride ?? defaultBrand // Prefer the live session for identity, fall back to the editable profile, // fall back to the stub user. const user = userOverride ?? { name: session?.name || profile.name || defaultUser.name, email: session?.email || profile.email || defaultUser.email, initials: profileInitials( session?.name || profile.name || defaultUser.name, ), } // Protected shell: bounce to /login when there's no session. useEffect(() => { if (typeof window === "undefined") return if (!session) { const next = encodeURIComponent( window.location.pathname + window.location.search, ) navigate(`/login?next=${next}`, { replace: true }) } }, [session, navigate]) if (!session) return null const [expanded, setExpanded] = useState(() => { if (typeof window === "undefined") return false return localStorage.getItem(SIDEBAR_KEY) === "1" }) useEffect(() => { localStorage.setItem(SIDEBAR_KEY, expanded ? "1" : "0") }, [expanded]) const [mobileOpen, setMobileOpen] = useState(false) const [scriptsOpen, setScriptsOpen] = useState(false) const BrandIcon = brand.icon useScriptsHotkey(() => setScriptsOpen(true)) return (
Skip to main content
{/* Mobile-only menu trigger, floating top-left of main */}
{brand.name}
{/* Floating glass pill, top-right, replacing the appbar action group */}
{profile.avatarUrl ? ( ) : null} {user.initials} {user.name} {user.email} navigate("/profile")} > Profile navigate("/settings")} > Settings Help { signOut() navigate("/login", { replace: true }) }} > Sign out
{children}
) } function NotificationDispatcher() { // Hidden bridge so the action bus can create real notifications: // fill notify-title "Hello" // fill notify-body "Body text" // fill notify-kind "info" # info|success|warning|error // fill notify-href "/library" # optional // click notify-create const titleRef = useRef(null) const bodyRef = useRef(null) const kindRef = useRef(null) const hrefRef = useRef(null) const submit = () => { const title = titleRef.current?.value.trim() ?? "" if (!title) return const body = bodyRef.current?.value.trim() || undefined const rawKind = (kindRef.current?.value || "info").trim().toLowerCase() const kind = ( ["info", "success", "warning", "error"].includes(rawKind) ? rawKind : "info" ) as "info" | "success" | "warning" | "error" const href = hrefRef.current?.value.trim() || undefined addNotification({ title, body, kind, href }) if (titleRef.current) titleRef.current.value = "" if (bodyRef.current) bodyRef.current.value = "" if (kindRef.current) kindRef.current.value = "" if (hrefRef.current) hrefRef.current.value = "" } return (
) } function NotificationsBell() { const items = useNotifications() const unread = unreadCount(items) const navigate = useNavigate() useEffect(() => { seedIfEmpty() }, []) return ( {unread > 0 && ( {unread > 9 ? "9+" : unread} )} } />
Notifications
    {items.length === 0 ? (
  • No notifications.
  • ) : ( items.map((n) => (
  • )) )}
) }