Skip to content

Commit

Permalink
Merge pull request #176 from kosori/feat/tree-view
Browse files Browse the repository at this point in the history
  • Loading branch information
codingcodax authored Oct 11, 2024
2 parents 3070a87 + e6252ae commit b444194
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 1 deletion.
123 changes: 123 additions & 0 deletions apps/www/content/docs/ui/file-tree.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: File Tree
description: Renders a hierarchical structure of files and folders.
links:
doc: https://www.radix-ui.com/docs/primitives/components/collapsible
api: https://www.radix-ui.com/docs/primitives/components/collapsible#api-reference
dependencies: ['@radix-ui/react-collapsible']
---

<ComponentPreview name='file-tree'>
```json doc-gen:file
{
"file": "./src/components/demos/FileTree/FileTree.tsx",
"codeblock": true
}
```
</ComponentPreview>

## Installation

<Steps>
<Step>
### Install the primitive

Install the `@radix-ui/react-collapsible` package.

```package-install
@radix-ui/react-collapsible
```

</Step>

<Step>
### Copy-paste the component

Copy and paste the component code in a `.tsx` file.

```json doc-gen:file
{
"file": "../../packages/ui/src/file-tree.tsx",
"codeblock": true
}
```

</Step>

<Step>
### Update import paths

Update the `@kosori/ui` import paths to fit your project structure, for example, using `~/components/ui`.

</Step>

<Step>
### Add keyframes animation

Add the `collapsible-down` and `collapsible-up` keyframes animation to your Tailwind CSS config.

```ts title="tailwind.config.ts"
import type { Config } from 'tailwindcss';

const config: Config = {
// ...,
keyframes: {
// [!code highlight:9]
'collapsible-down': {
from: { height: '0' },
to: { height: 'var(--radix-collapsible-content-height)' },
},
'collapsible-up': {
from: { height: 'var(--radix-collapsible-content-height)' },
to: { height: '0' },
},
},
animation: {
// [!code highlight:3]
'collapsible-down': 'collapsible-down 0.15s ease-out',
'collapsible-up': 'collapsible-up 0.15s ease-out',
}
};
```

</Step>

</Steps>

## Usage

```ts
import {
File,
FileTree,
Folder,
FolderFiles,
FolderName,
} from '~/components/ui/file-tree';
```

```tsx
<FileTree>
<Folder>
<FolderName>app</FolderName>
<FolderFiles>
<File>layout.tsx</File>
<File>page.tsx</File>
</FolderFiles>
</Folder>
</FileTree>
```

## Reference

### File

<TypeTable
type={{
icon: {
default: `<FileIcon />`,
description: 'Optional icon to display next to the file name.',
type: 'ReactNode',
},
}}
/>
9 changes: 9 additions & 0 deletions apps/www/src/components/ComponentPreview/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,15 @@ export const Components: Record<string, Component> = {
})),
),
},
'file-tree': {
name: 'file-tree',
type: 'component:example',
component: lazy(() =>
import('../demos/FileTree').then((module) => ({
default: module.FileTreeDemo,
})),
),
},
'dropdown-menu-checkboxes': {
name: 'dropdown-menu-checkboxes',
type: 'component:example',
Expand Down
43 changes: 43 additions & 0 deletions apps/www/src/components/demos/FileTree/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
File,
FileTree,
Folder,
FolderFiles,
FolderName,
} from '@kosori/ui/file-tree';

