diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index c1878ce..3fa64d0 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -20,6 +20,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
+ "react-resizable-panels": "^2.1.1",
"react-router-dom": "^6.26.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
@@ -10264,6 +10265,16 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
+ "node_modules/react-resizable-panels": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.1.tgz",
+ "integrity": "sha512-+cUV/yZBYfiBj+WJtpWDJ3NtR4zgDZfHt3+xtaETKE+FCvp+RK/NJxacDQKxMHgRUTSkfA6AnGljQ5QZNsCQoA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-router": {
"version": "6.26.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index bbbb134..9249900 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -26,6 +26,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
+ "react-resizable-panels": "^2.1.1",
"react-router-dom": "^6.26.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
diff --git a/frontend/src/components/WorkspaceSidebar/ProjectDirectoryItem.tsx b/frontend/src/components/WorkspaceSidebar/ProjectDirectoryItem.tsx
new file mode 100644
index 0000000..aee6484
--- /dev/null
+++ b/frontend/src/components/WorkspaceSidebar/ProjectDirectoryItem.tsx
@@ -0,0 +1,58 @@
+import { DirectoryItem } from '@/types';
+import { ChevronRight } from 'lucide-react';
+import { useState } from 'react';
+import ProjectFileItem from './ProjectFileItem';
+import { cn } from '@/lib/utils';
+
+export default function ProjectDirectoryItem({
+ className = '',
+ item,
+ depth = 1,
+}: {
+ className?: string;
+ item: DirectoryItem;
+ depth?: number;
+}) {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const paddingLeft = depth * 12;
+
+ return (
+ <>
+
setIsExpanded((prev) => !prev)}
+ >
+
+ {item.name}
+
+ {item.children?.map((child) => {
+ const childProps = {
+ className: isExpanded ? '' : 'hidden',
+ depth: depth + 1,
+ };
+
+ return child.type === 'directory' ? (
+
+ ) : (
+
+ );
+ })}
+ >
+ );
+}
diff --git a/frontend/src/components/WorkspaceSidebar/ProjectFileItem.tsx b/frontend/src/components/WorkspaceSidebar/ProjectFileItem.tsx
new file mode 100644
index 0000000..7f6a3a4
--- /dev/null
+++ b/frontend/src/components/WorkspaceSidebar/ProjectFileItem.tsx
@@ -0,0 +1,43 @@
+import { cn } from '@/lib/utils';
+import { FileItem } from '@/types';
+import { Check, Image, Minus } from 'lucide-react';
+
+export default function ProjectFileItem({
+ className = '',
+ item,
+ depth = 1,
+}: {
+ className?: string;
+ item: FileItem;
+ depth?: number;
+}) {
+ const paddingLeft = depth * 12;
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx
new file mode 100644
index 0000000..181740c
--- /dev/null
+++ b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx
@@ -0,0 +1,60 @@
+import { Project } from '@/types';
+import { ChevronRight, SquarePenIcon, Upload } from 'lucide-react';
+import { useState } from 'react';
+import ProjectFileItem from './ProjectFileItem';
+import ProjectDirectoryItem from './ProjectDirectoryItem';
+
+export default function ProjectStructure({ project }: { project: Project }) {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ return (
+
+
+
+ {project.children.map((item) =>
+ item.type === 'directory' ? (
+
+ ) : (
+
+ )
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/WorkspaceSidebar/index.stories.tsx b/frontend/src/components/WorkspaceSidebar/index.stories.tsx
new file mode 100644
index 0000000..03eef0c
--- /dev/null
+++ b/frontend/src/components/WorkspaceSidebar/index.stories.tsx
@@ -0,0 +1,82 @@
+import '@/index.css';
+import WorkspaceSidebar from '.';
+import { ResizablePanel, ResizablePanelGroup } from '../ui/resizable';
+import { Project } from '@/types';
+import { Meta } from '@storybook/react';
+import { Component } from 'react';
+
+const meta: Meta = {
+ title: 'Workspace/WorkspaceSidebar',
+ component: WorkspaceSidebar,
+ parameters: {
+ layout: 'fullscreen',
+ },
+};
+
+export default meta;
+
+const projects: Project[] = [
+ {
+ id: 1,
+ name: 'project-111',
+ type: 'Segmentation',
+ children: [
+ {
+ id: 12,
+ type: 'directory',
+ name: 'directory-1',
+ children: [
+ {
+ id: 123,
+ type: 'directory',
+ name: 'directory-2',
+ children: [
+ { id: 1, url: '', type: 'image', name: 'image-1.jpg', status: 'done' },
+ { id: 1, url: '', type: 'image', name: 'image-2.jpg', status: 'idle' },
+ ],
+ },
+ { id: 1, url: '', type: 'image', name: 'image-1.jpg', status: 'idle' },
+ { id: 1, url: '', type: 'image', name: 'image-2.jpg', status: 'done' },
+ ],
+ },
+ { id: 1, url: '', type: 'image', name: 'image-1.jpg', status: 'done' },
+ ],
+ },
+ {
+ id: 2,
+ name: 'very-extremely-long-long-project-name-222',
+ type: 'Classification',
+ children: [
+ {
+ id: 23,
+ type: 'directory',
+ name: 'this-is-my-very-very-long-directory-name-that-will-be-overflow',
+ children: [
+ { id: 1, url: '', type: 'image', name: 'image-1.jpg', status: 'done' },
+ { id: 1, url: '', type: 'image', name: 'image-2.jpg', status: 'done' },
+ ],
+ },
+ {
+ id: 1,
+ url: '',
+ type: 'image',
+ name: 'wow-this-is-my-very-very-long-image-name-so-this-will-be-overflow-too.jpg',
+ status: 'idle',
+ },
+ ],
+ },
+];
+
+export const Default = () => (
+
+);
diff --git a/frontend/src/components/WorkspaceSidebar/index.tsx b/frontend/src/components/WorkspaceSidebar/index.tsx
new file mode 100644
index 0000000..748e9f0
--- /dev/null
+++ b/frontend/src/components/WorkspaceSidebar/index.tsx
@@ -0,0 +1,45 @@
+import { SquarePen } from 'lucide-react';
+import { ResizableHandle, ResizablePanel } from '../ui/resizable';
+import ProjectStructure from './ProjectStructure';
+import { Button } from '../ui/button';
+import { Project } from '@/types';
+
+export default function WorkspaceSidebar({ workspaceName, projects }: { workspaceName: string; projects: Project[] }) {
+ return (
+ <>
+ {
+ console.log(size);
+ }}
+ >
+
+ {workspaceName}
+
+
+
+
+ {projects.map((project) => (
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
index c2fe07a..9a10612 100644
--- a/frontend/src/components/ui/button.tsx
+++ b/frontend/src/components/ui/button.tsx
@@ -24,6 +24,7 @@ const buttonVariants = cva(
},
size: {
default: 'h-10 px-4 py-2',
+ xs: 'rounded-md px-2 py-1',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
diff --git a/frontend/src/components/ui/resizable.tsx b/frontend/src/components/ui/resizable.tsx
new file mode 100644
index 0000000..a3bba1c
--- /dev/null
+++ b/frontend/src/components/ui/resizable.tsx
@@ -0,0 +1,43 @@
+import { GripVertical } from "lucide-react"
+import * as ResizablePrimitive from "react-resizable-panels"
+
+import { cn } from "@/lib/utils"
+
+const ResizablePanelGroup = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+)
+
+const ResizablePanel = ResizablePrimitive.Panel
+
+const ResizableHandle = ({
+ withHandle,
+ className,
+ ...props
+}: React.ComponentProps & {
+ withHandle?: boolean
+}) => (
+ div]:rotate-90 dark:bg-gray-800 dark:focus-visible:ring-gray-300",
+ className
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+)
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
new file mode 100644
index 0000000..1bfb42d
--- /dev/null
+++ b/frontend/src/types/index.ts
@@ -0,0 +1,21 @@
+export type FileItem = {
+ id: number;
+ name: string;
+ url: string;
+ type: 'image' | 'json';
+ status: 'idle' | 'done';
+};
+
+export type DirectoryItem = {
+ id: number;
+ name: string;
+ type: 'directory';
+ children: Array;
+};
+
+export type Project = {
+ id: number;
+ name: string;
+ type: 'Classification' | 'Detection' | 'Segmentation';
+ children: Array;
+};
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index 54ed2fb..e2a6b48 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -33,6 +33,10 @@ export default {
},
},
extend: {
+ padding: {
+ 0.5: '0.125rem',
+ 1.5: '0.375rem',
+ },
colors: {
transparent: 'transparent',
current: 'currentColor',