240 lines
5.4 KiB
TypeScript
240 lines
5.4 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { AnimatePresence, motion } from 'motion/react';
|
|
import { FileIcon, FolderIcon, FolderOpenIcon } from 'lucide-react';
|
|
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionItemProps,
|
|
AccordionTrigger,
|
|
AccordionTriggerProps,
|
|
useAccordionItem,
|
|
} from '@/registry/radix/accordion';
|
|
import {
|
|
MotionHighlight,
|
|
MotionHighlightItem,
|
|
} from '@/registry/effects/motion-highlight';
|
|
|
|
type FileButtonProps = React.ComponentProps<'div'> & {
|
|
icons?: {
|
|
close: React.ReactNode;
|
|
open: React.ReactNode;
|
|
};
|
|
icon?: React.ReactNode;
|
|
open?: boolean;
|
|
sideComponent?: React.ReactNode;
|
|
};
|
|
|
|
function FileButton({
|
|
children,
|
|
className,
|
|
icons,
|
|
icon,
|
|
open,
|
|
sideComponent,
|
|
...props
|
|
}: FileButtonProps) {
|
|
return (
|
|
<MotionHighlightItem className="size-full">
|
|
<div
|
|
data-slot="file-button"
|
|
className={cn(
|
|
'flex items-center truncate gap-2 p-2 h-10 relative z-10 rounded-lg w-full cursor-default',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<span className="flex [&_svg]:size-4 [&_svg]:shrink-0 items-center gap-2 shrink-1 truncate">
|
|
{icon
|
|
? typeof icon !== 'string'
|
|
? icon
|
|
: null
|
|
: icons && (
|
|
<AnimatePresence mode="wait">
|
|
<motion.span
|
|
key={open ? 'open' : 'close'}
|
|
initial={{ scale: 0.9 }}
|
|
animate={{ scale: 1 }}
|
|
exit={{ scale: 0.9 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
{open
|
|
? typeof icons.open !== 'string'
|
|
? icons.open
|
|
: null
|
|
: typeof icons.close !== 'string'
|
|
? icons.close
|
|
: null}
|
|
</motion.span>
|
|
</AnimatePresence>
|
|
)}
|
|
<span className="shrink-1 text-sm block truncate break-words">
|
|
{children}
|
|
</span>
|
|
</span>
|
|
{sideComponent}
|
|
</div>
|
|
</MotionHighlightItem>
|
|
);
|
|
}
|
|
|
|
type FilesProps = React.ComponentProps<'div'> & {
|
|
children: React.ReactNode;
|
|
activeClassName?: string;
|
|
defaultOpen?: string[];
|
|
open?: string[];
|
|
onOpenChange?: (open: string[]) => void;
|
|
};
|
|
|
|
function Files({
|
|
children,
|
|
className,
|
|
activeClassName,
|
|
defaultOpen,
|
|
open,
|
|
onOpenChange,
|
|
...props
|
|
}: FilesProps) {
|
|
return (
|
|
<div
|
|
data-slot="files"
|
|
className={cn(
|
|
'relative size-full rounded-xl border bg-background overflow-auto',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<MotionHighlight
|
|
controlledItems
|
|
mode="parent"
|
|
hover
|
|
className={cn(
|
|
'bg-muted rounded-lg pointer-events-none',
|
|
activeClassName,
|
|
)}
|
|
>
|
|
<Accordion
|
|
type="multiple"
|
|
className="p-2"
|
|
defaultValue={defaultOpen}
|
|
value={open}
|
|
onValueChange={onOpenChange}
|
|
>
|
|
{children}
|
|
</Accordion>
|
|
</MotionHighlight>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type FolderTriggerProps = AccordionTriggerProps & {
|
|
sideComponent?: React.ReactNode;
|
|
};
|
|
|
|
function FolderTrigger({
|
|
children,
|
|
className,
|
|
sideComponent,
|
|
...props
|
|
}: FolderTriggerProps) {
|
|
const { isOpen } = useAccordionItem();
|
|
|
|
return (
|
|
<AccordionTrigger
|
|
data-slot="folder-trigger"
|
|
className="h-auto py-0 hover:no-underline font-normal relative z-10 max-w-full"
|
|
{...props}
|
|
chevron={false}
|
|
>
|
|
<FileButton
|
|
open={isOpen}
|
|
icons={{ open: <FolderOpenIcon />, close: <FolderIcon /> }}
|
|
className={className}
|
|
sideComponent={sideComponent}
|
|
>
|
|
{children}
|
|
</FileButton>
|
|
</AccordionTrigger>
|
|
);
|
|
}
|
|
|
|
type FolderProps = Omit<
|
|
AccordionItemProps,
|
|
'value' | 'onValueChange' | 'defaultValue' | 'children'
|
|
> & {
|
|
children?: React.ReactNode;
|
|
name: string;
|
|
open?: string[];
|
|
onOpenChange?: (open: string[]) => void;
|
|
defaultOpen?: string[];
|
|
sideComponent?: React.ReactNode;
|
|
};
|
|
|
|
function Folder({
|
|
children,
|
|
className,
|
|
name,
|
|
open,
|
|
defaultOpen,
|
|
onOpenChange,
|
|
sideComponent,
|
|
...props
|
|
}: FolderProps) {
|
|
return (
|
|
<AccordionItem
|
|
data-slot="folder"
|
|
value={name}
|
|
className="relative border-b-0"
|
|
{...props}
|
|
>
|
|
<FolderTrigger className={className} sideComponent={sideComponent}>
|
|
{name}
|
|
</FolderTrigger>
|
|
{children && (
|
|
<AccordionContent className="relative pb-0 !ml-7 before:absolute before:-left-3 before:inset-y-0 before:w-px before:h-full before:bg-border">
|
|
<Accordion
|
|
type="multiple"
|
|
defaultValue={defaultOpen}
|
|
value={open}
|
|
onValueChange={onOpenChange}
|
|
>
|
|
{children}
|
|
</Accordion>
|
|
</AccordionContent>
|
|
)}
|
|
</AccordionItem>
|
|
);
|
|
}
|
|
|
|
type FileProps = Omit<React.ComponentProps<'div'>, 'children'> & {
|
|
name: string;
|
|
sideComponent?: React.ReactNode;
|
|
};
|
|
|
|
function File({ name, className, sideComponent, ...props }: FileProps) {
|
|
return (
|
|
<FileButton
|
|
data-slot="file"
|
|
icon={<FileIcon />}
|
|
className={className}
|
|
sideComponent={sideComponent}
|
|
{...props}
|
|
>
|
|
{name}
|
|
</FileButton>
|
|
);
|
|
}
|
|
|
|
export {
|
|
Files,
|
|
Folder,
|
|
File,
|
|
type FilesProps,
|
|
type FolderProps,
|
|
type FileProps,
|
|
};
|