feat(ui): update landing + dashboard, fix scroll, share search

- replace template home page with Compass branding and WIP notice
- rewrite dashboard as project status page with working/in-progress/planned sections
- fix content scrolling inside inset panel (no more clipped rounded corners)
- extract command menu into shared context so sidebar search triggers it too
This commit is contained in:
Nicholai Vogel 2026-01-24 13:01:28 -07:00
parent bf806123cd
commit 0cf9e02c4e
7 changed files with 364 additions and 75 deletions

View File

@ -1,34 +1,40 @@
import { AppSidebar } from "@/components/app-sidebar"
import { SiteHeader } from "@/components/site-header"
import { CommandMenuProvider } from "@/components/command-menu-provider"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { getProjects } from "@/app/actions/projects"
export default function DashboardLayout({
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const projectList = await getProjects()
return (
<SidebarProvider
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<AppSidebar variant="inset" />
<SidebarInset>
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col">
<div className="px-4 pt-2">
<SidebarTrigger className="-ml-1" />
<CommandMenuProvider>
<SidebarProvider
defaultOpen={false}
className="h-screen overflow-hidden"
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<AppSidebar variant="inset" projects={projectList} />
<SidebarInset className="overflow-hidden">
<SiteHeader />
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
<div className="@container/main flex flex-1 flex-col">
{children}
</div>
{children}
</div>
</div>
</SidebarInset>
</SidebarProvider>
</SidebarInset>
</SidebarProvider>
</CommandMenuProvider>
)
}

View File

@ -1,17 +1,142 @@
import { ChartAreaInteractive } from "@/components/chart-area-interactive"
import { DataTable } from "@/components/data-table"
import { SectionCards } from "@/components/section-cards"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { IconExternalLink } from "@tabler/icons-react"
import data from "./data.json"
const GITHUB_URL =
"https://github.com/High-Performance-Structures/compass"
const working = [
"Projects — create and manage projects with D1 database",
"Schedule — Gantt chart with phases, tasks, dependencies, and baselines",
"File browser — drive-style UI with folder navigation",
"Sidebar navigation with contextual project/file views",
]
const inProgress = [
"User authentication and accounts",
"Settings page",
"Search functionality",
"Notifications",
]
const planned = [
"Role-based permissions",
"File uploads and storage (R2)",
"Project activity feed",
"Mobile-responsive layout improvements",
]
export default function Page() {
return (
<div className="flex flex-col gap-3 py-2 md:gap-4 md:py-3">
<SectionCards />
<div className="px-3 lg:px-4">
<ChartAreaInteractive />
<div className="flex flex-1 items-start justify-center p-4 md:p-8">
<div className="flex w-full max-w-2xl flex-col gap-6 py-8">
<div className="text-center">
<h1 className="text-3xl font-bold tracking-tight">
Compass
</h1>
<p className="text-muted-foreground mt-2 text-balance">
Development preview features may be incomplete
or change without notice.
</p>
</div>
<div className="grid gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<span className="inline-block size-2 rounded-full bg-green-500" />
Working
<Badge variant="secondary" className="font-normal">
{working.length}
</Badge>
</CardTitle>
<CardDescription>
Features that are functional in this preview
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-1.5 text-sm">
{working.map((item) => (
<li key={item} className="flex items-start gap-2">
<span className="text-muted-foreground mt-0.5"></span>
{item}
</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<span className="inline-block size-2 rounded-full bg-yellow-500" />
In Progress
<Badge variant="secondary" className="font-normal">
{inProgress.length}
</Badge>
</CardTitle>
<CardDescription>
Currently being developed
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-1.5 text-sm">
{inProgress.map((item) => (
<li key={item} className="flex items-start gap-2">
<span className="text-muted-foreground mt-0.5"></span>
{item}
</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<span className="inline-block size-2 rounded-full bg-muted-foreground/50" />
Planned
<Badge variant="secondary" className="font-normal">
{planned.length}
</Badge>
</CardTitle>
<CardDescription>
On the roadmap but not started
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-1.5 text-sm text-muted-foreground">
{planned.map((item) => (
<li key={item} className="flex items-start gap-2">
<span className="mt-0.5"></span>
{item}
</li>
))}
</ul>
</CardContent>
</Card>
</div>
<div className="flex justify-center pt-2">
<Button variant="outline" asChild>
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
>
<IconExternalLink className="mr-2 size-4" />
View on GitHub
</a>
</Button>
</div>
</div>
<DataTable data={data} />
</div>
)
}

View File

@ -7,14 +7,14 @@ export default function Home() {
<div className="flex min-h-screen items-center justify-center p-8">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Dashboard App Template</CardTitle>
<CardTitle className="text-2xl">Compass</CardTitle>
<CardDescription>
A Next.js starter with shadcn/ui components and Cloudflare deployment
Work-in-progress development preview
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
<Link href="/dashboard">Explore Dashboard</Link>
</Button>
</CardContent>
</Card>

View File

@ -16,7 +16,9 @@ import { usePathname } from "next/navigation"
import { NavMain } from "@/components/nav-main"
import { NavSecondary } from "@/components/nav-secondary"
import { NavFiles } from "@/components/nav-files"
import { NavProjects } from "@/components/nav-projects"
import { NavUser } from "@/components/nav-user"
import { useCommandMenu } from "@/components/command-menu-provider"
import {
Sidebar,
SidebarContent,
@ -25,17 +27,18 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
name: "Martine Vogel",
email: "martine@compass.io",
avatar: "/avatars/martine.jpg",
},
navMain: [
{
title: "Dashboard",
title: "Compass",
url: "/dashboard",
icon: IconDashboard,
},
@ -66,20 +69,61 @@ const data = {
url: "#",
icon: IconHelp,
},
{
title: "Search",
url: "#",
icon: IconSearch,
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
function SidebarNav({
projects,
}: {
projects: { id: string; name: string }[]
}) {
const pathname = usePathname()
const { state } = useSidebar()
const { open: openSearch } = useCommandMenu()
const isExpanded = state === "expanded"
const isFilesMode = pathname?.startsWith("/dashboard/files")
const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test(
pathname ?? ""
)
const showContext = isExpanded && (isFilesMode || isProjectMode)
const mode = showContext && isFilesMode
? "files"
: showContext && isProjectMode
? "projects"
: "main"
const secondaryItems = [
...data.navSecondary,
{ title: "Search", icon: IconSearch, onClick: openSearch },
]
return (
<Sidebar collapsible="offcanvas" {...props}>
<div key={mode} className="animate-in fade-in slide-in-from-left-1 flex flex-1 flex-col duration-150">
{mode === "files" && (
<React.Suspense>
<NavFiles />
</React.Suspense>
)}
{mode === "projects" && <NavProjects projects={projects} />}
{mode === "main" && (
<>
<NavMain items={data.navMain} />
<NavSecondary items={secondaryItems} className="mt-auto" />
</>
)}
</div>
)
}
export function AppSidebar({
projects = [],
...props
}: React.ComponentProps<typeof Sidebar> & {
projects?: { id: string; name: string }[]
}) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
@ -89,36 +133,16 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
>
<a href="/dashboard">
<IconInnerShadowTop className="!size-5" />
<span className="text-base font-semibold">COMPASS</span>
<span className="text-base font-semibold">
COMPASS
</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<div className="relative flex-1 overflow-hidden">
<div
className={`absolute inset-0 flex flex-col transition-all duration-200 ${
isFilesMode
? "-translate-x-full opacity-0"
: "translate-x-0 opacity-100"
}`}
>
<NavMain items={data.navMain} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</div>
<div
className={`absolute inset-0 flex flex-col transition-all duration-200 ${
isFilesMode
? "translate-x-0 opacity-100"
: "translate-x-full opacity-0"
}`}
>
<React.Suspense>
<NavFiles />
</React.Suspense>
</div>
</div>
<SidebarNav projects={projects} />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import { CommandMenu } from "@/components/command-menu"
const CommandMenuContext = React.createContext<{
open: () => void
}>({ open: () => {} })
export function useCommandMenu() {
return React.useContext(CommandMenuContext)
}
export function CommandMenuProvider({
children,
}: {
children: React.ReactNode
}) {
const [isOpen, setIsOpen] = React.useState(false)
const value = React.useMemo(
() => ({ open: () => setIsOpen(true) }),
[]
)
return (
<CommandMenuContext.Provider value={value}>
{children}
<CommandMenu open={isOpen} setOpen={setIsOpen} />
</CommandMenuContext.Provider>
)
}

View File

@ -17,8 +17,9 @@ export function NavSecondary({
}: {
items: {
title: string
url: string
url?: string
icon: Icon
onClick?: () => void
}[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
@ -27,11 +28,21 @@ export function NavSecondary({
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
<SidebarMenuButton
asChild={!item.onClick}
onClick={item.onClick}
>
{item.onClick ? (
<>
<item.icon />
<span>{item.title}</span>
</>
) : (
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
)}
</SidebarMenuButton>
</SidebarMenuItem>
))}

View File

@ -1,11 +1,102 @@
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import {
IconLogout,
IconMoon,
IconSearch,
IconSun,
IconUserCircle,
} from "@tabler/icons-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { SidebarTrigger } from "@/components/ui/sidebar"
import { NotificationsPopover } from "@/components/notifications-popover"
import { useCommandMenu } from "@/components/command-menu-provider"
import { AccountModal } from "@/components/account-modal"
export function SiteHeader() {
const { theme, setTheme } = useTheme()
const { open: openCommand } = useCommandMenu()
const [accountOpen, setAccountOpen] = React.useState(false)
return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger className="-ml-1" />
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<div
className="relative mx-auto w-full max-w-md cursor-pointer"
onClick={openCommand}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openCommand()
}}
>
<IconSearch className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
<div className="bg-muted/50 border-input flex h-9 w-full items-center rounded-md border pl-9 pr-3 text-sm">
<span className="text-muted-foreground flex-1">
Search...
</span>
<kbd className="bg-muted text-muted-foreground pointer-events-none ml-2 inline-flex h-5 items-center gap-0.5 rounded border px-1.5 font-mono text-xs">
<span className="text-xs">&#x2318;</span>K
</kbd>
</div>
</div>
<div className="flex items-center gap-1">
<NotificationsPopover />
<Button
variant="ghost"
size="icon"
className="size-8"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
{theme === "dark" ? (
<IconSun className="size-4" />
) : (
<IconMoon className="size-4" />
)}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="ml-1 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring">
<Avatar className="size-7 grayscale">
<AvatarImage src="/avatars/martine.jpg" alt="Martine Vogel" />
<AvatarFallback className="text-xs">MV</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel className="font-normal">
<p className="text-sm font-medium">Martine Vogel</p>
<p className="text-muted-foreground text-xs">martine@compass.io</p>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => setAccountOpen(true)}>
<IconUserCircle />
Account
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<IconLogout />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<AccountModal open={accountOpen} onOpenChange={setAccountOpen} />
</header>
)
}