feat: add navigation link to projects in CanvasMenubar and enhance AppSidebar with tooltip for expand button
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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)}>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user