feat(tracker-web): adopt @bytelyst/ui AppShell nav shell (UX-8)
Replace the hand-rolled sticky top nav in the dashboard layout with the shared AppShell (AppShellSidebar/AppShellNav/AppShellNavItem/AppShellMain/ AppShellSkipLink + mobile toggle + overlay). The sidebar keeps the ProductSwitcher, user email and Sign out, and adds a ⌘K trigger (replays the global hotkey) and a theme toggle. Nav items use aria-current for the active route and client-side navigation; the skip-link targets the focusable main region. AppShell exports are routed through the Primitives adapter (CC.6 ratchet) and covered by the export-presence test. AppShellPageHeader is intentionally not used so the per-page PageHeader (UX-10) remains the single h1 per route (no duplicate headings). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
bfccd48b37
commit
cbd4274a52
@ -83,6 +83,16 @@ const EXPECTED_EXPORTS = [
|
||||
'DataListItem',
|
||||
'DataListMeta',
|
||||
'Timeline',
|
||||
// UX-8 AppShell additions
|
||||
'AppShell',
|
||||
'AppShellMain',
|
||||
'AppShellMobileToggle',
|
||||
'AppShellNav',
|
||||
'AppShellNavItem',
|
||||
'AppShellOverlay',
|
||||
'AppShellPageHeader',
|
||||
'AppShellSidebar',
|
||||
'AppShellSkipLink',
|
||||
] as const;
|
||||
|
||||
describe('Primitives adapter — export presence', () => {
|
||||
|
||||
@ -1,9 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
AppShell,
|
||||
AppShellSkipLink,
|
||||
AppShellMobileToggle,
|
||||
AppShellOverlay,
|
||||
AppShellSidebar,
|
||||
AppShellNav,
|
||||
AppShellNavItem,
|
||||
AppShellMain,
|
||||
Button,
|
||||
} from '@/components/ui/Primitives';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { useTheme } from '@/lib/theme-context';
|
||||
import { ProductSwitcher } from '@/components/product-switcher';
|
||||
import { SystemBanners } from '@/components/system-banners';
|
||||
|
||||
@ -13,9 +25,17 @@ const NAV_ITEMS = [
|
||||
{ href: '/dashboard/board', label: 'Board' },
|
||||
];
|
||||
|
||||
/** Open the ⌘K command palette by replaying the global hotkey. */
|
||||
function openCommandPalette() {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true }));
|
||||
}
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading, logout } = useAuth();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [navOpen, setNavOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
@ -31,45 +51,71 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
);
|
||||
}
|
||||
|
||||
const go = (href: string) => (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setNavOpen(false);
|
||||
router.push(href);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Top nav bar */}
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-card/80 backdrop-blur-sm">
|
||||
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4">
|
||||
<div className="flex items-center gap-6">
|
||||
<Link href="/dashboard" className="text-lg font-bold tracking-tight">
|
||||
<AppShell>
|
||||
<AppShellSkipLink />
|
||||
<AppShellMobileToggle open={navOpen} onClick={() => setNavOpen(o => !o)} />
|
||||
<AppShellOverlay open={navOpen} onClick={() => setNavOpen(false)} />
|
||||
|
||||
<AppShellSidebar open={navOpen} label="Primary">
|
||||
<div className="flex h-full flex-col gap-6 p-4">
|
||||
<Link href="/dashboard" className="px-1 text-lg font-bold tracking-tight">
|
||||
Tracker
|
||||
</Link>
|
||||
<nav className="flex items-center gap-1">
|
||||
|
||||
<AppShellNav>
|
||||
{NAV_ITEMS.map(item => (
|
||||
<Link
|
||||
<AppShellNavItem
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
active={
|
||||
item.href === '/dashboard'
|
||||
? pathname === item.href
|
||||
: pathname.startsWith(item.href)
|
||||
}
|
||||
onClick={go(item.href)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</AppShellNavItem>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<ProductSwitcher />
|
||||
<span className="text-sm text-muted-foreground">{user.email}</span>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</AppShellNav>
|
||||
|
||||
{/* Page content */}
|
||||
<main className="mx-auto max-w-7xl px-4 py-6">
|
||||
<div className="mt-auto space-y-3">
|
||||
<ProductSwitcher />
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
onClick={openCommandPalette}
|
||||
>
|
||||
Search… ⌘K
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
>
|
||||
{theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
</Button>
|
||||
<div className="truncate px-1 text-sm text-muted-foreground">{user.email}</div>
|
||||
<Button variant="ghost" size="sm" className="w-full justify-start" onClick={logout}>
|
||||
Sign out
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AppShellSidebar>
|
||||
|
||||
<AppShellMain>
|
||||
<SystemBanners />
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</AppShellMain>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@ -146,3 +146,25 @@ export {
|
||||
} from '@bytelyst/ui';
|
||||
|
||||
export { Timeline, type TimelineItem, type TimelineProps } from '@bytelyst/ui';
|
||||
|
||||
// ── UX-8: AppShell nav shell ──────────────────────────────────────────────
|
||||
export {
|
||||
AppShell,
|
||||
AppShellMain,
|
||||
AppShellMobileToggle,
|
||||
AppShellNav,
|
||||
AppShellNavItem,
|
||||
AppShellOverlay,
|
||||
AppShellPageHeader,
|
||||
AppShellSidebar,
|
||||
AppShellSkipLink,
|
||||
type AppShellMainProps,
|
||||
type AppShellMobileToggleProps,
|
||||
type AppShellNavItemProps,
|
||||
type AppShellNavProps,
|
||||
type AppShellOverlayProps,
|
||||
type AppShellPageHeaderProps,
|
||||
type AppShellProps,
|
||||
type AppShellSidebarProps,
|
||||
type AppShellSkipLinkProps,
|
||||
} from '@bytelyst/ui';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user