Feat: 워크스페이스 사이드바 컴포넌트 추가 S11P21S002-75

This commit is contained in:
jhynsoo 2024-08-28 16:08:12 +09:00
parent 525051f498
commit c19082949b
11 changed files with 369 additions and 0 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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 (
<>
<div
className={cn('flex cursor-pointer items-center gap-2 rounded-md py-0.5 pr-1 hover:bg-gray-200', className)}
style={{
paddingLeft,
}}
onClick={() => setIsExpanded((prev) => !prev)}
>
<button className="flex items-center">
<ChevronRight
size={16}
className={`stroke-gray-500 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
/>
</button>
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{item.name}</span>
</div>
{item.children?.map((child) => {
const childProps = {
className: isExpanded ? '' : 'hidden',
depth: depth + 1,
};
return child.type === 'directory' ? (
<ProjectDirectoryItem
key={`${item.name}-${child.name}`}
item={child}
{...childProps}
/>
) : (
<ProjectFileItem
key={`${item.name}-${child.name}`}
item={child}
{...childProps}
/>
);
})}
</>
);
}

View File

@ -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 (
<button
className={cn('flex w-full gap-2 rounded-md py-0.5 pr-1 hover:bg-gray-200', className)}
style={{
paddingLeft,
}}
>
<div className="flex items-center">
<Image
size={16}
className="stroke-gray-500"
/>
</div>
<span className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left">{item.name}</span>
{item.status === 'idle' ? (
<Minus
size={16}
className="stroke-gray-400"
/>
) : (
<Check
size={16}
className="stroke-green-500"
/>
)}
</button>
);
}

View File

@ -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 (
<div className="flex select-none flex-col px-1 pb-2">
<header className="flex w-full items-center gap-2 rounded px-1 hover:bg-gray-200">
<div
className="flex w-full cursor-pointer items-center gap-1 overflow-hidden pr-1"
onClick={() => setIsExpanded((prev) => !prev)}
>
<button>
<ChevronRight
size={16}
className={`stroke-gray-500 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
/>
</button>
<div className="flex flex-col overflow-hidden">
<h2 className="body-small-strong overflow-hidden text-ellipsis whitespace-nowrap">{project.name}</h2>
<h3 className="caption overflow-hidden text-ellipsis whitespace-nowrap">{project.type}</h3>
</div>
</div>
<button
className="flex gap-1"
onClick={() => console.log('edit project')}
>
<SquarePenIcon size={16} />
</button>
<button
className="flex gap-1"
onClick={() => console.log('upload image')}
>
<Upload size={16} />
</button>
</header>
<div className={`caption flex flex-col ${isExpanded ? '' : 'hidden'}`}>
{project.children.map((item) =>
item.type === 'directory' ? (
<ProjectDirectoryItem
key={`${project.id}-${item.name}`}
item={item}
className={isExpanded ? '' : 'hidden'}
/>
) : (
<ProjectFileItem
key={`${project.id}-${item.name}`}
item={item}
className={isExpanded ? '' : 'hidden'}
/>
)
)}
</div>
</div>
);
}

View File

@ -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<typeof Component> = {
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 = () => (
<div className="h-screen">
<ResizablePanelGroup direction="horizontal">
<WorkspaceSidebar
workspaceName="Workspace-name-1"
projects={projects}
/>
<ResizablePanel className="flex w-full items-center justify-center">
<div>Content</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);

View File

@ -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 (
<>
<ResizablePanel
minSize={10}
maxSize={35}
defaultSize={20}
className="flex h-full flex-col bg-gray-100"
onResize={(size) => {
console.log(size);
}}
>
<header className="body flex w-full items-center gap-2 p-2">
<h1 className="w-full overflow-hidden text-ellipsis whitespace-nowrap">{workspaceName}</h1>
<button>
<SquarePen size={16} />
</button>
<Button
variant="outline"
size="xs"
className="caption border-gray-800 bg-gray-100"
onClick={() => console.log('New project')}
>
</Button>
</header>
<div>
{projects.map((project) => (
<ProjectStructure
key={project.id}
project={project}
/>
))}
</div>
</ResizablePanel>
<ResizableHandle className="bg-gray-300" />
</>
);
}

View File

@ -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',

View File

@ -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<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-gray-200 after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90 dark:bg-gray-800 dark:focus-visible:ring-gray-300",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border border-gray-200 bg-gray-200 dark:border-gray-800 dark:bg-gray-800">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -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<DirectoryItem | FileItem>;
};
export type Project = {
id: number;
name: string;
type: 'Classification' | 'Detection' | 'Segmentation';
children: Array<DirectoryItem | FileItem>;
};

View File

@ -33,6 +33,10 @@ export default {
},
},
extend: {
padding: {
0.5: '0.125rem',
1.5: '0.375rem',
},
colors: {
transparent: 'transparent',
current: 'currentColor',