feat: add navigation link to projects in CanvasMenubar and enhance AppSidebar with tooltip for expand button

This commit is contained in:
2026-03-10 00:01:59 +01:00
parent 3ad02cfb76
commit bb58727e04
3 changed files with 99 additions and 76 deletions

View File

@@ -3,6 +3,7 @@
*/ */
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { Link } from 'react-router-dom'
import { import {
Menubar, Menubar,
MenubarContent, MenubarContent,
@@ -17,7 +18,7 @@ import {
} from '@/components/ui/menubar' } from '@/components/ui/menubar'
import { Kbd, KbdGroup } from '@/components/ui/kbd' import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { useTheme } from '@/lib/themeContext' import { useTheme } from '@/lib/themeContext'
import { Download, FolderOpen, Moon, Redo2, Sun, Undo2 } from 'lucide-react' import { ArrowLeft, Download, FolderOpen, Moon, Redo2, Sun, Undo2 } from 'lucide-react'
export type CanvasMenubarProps = { export type CanvasMenubarProps = {
onImport: () => void onImport: () => void
@@ -72,6 +73,13 @@ export function CanvasMenubar({
return ( return (
<Menubar className="shrink-0 rounded-none border-x-0 border-t-0 border-b-0"> <Menubar className="shrink-0 rounded-none border-x-0 border-t-0 border-b-0">
<Link
to="/projects"
aria-label="Back to projects"
className="flex shrink-0 items-center rounded-sm px-2 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground"
>
<ArrowLeft className="size-4" />
</Link>
<MenubarMenu> <MenubarMenu>
<MenubarTrigger className="font-medium">Project</MenubarTrigger> <MenubarTrigger className="font-medium">Project</MenubarTrigger>
<MenubarContent> <MenubarContent>

View File

@@ -36,6 +36,7 @@ import {
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { MoreHorizontal, Plus, Trash2, Pencil, FolderOpen, ListTodo } from 'lucide-react' import { MoreHorizontal, Plus, Trash2, Pencil, FolderOpen, ListTodo } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSidebar } from '@/components/ui/sidebar' import { useSidebar } from '@/components/ui/sidebar'
import { usePlatform } from './platformContext' import { usePlatform } from './platformContext'
import { getProjectIcon } from './icon-map' import { getProjectIcon } from './icon-map'
@@ -43,7 +44,8 @@ import { NewProjectDialog } from './NewProjectDialog'
import type { Project } from './types' import type { Project } from './types'
export function AppSidebar() { export function AppSidebar() {
const { isMobile } = useSidebar() const { isMobile, state, setOpen } = useSidebar()
const isCollapsed = state === 'collapsed'
const { orderedProjects, createProject, deleteProject, renameProject, reorderProjects, projectOrder } = usePlatform() const { orderedProjects, createProject, deleteProject, renameProject, reorderProjects, projectOrder } = usePlatform()
const navigate = useNavigate() const navigate = useNavigate()
const { projectId: selectedProjectId } = useParams<{ projectId: string }>() const { projectId: selectedProjectId } = useParams<{ projectId: string }>()
@@ -121,36 +123,84 @@ export function AppSidebar() {
return ( return (
<> <>
<Sidebar collapsible="icon"> <Sidebar collapsible="icon" className="relative">
<SidebarHeader> {isCollapsed && (
<SidebarMenu> <button
type="button"
className="absolute inset-0 z-0 cursor-pointer border-0 bg-transparent p-0 outline-none"
onClick={() => setOpen(true)}
aria-label="Expand sidebar"
/>
)}
<SidebarHeader className="relative z-10 flex flex-row items-center gap-2">
<SidebarMenu className="flex-1 min-w-0">
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild size="lg" tooltip="Zoë" className="font-semibold font-serif"> {isCollapsed ? (
<Link to="/projects"> <Tooltip>
<span className="flex size-8 min-w-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground dark:bg-white dark:text-black"> <TooltipTrigger asChild>
<span <SidebarMenuButton
className="size-6 shrink-0 rounded-[2px] opacity-90" size="lg"
style={{ className="font-semibold font-serif"
maskImage: 'url(/app-icon.svg)', onClick={(e) => {
maskRepeat: 'no-repeat', e.preventDefault()
maskPosition: 'center', e.stopPropagation()
maskSize: 'contain', setOpen(true)
WebkitMaskImage: 'url(/app-icon.svg)',
WebkitMaskRepeat: 'no-repeat',
WebkitMaskPosition: 'center',
WebkitMaskSize: 'contain',
backgroundColor: 'currentColor',
}} }}
aria-hidden >
/> <span className="flex size-8 min-w-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground dark:bg-white dark:text-black">
</span> <span
<span className="font-serif group-data-[collapsible=icon]:hidden uppercase tracking-wide">ZOË</span> className="size-6 shrink-0 rounded-[2px] opacity-90"
</Link> style={{
</SidebarMenuButton> maskImage: 'url(/app-icon.svg)',
maskRepeat: 'no-repeat',
maskPosition: 'center',
maskSize: 'contain',
WebkitMaskImage: 'url(/app-icon.svg)',
WebkitMaskRepeat: 'no-repeat',
WebkitMaskPosition: 'center',
WebkitMaskSize: 'contain',
backgroundColor: 'currentColor',
}}
aria-hidden
/>
</span>
<span className="font-serif group-data-[collapsible=icon]:hidden uppercase tracking-wide">ZOË</span>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent side="right">Click to expand sidebar</TooltipContent>
</Tooltip>
) : (
<SidebarMenuButton asChild size="lg" tooltip="Zoë" className="font-semibold font-serif">
<Link to="/projects">
<span className="flex size-8 min-w-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground dark:bg-white dark:text-black">
<span
className="size-6 shrink-0 rounded-[2px] opacity-90"
style={{
maskImage: 'url(/app-icon.svg)',
maskRepeat: 'no-repeat',
maskPosition: 'center',
maskSize: 'contain',
WebkitMaskImage: 'url(/app-icon.svg)',
WebkitMaskRepeat: 'no-repeat',
WebkitMaskPosition: 'center',
WebkitMaskSize: 'contain',
backgroundColor: 'currentColor',
}}
aria-hidden
/>
</span>
<span className="font-serif group-data-[collapsible=icon]:hidden uppercase tracking-wide">ZOË</span>
</Link>
</SidebarMenuButton>
)}
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
{!isCollapsed && <SidebarTrigger className="ml-auto shrink-0" />}
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent
className="relative z-10"
onClick={isCollapsed ? (e) => { if ((e.target as HTMLElement).closest('button') || (e.target as HTMLElement).closest('a')) return; setOpen(true) } : undefined}
>
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">Projects</SidebarGroupLabel> <SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">Projects</SidebarGroupLabel>
<SidebarMenu> <SidebarMenu>
@@ -250,8 +300,8 @@ export function AppSidebar() {
</SidebarMenu> </SidebarMenu>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
<SidebarFooter /> <SidebarFooter className="relative z-10" />
<SidebarRail /> <SidebarRail className="relative z-10" />
</Sidebar> </Sidebar>
<Dialog open={!!renameTarget} onOpenChange={(open) => !open && setRenameTarget(null)}> <Dialog open={!!renameTarget} onOpenChange={(open) => !open && setRenameTarget(null)}>

View File

@@ -2,65 +2,30 @@
* Platform: main layout with sidebar and header. Renders child routes (projects list or canvas) via Outlet. * Platform: main layout with sidebar and header. Renders child routes (projects list or canvas) via Outlet.
*/ */
import React from 'react' import React, { useEffect, useState } from 'react'
import { Link, Outlet, useParams, useLocation } from 'react-router-dom' import { Outlet, useParams } from 'react-router-dom'
import { import { SidebarProvider } from '@/components/ui/sidebar'
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import { Separator } from '@/components/ui/separator'
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
import { PlatformProvider, usePlatform } from './platformContext' import { PlatformProvider, usePlatform } from './platformContext'
import { AppSidebar } from './AppSidebar' import { AppSidebar } from './AppSidebar'
function PlatformLayoutInner() { function PlatformLayoutInner() {
const { projectId } = useParams<{ projectId: string }>() const { projectId } = useParams<{ projectId: string }>()
const location = useLocation()
const { projects } = usePlatform() const { projects } = usePlatform()
const project = projectId ? projects.find((p) => p.id === projectId) : null const [sidebarOpen, setSidebarOpen] = useState(true)
const isProjectsList = location.pathname === '/projects'
useEffect(() => {
if (projectId) setSidebarOpen(false)
else setSidebarOpen(true)
}, [projectId])
return ( return (
<SidebarProvider> <SidebarProvider open={sidebarOpen} onOpenChange={setSidebarOpen}>
<AppSidebar /> <AppSidebar />
<SidebarInset className="flex min-h-0 flex-1 flex-col"> <div className="relative flex min-h-0 w-full flex-1 flex-col bg-background">
<header className="flex h-12 shrink-0 items-center gap-2 border-b border-border/40 px-1 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
{isProjectsList ? (
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">Projects</BreadcrumbPage>
</BreadcrumbItem>
) : (
<>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink asChild>
<Link to="/projects">Projects</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">
{project ? project.name : 'Project'}
</BreadcrumbPage>
</BreadcrumbItem>
</>
)}
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex min-h-0 flex-1 flex-col"> <div className="flex min-h-0 flex-1 flex-col">
<Outlet /> <Outlet />
</div> </div>
</SidebarInset> </div>
</SidebarProvider> </SidebarProvider>
) )
} }