Files
dify/packages/dify-ui/src/file-tree/index.tsx
T

344 lines
9.7 KiB
TypeScript

'use client'
import { Collapsible as BaseCollapsible } from '@base-ui/react/collapsible'
import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import * as React from 'react'
import { cn } from '../cn'
import { resolveClassName } from '../internals/resolve-class-name'
const FileTreeLevelContext = React.createContext(1)
function useFileTreeLevel() {
return React.useContext(FileTreeLevelContext)
}
function getLabelText(children: React.ReactNode) {
return typeof children === 'string' || typeof children === 'number' ? String(children) : undefined
}
function renderGuides(level: number) {
return Array.from({ length: Math.max(level - 1, 0) }, (_, index) => <FileTreeGuide key={index} />)
}
type FileTreeFileState = {
selected: boolean
disabled: boolean
level: number
}
function fileTreeRowClassName({ className }: { className?: string }) {
return cn(
'group/file-tree-row relative flex h-6 w-full min-w-0 cursor-pointer items-center rounded-md ps-2 pe-1.5 text-start outline-hidden select-none',
'hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
'data-selected:bg-state-base-active',
'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:bg-transparent',
'aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:hover:bg-transparent',
className,
)
}
type FileTreeProps = useRender.ComponentProps<'section'>
function FileTree({ render, className, children, ...props }: FileTreeProps) {
const defaultProps: useRender.ElementProps<'section'> = {
className: cn('flex min-w-0 flex-col gap-px p-1', className),
children: <FileTreeLevelContext.Provider value={1}>{children}</FileTreeLevelContext.Provider>,
}
return useRender({
defaultTagName: 'section',
render,
props: mergeProps<'section'>(defaultProps, props),
})
}
type FileTreeListProps = useRender.ComponentProps<'ul'>
function FileTreeList({ render, className, ...props }: FileTreeListProps) {
const defaultProps: useRender.ElementProps<'ul'> = {
className: cn('m-0 flex min-w-0 list-none flex-col gap-px p-0', className),
}
return useRender({
defaultTagName: 'ul',
render,
props: mergeProps<'ul'>(defaultProps, props),
})
}
type FileTreeFolderProps = Omit<BaseCollapsible.Root.Props, 'render'> & {
render?: BaseCollapsible.Root.Props['render']
}
function FileTreeFolder({ render = <li />, className, ...props }: FileTreeFolderProps) {
return (
<BaseCollapsible.Root
render={render}
className={(state) => cn('min-w-0', resolveClassName(className, state))}
{...props}
/>
)
}
type FileTreeFolderTriggerProps = BaseCollapsible.Trigger.Props & {
level?: number
}
function FileTreeFolderTrigger({
className,
children,
disabled,
level: levelProp,
...props
}: FileTreeFolderTriggerProps) {
const contextLevel = useFileTreeLevel()
const level = levelProp ?? contextLevel
return (
<BaseCollapsible.Trigger
className={(state) => fileTreeRowClassName({ className: resolveClassName(className, state) })}
disabled={disabled}
data-disabled={disabled || undefined}
{...props}
>
{renderGuides(level)}
<div className="flex min-w-0 flex-[1_0_0] items-center py-0.5">{children}</div>
</BaseCollapsible.Trigger>
)
}
type FileTreeFolderPanelProps = Omit<BaseCollapsible.Panel.Props, 'render'> & {
render?: BaseCollapsible.Panel.Props['render']
}
function FileTreeFolderPanel({
render = <ul />,
className,
children,
...props
}: FileTreeFolderPanelProps) {
const level = useFileTreeLevel()
return (
<BaseCollapsible.Panel
render={render}
className={(state) =>
cn('m-0 flex min-w-0 list-none flex-col gap-px p-0', resolveClassName(className, state))
}
{...props}
>
<FileTreeLevelContext.Provider value={level + 1}>{children}</FileTreeLevelContext.Provider>
</BaseCollapsible.Panel>
)
}
type FileTreeFileProps = Omit<useRender.ComponentProps<'button', FileTreeFileState>, 'type'> & {
level?: number
selected?: boolean
}
function FileTreeFile({
render,
className,
children,
disabled = false,
level: levelProp,
selected = false,
...props
}: FileTreeFileProps) {
const contextLevel = useFileTreeLevel()
const level = levelProp ?? contextLevel
const state: FileTreeFileState = {
selected,
disabled,
level,
}
const defaultProps = {
type: 'button',
disabled,
'data-selected': selected || undefined,
'data-disabled': disabled || undefined,
'aria-current': selected ? 'true' : undefined,
className: fileTreeRowClassName({ className }),
children: (
<React.Fragment>
{renderGuides(level)}
<div className="flex min-w-0 flex-[1_0_0] items-center py-0.5">{children}</div>
</React.Fragment>
),
} as useRender.ElementProps<'button'>
const file = useRender({
defaultTagName: 'button',
render,
state,
props: mergeProps<'button'>(defaultProps, props),
})
return <li className="min-w-0">{file}</li>
}
type FileTreeGuideProps = useRender.ComponentProps<'span'>
function FileTreeGuide({ render, className, ...props }: FileTreeGuideProps) {
const defaultProps: useRender.ElementProps<'span'> = {
'aria-hidden': true,
className: cn(
'relative h-6 w-5 shrink-0 before:absolute before:top-0 before:-bottom-px before:left-1/2 before:w-px before:-translate-x-1/2 before:bg-divider-subtle',
className,
),
}
return useRender({
defaultTagName: 'span',
render,
props: mergeProps<'span'>(defaultProps, props),
})
}
type FileTreeIconType =
| 'folder'
| 'file'
| 'markdown'
| 'json'
| 'image'
| 'code'
| 'database'
| 'text'
| 'pdf'
| 'table'
| 'archive'
const fileTreeIconClassNames: Record<Exclude<FileTreeIconType, 'folder'>, string> = {
file: 'i-ri-file-3-fill text-[#A4AABF]',
markdown: 'i-ri-markdown-fill text-[#309BEC]',
json: 'i-ri-braces-fill text-[#A4AABF]',
image: 'i-ri-file-image-fill text-[#00B2EA]',
code: 'i-ri-file-code-fill text-[#A4AABF]',
database: 'i-ri-database-2-fill text-[#A4AABF]',
text: 'i-ri-file-text-fill text-[#6F8BB5]',
pdf: 'i-ri-file-pdf-2-fill text-[#EA3434]',
table: 'i-ri-file-excel-fill text-[#01AC49]',
archive: 'i-ri-file-zip-fill text-[#A4AABF]',
}
type FileTreeIconProps = Omit<useRender.ComponentProps<'span'>, 'children'> & {
type?: FileTreeIconType
children?: React.ReactNode
}
function FileTreeIcon({ type = 'file', render, className, children, ...props }: FileTreeIconProps) {
const defaultProps: useRender.ElementProps<'span'> = {
'aria-hidden': true,
className: cn(
'relative flex size-5 shrink-0 items-center justify-center text-text-secondary',
className,
),
children: (
<React.Fragment>
{children ??
(type === 'folder' ? (
<React.Fragment>
<span className="i-ri-folder-line size-4 group-data-panel-open/file-tree-row:hidden" />
<span className="i-ri-folder-open-line hidden size-4 text-text-secondary group-data-panel-open/file-tree-row:block" />
</React.Fragment>
) : (
<span className={cn('size-4', fileTreeIconClassNames[type])} />
))}
</React.Fragment>
),
}
return useRender({
defaultTagName: 'span',
render,
props: mergeProps<'span'>(defaultProps, props),
})
}
type FileTreeLabelProps = useRender.ComponentProps<'span'>
type FileTreeLabelElementProps = useRender.ElementProps<'span'> & {
'data-label'?: string
}
function FileTreeLabel({ render, className, children, ...props }: FileTreeLabelProps) {
const labelText = getLabelText(children)
const defaultProps = {
'data-label': labelText,
className: cn(
'w-0 min-w-0 flex-1 truncate rounded-[5px] px-1 py-0.5',
labelText &&
'after:invisible after:block after:h-0 after:overflow-hidden after:system-sm-medium after:content-[attr(data-label)]',
'system-sm-regular text-text-secondary group-data-selected/file-tree-row:system-sm-medium group-data-selected/file-tree-row:text-text-primary',
className,
),
children,
} satisfies FileTreeLabelElementProps
return useRender({
defaultTagName: 'span',
render,
props: mergeProps<'span'>(defaultProps, props),
})
}
type FileTreeMetaProps = useRender.ComponentProps<'span'>
function FileTreeMeta({ render, className, ...props }: FileTreeMetaProps) {
const defaultProps: useRender.ElementProps<'span'> = {
className: cn('min-w-0 shrink truncate system-xs-regular text-text-tertiary', className),
}
return useRender({
defaultTagName: 'span',
render,
props: mergeProps<'span'>(defaultProps, props),
})
}
type FileTreeBadgeProps = useRender.ComponentProps<'span'>
function FileTreeBadge({ render, className, ...props }: FileTreeBadgeProps) {
const defaultProps: useRender.ElementProps<'span'> = {
className: cn(
'ms-1 inline-flex min-w-4 shrink-0 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary',
className,
),
}
return useRender({
defaultTagName: 'span',
render,
props: mergeProps<'span'>(defaultProps, props),
})
}
export {
FileTree,
FileTreeBadge,
FileTreeFile,
FileTreeFolder,
FileTreeFolderPanel,
FileTreeFolderTrigger,
FileTreeGuide,
FileTreeIcon,
FileTreeLabel,
FileTreeList,
FileTreeMeta,
}
export type {
FileTreeBadgeProps,
FileTreeFileProps,
FileTreeFolderPanelProps,
FileTreeFolderProps,
FileTreeFolderTriggerProps,
FileTreeGuideProps,
FileTreeIconProps,
FileTreeIconType,
FileTreeLabelProps,
FileTreeListProps,
FileTreeMetaProps,
FileTreeProps,
}