From e441d36cf484f785c1f7d12a559a2cd23c1e4c33 Mon Sep 17 00:00:00 2001 From: dtoro Date: Mon, 9 Mar 2026 22:10:32 +0100 Subject: [PATCH] feat: implement project management features with routing, including project creation, deletion, renaming, and graph persistence --- frontend/package-lock.json | 42 +++ frontend/package.json | 1 + frontend/src/app/canvas/CanvasPage.tsx | 37 ++- frontend/src/app/platform/AppSidebar.tsx | 83 +++--- frontend/src/app/platform/CanvasRoute.tsx | 34 +++ frontend/src/app/platform/PlatformPage.tsx | 137 +++------ frontend/src/app/platform/ProjectsPage.tsx | 269 ++++++++++++++++++ frontend/src/app/platform/platformContext.tsx | 108 +++++++ .../src/app/platform/projectGraphStorage.ts | 36 +++ frontend/src/app/platform/types.ts | 2 + frontend/src/components/ui/table.tsx | 120 ++++++++ frontend/src/main.tsx | 13 +- 12 files changed, 740 insertions(+), 142 deletions(-) create mode 100644 frontend/src/app/platform/CanvasRoute.tsx create mode 100644 frontend/src/app/platform/ProjectsPage.tsx create mode 100644 frontend/src/app/platform/platformContext.tsx create mode 100644 frontend/src/app/platform/projectGraphStorage.ts create mode 100644 frontend/src/components/ui/table.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 20eca3b..dcc5888 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "nunjucks": "^3.2.4", "react": "18.2.0", "react-dom": "18.2.0", + "react-router-dom": "^6.28.0", "react-zoom-pan-pinch": "^3.7.0", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7" @@ -2827,6 +2828,15 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -6895,6 +6905,38 @@ } } }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0345ee1..c9bf33d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "nunjucks": "^3.2.4", "react": "18.2.0", "react-dom": "18.2.0", + "react-router-dom": "^6.28.0", "react-zoom-pan-pinch": "^3.7.0", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7" diff --git a/frontend/src/app/canvas/CanvasPage.tsx b/frontend/src/app/canvas/CanvasPage.tsx index 46323a6..c35f97f 100644 --- a/frontend/src/app/canvas/CanvasPage.tsx +++ b/frontend/src/app/canvas/CanvasPage.tsx @@ -57,6 +57,12 @@ import { isConnectionAllowed, } from '@/lib/nodeRegistry' import type { AppNode, AppEdge } from '@/lib/nodeTypes' +import { + loadGraphFromStorage, + saveGraphToStorage, + PROJECT_FILE_EXT, + PROJECT_VERSION, +} from '@/app/platform/projectGraphStorage' const SNAP_GRID: [number, number] = [15, 15] const snapToGrid = (x: number, y: number): { x: number; y: number } => ({ @@ -110,9 +116,6 @@ function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } { } } -const PROJECT_FILE_EXT = '.zui.json' -const PROJECT_VERSION = 1 - export type CanvasMessage = { type: 'success' | 'error'; text: string } function FlowFitViewOnLoad() { @@ -129,8 +132,19 @@ export type CanvasPageProps = { projectId?: string } -export function CanvasPage({ projectId: _projectId }: CanvasPageProps) { +function getInitialGraph(projectId: string | undefined): { nodes: AppNode[]; edges: AppEdge[] } { + if (projectId) { + const stored = loadGraphFromStorage(projectId) + if (stored && (stored.nodes.length > 0 || stored.edges.length > 0)) { + return { nodes: stored.nodes as AppNode[], edges: stored.edges as AppEdge[] } + } + } + return getExampleGraph() +} + +export function CanvasPage({ projectId }: CanvasPageProps) { const { theme } = useTheme() + const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId]) const { nodes, edges, @@ -145,7 +159,20 @@ export function CanvasPage({ projectId: _projectId }: CanvasPageProps) { canUndo, canRedo, setStateImmediate, - } = useGraphStateWithHistory(getExampleGraph().nodes, getExampleGraph().edges) + } = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges) + + // Persist graph to localStorage when projectId is set (debounced) + const saveTimeoutRef = React.useRef | null>(null) + React.useEffect(() => { + if (!projectId) return + const save = () => { + saveGraphToStorage(projectId, { version: PROJECT_VERSION, nodes, edges }) + } + saveTimeoutRef.current = setTimeout(save, 500) + return () => { + if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current) + } + }, [projectId, nodes, edges]) const importInputRef = useRef(null) const [rfInstance, setRfInstance] = React.useState(null) diff --git a/frontend/src/app/platform/AppSidebar.tsx b/frontend/src/app/platform/AppSidebar.tsx index 4ea84b7..4485bed 100644 --- a/frontend/src/app/platform/AppSidebar.tsx +++ b/frontend/src/app/platform/AppSidebar.tsx @@ -1,8 +1,10 @@ /** * Platform sidebar (sidebar-07 style): projects list, create, delete, collapse to icons. + * Uses platform context and router for navigation. */ import React from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' import { Sidebar, SidebarContent, @@ -25,26 +27,29 @@ import { } from '@/components/ui/dropdown-menu' import { MoreHorizontal, Plus, Trash2 } from 'lucide-react' import { useSidebar } from '@/components/ui/sidebar' -import type { Project } from './types' +import { usePlatform } from './platformContext' import { getProjectIcon } from './icon-map' import { NewProjectDialog } from './NewProjectDialog' -type AppSidebarProps = { - projects: Project[] - selectedProjectId: string | null - onSelectProject: (id: string) => void - onDeleteProject: (id: string) => void - onCreateProject: (project: Project) => void -} - -export function AppSidebar({ - projects, - selectedProjectId, - onSelectProject, - onDeleteProject, - onCreateProject, -}: AppSidebarProps) { +export function AppSidebar() { const { isMobile } = useSidebar() + const { projects, createProject, deleteProject } = usePlatform() + const navigate = useNavigate() + const { projectId: selectedProjectId } = useParams<{ projectId: string }>() + + const handleSelectProject = (id: string) => { + navigate(`/projects/${id}`) + } + + const handleCreateProject = (project: Parameters[0]) => { + createProject(project) + navigate(`/projects/${project.id}`) + } + + const handleDeleteProject = (id: string) => { + deleteProject(id) + if (selectedProjectId === id) navigate('/projects', { replace: true }) + } return ( <> @@ -52,25 +57,27 @@ export function AppSidebar({ - - - - - ZOË + + + + + + ZOË + @@ -87,7 +94,7 @@ export function AppSidebar({ onSelectProject(project.id)} + onClick={() => handleSelectProject(project.id)} > {project.name} @@ -106,7 +113,7 @@ export function AppSidebar({ > onDeleteProject(project.id)} + onClick={() => handleDeleteProject(project.id)} > Delete project @@ -118,7 +125,7 @@ export function AppSidebar({ })} diff --git a/frontend/src/app/platform/CanvasRoute.tsx b/frontend/src/app/platform/CanvasRoute.tsx new file mode 100644 index 0000000..391328d --- /dev/null +++ b/frontend/src/app/platform/CanvasRoute.tsx @@ -0,0 +1,34 @@ +/** + * Route wrapper for the canvas: resolves projectId from URL and updates lastEditedAt on open. + */ + +import React, { useEffect } from 'react' +import { useParams } from 'react-router-dom' +import { CanvasPage } from '../canvas/CanvasPage' +import { usePlatform } from './platformContext' + +export function CanvasRoute() { + const { projectId } = useParams<{ projectId: string }>() + const { projects, updateLastEdited } = usePlatform() + + const project = projects.find((p) => p.id === projectId) + + useEffect(() => { + if (projectId) updateLastEdited(projectId) + }, [projectId, updateLastEdited]) + + if (!projectId) return null + if (!project) { + return ( +
+