export const FileTreeDemo = () => {
return (
<FileTree className='w-full max-w-xs'>
<Folder>
<FolderName>app</FolderName>

<FolderFiles>
<Folder>
<FolderName>components</FolderName>

<FolderFiles>
<File>button.tsx</File>
<File>card.tsx</File>
</FolderFiles>
</Folder>

<File>layout.tsx</File>
<File>global.css</File>
<File>page.tsx</File>
</FolderFiles>
</Folder>

<Folder defaultOpen>
<FolderName>public</FolderName>

<FolderFiles>
<File>favicon.ico</File>
<File>index.html</File>
</FolderFiles>
</Folder>

<File>package.json</File>
</FileTree>
);
};
1 change: 1 addition & 0 deletions apps/www/src/components/demos/FileTree/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './FileTree';
1 change: 0 additions & 1 deletion packages/ui/src/accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ const { item, trigger, triggerIcon, content } = accordionStyles();
*
* @see {@link https://dub.sh/ui-accordion Accordion Docs} for further information.
*/

export const Accordion = Root;

type AccordionItemRef = React.ElementRef<typeof Item>;
Expand Down
187 changes: 187 additions & 0 deletions packages/ui/src/file-tree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
'use client';

import { forwardRef } from 'react';
import { clsx } from 'clsx/lite';
import { FileIcon, FolderIcon, FolderOpenIcon } from 'lucide-react';
import { tv } from 'tailwind-variants';

import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@kosori/ui/collapsible';

const fileTreeStyles = tv({
slots: {
fileTree: 'rounded-lg border bg-grey-bg-subtle p-2',
folder: 'w-full',
folderName: clsx(
'group w-full inline-flex items-center gap-2 rounded-lg px-2 h-8',
'hover:bg-grey-bg-hover',
),
folderFiles: clsx(
'overflow-hidden',
'data-[state=closed]:animate-collapsible-up',
'data-[state=open]:animate-collapsible-down',
),
file: clsx(
'w-full inline-flex h-8 select-none items-center gap-2 rounded-lg px-2 text-sm',
'hover:bg-grey-bg-hover',
),
},
});

const { fileTree, folder, folderName, folderFiles, file } = fileTreeStyles();

type FileTreeRef = HTMLDivElement;
type FileTreeProps = React.ComponentPropsWithoutRef<'div'>;

/**
* FileTree component that represents a hierarchical structure of files and folders.
*
* @param {FileTreeProps} props - Additional props to pass to the file tree container.
*
* @example
* <FileTree>
* <Folder>
* <FolderName>Documents</FolderName>
* <FolderFiles>
* <File>Resume.pdf</File>
* <File>CoverLetter.docx</File>
* </FolderFiles>
* </Folder>
* <Folder>
* <FolderName>Images</FolderName>
* <FolderFiles>
* <File>Vacation.jpg</File>
* <File>Profile.png</File>
* </FolderFiles>
* </Folder>
* </FileTree>
*/
export const FileTree = forwardRef<FileTreeRef, FileTreeProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={fileTree({ className })} {...props} />
),
);

FileTree.displayName = 'FileTree';

type FolderRef = React.ElementRef<typeof Collapsible>;
type FolderProps = React.ComponentPropsWithoutRef<typeof Collapsible>;

/**
* Folder component that represents a collapsible folder in the file tree.
*
* @param {FolderProps} props - Additional props to pass to the folder component.
*
* @example
* <Folder>
* <FolderName>My Folder</FolderName>
* <FolderFiles>
* <File>File1.txt</File>
* <File>File2.txt</File>
* </FolderFiles>
* </Folder>
*/
export const Folder = forwardRef<FolderRef, FolderProps>(
({ className, ...props }, ref) => (
<Collapsible ref={ref} className={folder({ className })} {...props} />
),
);

Folder.displayName = 'Folder';

type FolderNameRef = React.ElementRef<typeof CollapsibleTrigger>;
type FolderNameProps = React.ComponentPropsWithoutRef<
typeof CollapsibleTrigger
>;

/**
* FolderName component that acts as the clickable header for a Folder.
*
* @param {FolderNameProps} props - Additional props to pass to the folder name.
*
* @example
* <FolderName>My Folder</FolderName>
*/
export const FolderName = forwardRef<FolderNameRef, FolderNameProps>(
({ className, children, ...props }, ref) => (
<CollapsibleTrigger
ref={ref}
className={folderName({ className })}
{...props}
>
<FolderIcon
className={clsx('size-4', 'group-data-[state=open]:hidden')}
/>
<FolderOpenIcon
className={clsx('size-4', 'group-data-[state=closed]:hidden')}
/>
{children}
</CollapsibleTrigger>
),
);

FolderName.displayName = 'FolderName';

type FolderFilesRef = React.ElementRef<typeof CollapsibleContent>;
type FolderFilesProps = React.ComponentPropsWithoutRef<
typeof CollapsibleContent
>;

/**
* FolderFiles component that displays the contents of a Folder.
*
* @param {FolderFilesProps} props - Additional props to pass to the folder files container.
*
* @example
* <FolderFiles>
* <File>File1.txt</File>
* <File>File2.txt</File>
* </FolderFiles>
*/
export const FolderFiles = forwardRef<FolderFilesRef, FolderFilesProps>(
({ className, children, ...props }, ref) => (
<CollapsibleContent
ref={ref}
className={folderFiles({ className })}
{...props}
>
<div className='ms-2 flex flex-col border-l ps-2'>{children}</div>
</CollapsibleContent>
),
);

FolderFiles.displayName = 'FolderFiles';

type FileRef = HTMLDivElement;
type FileProps = React.ComponentPropsWithoutRef<'div'> & {
/**
* Optional icon to display next to the file name.
*/
icon?: React.ReactNode;
};

/**
* File component that represents a single file in the file tree.
*
* @param {FileProps} props - Additional props to pass to the file component.
* @param {React.ReactNode} [icon=<FileIcon />] - Optional icon to display next to the file name.
*
* @example
* <File>MyFile.txt</File>
*/
export const File = forwardRef<FileRef, FileProps>(
(
{ className, icon = <FileIcon className='size-4' />, children, ...props },
ref,
) => (
<div ref={ref} className={file({ className })} {...props}>
{icon}
{children}
</div>
),
);

File.displayName = 'File';
Loading

0 comments on commit b444194

Please sign in to comment.