feat(ui): consume common app shell
This commit is contained in:
parent
4cfe5aee5e
commit
63211c0019
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user