Project not found.

+
+ ) + } + + return ( +
+ +
+ ) +} diff --git a/frontend/src/app/platform/PlatformPage.tsx b/frontend/src/app/platform/PlatformPage.tsx index 445c554..466cd9b 100644 --- a/frontend/src/app/platform/PlatformPage.tsx +++ b/frontend/src/app/platform/PlatformPage.tsx @@ -1,9 +1,9 @@ /** - * Platform: main entry layout with sidebar (sidebar-07). Projects in sidebar; - * when a project is selected, the canvas is shown in the main area. + * Platform: main layout with sidebar and header. Renders child routes (projects list or canvas) via Outlet. */ -import React, { useCallback, useState } from 'react' +import React from 'react' +import { Link, Outlet, useParams, useLocation } from 'react-router-dom' import { Breadcrumb, BreadcrumbItem, @@ -14,79 +14,19 @@ import { } 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 { PlatformProvider, usePlatform } from './platformContext' 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(loadProjects) - const [selectedProjectId, setSelectedProjectId] = useState(() => { - 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) +function PlatformLayoutInner() { + const { projectId } = useParams<{ projectId: string }>() + const location = useLocation() + const { projects } = usePlatform() + const project = projectId ? projects.find((p) => p.id === projectId) : null + const isProjectsList = location.pathname === '/projects' return ( - +
@@ -94,40 +34,41 @@ export function PlatformPage() { - - Projects - - - - - {selectedProject ? selectedProject.name : 'No project'} - - + {isProjectsList ? ( + + Projects + + ) : ( + <> + + + Projects + + + + + + {project ? project.name : 'Project'} + + + + )}
- {selectedProjectId && selectedProject ? ( -
- -
- ) : ( -
-

No project selected. Create one to open the canvas.

- - - New project - - } - /> -
- )} +
) } + +export function PlatformPage() { + return ( + + + + ) +} diff --git a/frontend/src/app/platform/ProjectsPage.tsx b/frontend/src/app/platform/ProjectsPage.tsx new file mode 100644 index 0000000..5cd8c32 --- /dev/null +++ b/frontend/src/app/platform/ProjectsPage.tsx @@ -0,0 +1,269 @@ +/** + * Projects list page: table of all projects with metadata, sorted by last edited. + * Actions: open, rename, export, delete. Pagination. + */ + +import React, { useMemo, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { ChevronLeft, ChevronRight, MoreHorizontal, Pencil, Trash2, Download, FolderOpen } from 'lucide-react' +import { usePlatform } from './platformContext' +import { getProjectIcon } from './icon-map' +import { + loadGraphFromStorage, + removeGraphFromStorage, + PROJECT_FILE_EXT, + PROJECT_VERSION, +} from './projectGraphStorage' +import type { Project } from './types' + +const PAGE_SIZE = 10 + +function formatDate(ts: number) { + return new Date(ts).toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +} + +export function ProjectsPage() { + const { projects, deleteProject, renameProject } = usePlatform() + const navigate = useNavigate() + const [page, setPage] = useState(0) + const [renameTarget, setRenameTarget] = useState(null) + const [renameValue, setRenameValue] = useState('') + const [deleteTarget, setDeleteTarget] = useState(null) + + const sorted = useMemo(() => { + return [...projects].sort((a, b) => (b.lastEditedAt ?? b.createdAt) - (a.lastEditedAt ?? a.createdAt)) + }, [projects]) + + const totalPages = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE)) + const start = page * PAGE_SIZE + const pageItems = sorted.slice(start, start + PAGE_SIZE) + + const handleOpen = (projectId: string) => { + navigate(`/projects/${projectId}`) + } + + const handleRenameOpen = (project: Project) => { + setRenameTarget(project) + setRenameValue(project.name) + } + + const handleRenameSubmit = () => { + if (renameTarget && renameValue.trim()) { + renameProject(renameTarget.id, renameValue.trim()) + setRenameTarget(null) + setRenameValue('') + } + } + + const handleDeleteOpen = (project: Project) => { + setDeleteTarget(project) + } + + const handleDeleteConfirm = () => { + if (deleteTarget) { + removeGraphFromStorage(deleteTarget.id) + deleteProject(deleteTarget.id) + setDeleteTarget(null) + navigate('/projects', { replace: true }) + } + } + + const handleExport = (project: Project) => { + const stored = loadGraphFromStorage(project.id) + const state = stored + ? { version: PROJECT_VERSION, nodes: stored.nodes, edges: stored.edges } + : { version: PROJECT_VERSION, nodes: [], edges: [] } + const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${project.name.replace(/[^\w.-]/g, '_')}${PROJECT_FILE_EXT}` + a.click() + URL.revokeObjectURL(url) + } + + return ( +
+
+
+

Projects

+
+ {sorted.length === 0 ? ( +

No projects yet. Create one from the sidebar.

+ ) : ( + <> + + + + Name + Created + Last edited + + + + + {pageItems.map((project) => { + const Icon = getProjectIcon(project.iconId) + return ( + handleOpen(project.id)} + > + +
+ + {project.name} +
+
+ + {formatDate(project.createdAt)} + + + {formatDate(project.lastEditedAt ?? project.createdAt)} + + e.stopPropagation()}> + + + + + + handleOpen(project.id)}> + + Open + + handleRenameOpen(project)}> + + Rename + + handleExport(project)}> + + Export + + handleDeleteOpen(project)} + > + + Delete + + + + +
+ ) + })} +
+
+ {totalPages > 1 && ( +
+

+ Page {page + 1} of {totalPages} +

+
+ + +
+
+ )} + + )} +
+ + {/* Rename dialog */} + !open && setRenameTarget(null)}> + + + Rename project + Enter a new name for this project. + + setRenameValue(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleRenameSubmit()} + placeholder="Project name" + /> + + + + + + + + {/* Delete confirmation */} + !open && setDeleteTarget(null)}> + + + Delete project + + {deleteTarget + ? `Are you sure you want to delete "${deleteTarget.name}"? This cannot be undone.` + : ''} + + + + + + + + +
+ ) +} diff --git a/frontend/src/app/platform/platformContext.tsx b/frontend/src/app/platform/platformContext.tsx new file mode 100644 index 0000000..bbffd4f --- /dev/null +++ b/frontend/src/app/platform/platformContext.tsx @@ -0,0 +1,108 @@ +/** + * Platform context: projects list and handlers for create/delete/rename/updateLastEdited. + * Used by AppSidebar, ProjectsTablePage, and canvas route. + */ + +import React, { createContext, useCallback, useContext, useState } from 'react' +import type { Project } from './types' + +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' + ) + .map((p) => ({ + ...p, + lastEditedAt: typeof (p as Project).lastEditedAt === 'number' ? (p as Project).lastEditedAt : p.createdAt, + })) + } catch { + return [] + } +} + +function saveProjects(projects: Project[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(projects)) +} + +export type PlatformContextValue = { + projects: Project[] + persist: (next: Project[]) => void + createProject: (project: Project) => void + deleteProject: (id: string) => void + renameProject: (id: string, name: string) => void + updateLastEdited: (id: string) => void +} + +const PlatformContext = createContext(null) + +export function PlatformProvider({ children }: { children: React.ReactNode }) { + const [projects, setProjects] = useState(loadProjects) + + const persist = useCallback((next: Project[]) => { + setProjects(next) + saveProjects(next) + }, []) + + const createProject = useCallback( + (project: Project) => { + const withEdited = { ...project, lastEditedAt: project.createdAt } + persist([...projects, withEdited]) + }, + [projects, persist] + ) + + const deleteProject = useCallback( + (id: string) => { + const next = projects.filter((p) => p.id !== id) + persist(next) + }, + [projects, persist] + ) + + const renameProject = useCallback( + (id: string, name: string) => { + const next = projects.map((p) => (p.id === id ? { ...p, name } : p)) + persist(next) + }, + [projects, persist] + ) + + const updateLastEdited = useCallback( + (id: string) => { + const now = Date.now() + const next = projects.map((p) => (p.id === id ? { ...p, lastEditedAt: now } : p)) + persist(next) + }, + [projects, persist] + ) + + const value: PlatformContextValue = { + projects, + persist, + createProject, + deleteProject, + renameProject, + updateLastEdited, + } + + return {children} +} + +export function usePlatform() { + const ctx = useContext(PlatformContext) + if (!ctx) throw new Error('usePlatform must be used within PlatformProvider') + return ctx +} diff --git a/frontend/src/app/platform/projectGraphStorage.ts b/frontend/src/app/platform/projectGraphStorage.ts new file mode 100644 index 0000000..90b1b10 --- /dev/null +++ b/frontend/src/app/platform/projectGraphStorage.ts @@ -0,0 +1,36 @@ +/** + * Per-project graph persistence (localStorage). + * Used by CanvasPage to load/save and by ProjectsTablePage for export. + */ + +export const PROJECT_FILE_EXT = '.zui.json' +export const PROJECT_VERSION = 1 + +const GRAPH_KEY_PREFIX = 'zui_graph_' + +export function getGraphStorageKey(projectId: string): string { + return `${GRAPH_KEY_PREFIX}${projectId}` +} + +export type GraphState = { version: number; nodes: unknown[]; edges: unknown[] } + +export function loadGraphFromStorage(projectId: string): GraphState | null { + try { + const raw = localStorage.getItem(getGraphStorageKey(projectId)) + if (!raw) return null + const data = JSON.parse(raw) as unknown + if (!data || typeof data !== 'object' || !Array.isArray((data as GraphState).nodes) || !Array.isArray((data as GraphState).edges)) + return null + return data as GraphState + } catch { + return null + } +} + +export function saveGraphToStorage(projectId: string, state: GraphState): void { + localStorage.setItem(getGraphStorageKey(projectId), JSON.stringify(state)) +} + +export function removeGraphFromStorage(projectId: string): void { + localStorage.removeItem(getGraphStorageKey(projectId)) +} diff --git a/frontend/src/app/platform/types.ts b/frontend/src/app/platform/types.ts index 3c4808f..51eb67d 100644 --- a/frontend/src/app/platform/types.ts +++ b/frontend/src/app/platform/types.ts @@ -10,6 +10,8 @@ export type Project = { /** Icon identifier: key of PROJECT_ICONS map */ iconId: string createdAt: number + /** Last time the project was opened/edited; used for sorting. Defaults to createdAt if missing. */ + lastEditedAt?: number } /** Project icon ids: Lucide "Animals" category only */ diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/frontend/src/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 5289f2a..0c3e4f9 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,8 +1,11 @@ import React from 'react' import { createRoot } from 'react-dom/client' +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' import { ThemeProvider } from './lib/themeContext' import { registerBuiltinNodes } from './lib/registerBuiltinNodes' import { PlatformPage } from './app/platform/PlatformPage' +import { ProjectsPage } from './app/platform/ProjectsPage' +import { CanvasRoute } from './app/platform/CanvasRoute' import './styles.css' import '@xyflow/react/dist/style.css' @@ -11,7 +14,15 @@ registerBuiltinNodes() createRoot(document.getElementById('root')!).render( - + + + }> + } /> + } /> + } /> + + + )