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:
parent
bf806123cd
commit
0cf9e02c4e
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />
|
||||
|
||||
32
src/components/command-menu-provider.tsx
Executable file
32
src/components/command-menu-provider.tsx
Executable 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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">⌘</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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user