feat(ui): consume common app shell

This commit is contained in:
Saravana Achu Mac 2026-05-06 13:35:54 -07:00
parent 4cfe5aee5e
commit 63211c0019
4 changed files with 96 additions and 67 deletions

View File

@ -25,10 +25,12 @@
--bl-accent-muted: var(--nl-accent-muted);
--bl-surface-card: var(--nl-surface-card);
--bl-surface-muted: var(--nl-surface-muted);
--bl-surface-sidebar: var(--nl-surface-sidebar);
--bl-text-primary: var(--nl-text-primary);
--bl-text-secondary: var(--nl-text-secondary);
--bl-text-tertiary: var(--nl-text-tertiary);
--bl-border: var(--nl-border-default);
--bl-overlay-scrim: var(--nl-overlay-scrim);
--bl-success: var(--nl-status-success);
--bl-success-muted: var(--nl-success-muted);
--bl-success-border: var(--nl-status-success);
@ -92,6 +94,7 @@ button {
.main-panel {
min-width: 0;
padding: var(--nl-space-8);
padding-left: calc(var(--bl-app-sidebar-width, 280px) + var(--nl-space-8));
}
.app-shell > .sidebar {

View File

@ -3,7 +3,14 @@
import { type ReactNode, useState, useCallback, useEffect } from "react";
import { usePathname } from "next/navigation";
import { Sidebar } from "@/components/Sidebar";
import { Button, Card } from "@/components/ui/Primitives";
import {
AppShell as AppShellFrame,
AppShellMain,
AppShellMobileToggle,
AppShellOverlay,
AppShellPageHeader,
AppShellSkipLink,
} from "@/components/ui/Primitives";
export function AppShell({
title,
@ -27,47 +34,15 @@ export function AppShell({
const close = useCallback(() => setSidebarOpen(false), []);
return (
<div className="app-shell">
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<Button
className="sidebar-toggle"
onClick={toggle}
aria-label={sidebarOpen ? "Close menu" : "Open menu"}
type="button"
variant="secondary"
size="sm"
>
{sidebarOpen ? "\u2715" : "\u2630"}
</Button>
<div
className={`sidebar-overlay${sidebarOpen ? " open" : ""}`}
onClick={close}
aria-hidden="true"
/>
<AppShellFrame sidebarWidth={280}>
<AppShellSkipLink />
<AppShellMobileToggle open={sidebarOpen} onClick={toggle} />
<AppShellOverlay open={sidebarOpen} onClick={close} />
<Sidebar open={sidebarOpen} />
<main id="main-content" className="main-panel" tabIndex={-1} aria-labelledby="page-title">
<div className="page-grid">
<header>
<Card
style={{ padding: "var(--nl-space-6)", display: "flex", justifyContent: "space-between", gap: "var(--nl-space-4)", alignItems: "start", flexWrap: "wrap" }}
>
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}>
<h1
id="page-title"
style={{ margin: 0, fontFamily: "var(--nl-font-display)", fontSize: "var(--nl-fs-2xl)", fontWeight: 700 }}
>
{title}
</h1>
<div style={{ color: "var(--nl-text-secondary)", maxWidth: 720 }}>{description}</div>
</div>
{actions ? <div aria-label="Page actions">{actions}</div> : null}
</Card>
</header>
{children}
</div>
</main>
</div>
<AppShellMain labelledBy="page-title" className="main-panel">
<AppShellPageHeader title={title} description={description} actions={actions} />
{children}
</AppShellMain>
</AppShellFrame>
);
}

View File

@ -1,10 +1,16 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { House, Search, Settings, Sparkles, FolderKanban, ShieldCheck, MessageCircle, Brain } from "lucide-react";
import { PRODUCT_NAME } from "@/lib/product-config";
import { isFeatureEnabled } from "@/lib/feature-flags";
import {
AppShellNav,
AppShellNavItem,
AppShellSidebar,
Badge,
Panel,
} from "@/components/ui/Primitives";
const navItems: { href: string; label: string; icon: typeof House; flag?: string }[] = [
{ href: "/dashboard", label: "Dashboard", icon: House },
@ -21,56 +27,47 @@ export function Sidebar({ open }: { open?: boolean }) {
const pathname = usePathname() ?? "";
return (
<aside className={`sidebar app-sidebar${open ? ' open' : ''}`} style={{ padding: "var(--nl-space-6)" }} aria-label="Primary">
<AppShellSidebar open={open ?? false} width={280} className="sidebar p-[var(--nl-space-6)]">
<div style={{ display: "grid", gap: "var(--nl-space-6)" }}>
<div className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
<div className="badge" style={{ width: "fit-content" }}>
<Panel as="div" style={{ display: "grid", gap: "var(--nl-space-3)" }}>
<Badge style={{ width: "fit-content" }}>
<Sparkles size={14} />
Web Agent workstream
</div>
</Badge>
<div>
<div style={{ fontSize: "var(--nl-fs-xl)", fontFamily: "var(--nl-font-display)", fontWeight: 700 }}>{PRODUCT_NAME}</div>
<div style={{ color: "var(--nl-text-secondary)", marginTop: 6 }}>
Human-first notes UX with agent-safe structure and review surfaces.
</div>
</div>
</div>
</Panel>
<nav aria-label="Primary navigation" style={{ display: "grid", gap: "var(--nl-space-2)" }}>
<AppShellNav>
{navItems.filter((item) => !item.flag || isFeatureEnabled(item.flag)).map((item) => {
const Icon = item.icon;
const active = pathname === item.href || pathname.startsWith(`${item.href}/`);
return (
<Link
<AppShellNavItem
key={item.href}
href={item.href}
className="surface-muted nav-link"
aria-current={active ? "page" : undefined}
style={{
display: "flex",
alignItems: "center",
gap: "var(--nl-space-3)",
padding: "12px 14px",
borderColor: active ? "var(--nl-accent-primary)" : undefined,
background: active ? "var(--nl-accent-muted)" : undefined,
}}
active={active}
icon={<Icon size={18} />}
>
<Icon size={18} />
<span>{item.label}</span>
</Link>
{item.label}
</AppShellNavItem>
);
})}
</nav>
</AppShellNav>
<div className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-2)" }}>
<Panel as="div" style={{ display: "grid", gap: "var(--nl-space-2)" }}>
<strong>Keyboard flow</strong>
<span style={{ color: "var(--nl-text-secondary)" }}>
<kbd style={{ opacity: 0.85 }}>K</kbd> / <kbd style={{ opacity: 0.85 }}>Ctrl+K</kbd> command palette
</span>
<span style={{ color: "var(--nl-text-secondary)" }}>Use Tab to move between navigation, filters, and dense result surfaces.</span>
<span style={{ color: "var(--nl-text-secondary)" }}>Use the skip link to jump directly into page content.</span>
</div>
</Panel>
</div>
</aside>
</AppShellSidebar>
);
}

View File

@ -1,4 +1,13 @@
import {
AppShell as BytelystAppShell,
AppShellMain as BytelystAppShellMain,
AppShellMobileToggle as BytelystAppShellMobileToggle,
AppShellNav as BytelystAppShellNav,
AppShellNavItem as BytelystAppShellNavItem,
AppShellOverlay as BytelystAppShellOverlay,
AppShellPageHeader as BytelystAppShellPageHeader,
AppShellSidebar as BytelystAppShellSidebar,
AppShellSkipLink as BytelystAppShellSkipLink,
Badge as BytelystBadge,
Button as BytelystButton,
Card as BytelystCard,
@ -53,6 +62,15 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
type AppShellMainProps,
type AppShellMobileToggleProps,
type AppShellNavItemProps,
type AppShellNavProps,
type AppShellOverlayProps,
type AppShellPageHeaderProps,
type AppShellProps,
type AppShellSidebarProps,
type AppShellSkipLinkProps,
type BadgeProps,
type ButtonProps,
type CardProps,
@ -144,6 +162,42 @@ const statusToneMap: Record<NoteLettStatus, StatusTone> = {
review: "accent",
};
export function AppShell({ className, ...props }: AppShellProps) {
return <BytelystAppShell className={className} {...props} />;
}
export function AppShellSkipLink({ className, ...props }: AppShellSkipLinkProps) {
return <BytelystAppShellSkipLink className={className} {...props} />;
}
export function AppShellMobileToggle({ className, ...props }: AppShellMobileToggleProps) {
return <BytelystAppShellMobileToggle className={className} {...props} />;
}
export function AppShellOverlay({ className, ...props }: AppShellOverlayProps) {
return <BytelystAppShellOverlay className={className} {...props} />;
}
export function AppShellSidebar({ className, ...props }: AppShellSidebarProps) {
return <BytelystAppShellSidebar className={className} {...props} />;
}
export function AppShellMain({ className, ...props }: AppShellMainProps) {
return <BytelystAppShellMain className={className} {...props} />;
}
export function AppShellPageHeader({ className, ...props }: AppShellPageHeaderProps) {
return <BytelystAppShellPageHeader className={className} {...props} />;
}
export function AppShellNav({ className, ...props }: AppShellNavProps) {
return <BytelystAppShellNav className={className} {...props} />;
}
export function AppShellNavItem({ className, ...props }: AppShellNavItemProps) {
return <BytelystAppShellNavItem className={className} {...props} />;
}
export function getStatusTone(status: NoteLettStatus): StatusTone {
return statusToneMap[status];
}