feat: add Sheet and Sidebar components for improved UI layout
- Introduced a new Sheet component for modal-like functionality with customizable content and animations. - Implemented a Sidebar component with mobile responsiveness, state management, and keyboard shortcuts for toggling. - Added Skeleton component for loading states and Tooltip component for enhanced user guidance. - Created a useIsMobile hook to manage mobile view detection. - Updated main entry point to render the new PlatformPage component. - Enhanced styles for sidebar and scrollbar customization in CSS. - Extended Tailwind configuration to include sidebar color variables for better theming.
This commit is contained in:
133
frontend/src/app/platform/PlatformPage.tsx
Normal file
133
frontend/src/app/platform/PlatformPage.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Platform: main entry layout with sidebar (sidebar-07). Projects in sidebar;
|
||||
* when a project is selected, the canvas is shown in the main area.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import {
|
||||
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 { Button } from '@/components/ui/button'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { AppSidebar } from './AppSidebar'
|
||||
import { CanvasPage } from '../canvas/CanvasPage'
|
||||
import type { Project } from './types'
|
||||
import { NewProjectDialog } from './NewProjectDialog'
|
||||
|
||||
const STORAGE_KEY = 'zui_platform_projects'
|
||||
|
||||
function loadProjects(): Project[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed.filter(
|
||||
(p): p is Project =>
|
||||
p &&
|
||||
typeof p === 'object' &&
|
||||
typeof (p as Project).id === 'string' &&
|
||||
typeof (p as Project).name === 'string' &&
|
||||
typeof (p as Project).iconId === 'string' &&
|
||||
typeof (p as Project).createdAt === 'number'
|
||||
)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveProjects(projects: Project[]) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(projects))
|
||||
}
|
||||
|
||||
export function PlatformPage() {
|
||||
const [projects, setProjects] = useState<Project[]>(loadProjects)
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(() => {
|
||||
const list = loadProjects()
|
||||
return list.length > 0 ? list[0].id : null
|
||||
})
|
||||
|
||||
const persist = useCallback((next: Project[]) => {
|
||||
setProjects(next)
|
||||
saveProjects(next)
|
||||
}, [])
|
||||
|
||||
const handleCreateProject = useCallback(
|
||||
(project: Project) => {
|
||||
persist([...projects, project])
|
||||
setSelectedProjectId(project.id)
|
||||
},
|
||||
[projects, persist]
|
||||
)
|
||||
|
||||
const handleDeleteProject = useCallback(
|
||||
(id: string) => {
|
||||
const next = projects.filter((p) => p.id !== id)
|
||||
persist(next)
|
||||
if (selectedProjectId === id) setSelectedProjectId(next[0]?.id ?? null)
|
||||
},
|
||||
[projects, persist, selectedProjectId]
|
||||
)
|
||||
|
||||
const selectedProject = projects.find((p) => p.id === selectedProjectId)
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar
|
||||
projects={projects}
|
||||
selectedProjectId={selectedProjectId}
|
||||
onSelectProject={setSelectedProjectId}
|
||||
onDeleteProject={handleDeleteProject}
|
||||
onCreateProject={handleCreateProject}
|
||||
/>
|
||||
<SidebarInset className="flex min-h-0 flex-1 flex-col">
|
||||
<header className="flex h-12 shrink-0 items-center gap-2 border-b border-border/40 px-4 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>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="#">Zui</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="line-clamp-1">
|
||||
{selectedProject ? selectedProject.name : 'No project'}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{selectedProjectId && selectedProject ? (
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<CanvasPage projectId={selectedProjectId} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 rounded-xl border border-dashed bg-muted/30 p-8">
|
||||
<p className="text-sm text-muted-foreground">No project selected. Create one to open the canvas.</p>
|
||||
<NewProjectDialog
|
||||
onCreate={handleCreateProject}
|
||||
trigger={
|
||||
<Button>
|
||||
<Plus className="size-4" />
|
||||
New project
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user