- 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.
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
/**
|
|
* 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>
|
|
)
|
|
}
|