mirror of
https://github.com/langgenius/dify.git
synced 2026-07-27 23:18:33 +08:00
442 lines
15 KiB
TypeScript
442 lines
15 KiB
TypeScript
'use client'
|
|
|
|
import type { VariantProps } from 'class-variance-authority'
|
|
import type * as React from 'react'
|
|
import type { Placement } from '../placement'
|
|
import { Combobox as BaseCombobox } from '@base-ui/react/combobox'
|
|
import { cva } from 'class-variance-authority'
|
|
import { cn } from '../cn'
|
|
import { formLabelClassName, textControlCompoundFocusClassName } from '../form-control-shared'
|
|
import {
|
|
floatingGroupLabelClassName,
|
|
floatingItemIndicatorClassName,
|
|
floatingPopupAnimationClassName,
|
|
floatingSeparatorClassName,
|
|
} from '../overlay-shared'
|
|
import { parsePlacement } from '../placement'
|
|
|
|
export type { Placement }
|
|
|
|
export type ComboboxProps<
|
|
Value,
|
|
Multiple extends boolean | undefined = false,
|
|
> = BaseCombobox.Root.Props<Value, Multiple>
|
|
|
|
export function Combobox<Value, Multiple extends boolean | undefined = false>(
|
|
props: ComboboxProps<Value, Multiple>,
|
|
): React.JSX.Element {
|
|
return <BaseCombobox.Root {...props} />
|
|
}
|
|
|
|
export const ComboboxValue = BaseCombobox.Value
|
|
export const ComboboxGroup = BaseCombobox.Group
|
|
export const ComboboxCollection = BaseCombobox.Collection
|
|
export const ComboboxRow = BaseCombobox.Row
|
|
export const useComboboxFilter = BaseCombobox.useFilter
|
|
export const useComboboxFilteredItems = BaseCombobox.useFilteredItems
|
|
|
|
export type ComboboxChangeEventDetails = BaseCombobox.Root.ChangeEventDetails
|
|
export type ComboboxHighlightEventDetails = BaseCombobox.Root.HighlightEventDetails
|
|
|
|
const comboboxPopupClassName = [
|
|
'w-(--anchor-width) max-w-[min(28rem,var(--available-width))] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg outline-hidden',
|
|
]
|
|
|
|
const comboboxListClassName = [
|
|
'max-h-[min(20rem,var(--available-height))] overflow-y-auto overflow-x-hidden overscroll-contain p-1 outline-hidden scroll-py-1',
|
|
'data-empty:max-h-none data-empty:p-0',
|
|
]
|
|
|
|
const comboboxItemClassName = [
|
|
'grid min-h-8 cursor-pointer select-none grid-cols-[1fr_auto] items-center gap-2 rounded-lg px-2 py-1.5 text-text-secondary outline-hidden transition-colors',
|
|
'hover:bg-state-base-hover-alt hover:text-text-primary',
|
|
'data-highlighted:bg-state-base-hover data-highlighted:text-text-primary',
|
|
'data-selected:text-text-primary',
|
|
'data-disabled:cursor-not-allowed data-disabled:opacity-30 data-disabled:hover:bg-transparent data-disabled:hover:text-text-secondary',
|
|
'motion-reduce:transition-none',
|
|
]
|
|
|
|
const comboboxTriggerVariants = cva(
|
|
[
|
|
'group/combobox-trigger flex w-full min-w-0 items-center border-0 bg-components-input-bg-normal text-start text-components-input-text-filled outline-hidden transition-colors',
|
|
'hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-popup-open:bg-state-base-hover-alt',
|
|
'focus-visible:inset-ring-1 focus-visible:inset-ring-components-input-border-active',
|
|
'data-placeholder:text-components-input-text-placeholder',
|
|
'data-readonly:cursor-default data-readonly:bg-transparent data-readonly:hover:bg-transparent',
|
|
'data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled data-disabled:hover:bg-components-input-bg-disabled',
|
|
'data-disabled:data-placeholder:text-components-input-text-disabled',
|
|
'motion-reduce:transition-none',
|
|
],
|
|
{
|
|
variants: {
|
|
size: {
|
|
small: 'h-6 gap-px rounded-md px-2 py-1 system-xs-regular',
|
|
medium: 'h-8 gap-0.5 rounded-lg px-3 py-2 system-sm-regular',
|
|
large: 'h-9 gap-0.5 rounded-[10px] px-4 py-2 system-md-regular',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
size: 'medium',
|
|
},
|
|
},
|
|
)
|
|
|
|
export type ComboboxSize = NonNullable<VariantProps<typeof comboboxTriggerVariants>['size']>
|
|
|
|
type ComboboxTriggerProps = Omit<BaseCombobox.Trigger.Props, 'className'> &
|
|
VariantProps<typeof comboboxTriggerVariants> & {
|
|
className?: string
|
|
icon?: React.ReactNode | false
|
|
}
|
|
|
|
export function ComboboxTrigger({
|
|
className,
|
|
children,
|
|
icon,
|
|
size,
|
|
type = 'button',
|
|
...props
|
|
}: ComboboxTriggerProps) {
|
|
return (
|
|
<BaseCombobox.Trigger
|
|
type={type}
|
|
className={cn(comboboxTriggerVariants({ size, className }))}
|
|
{...props}
|
|
>
|
|
<span className="min-w-0 grow truncate">{children}</span>
|
|
{icon !== false && (
|
|
<BaseCombobox.Icon className="shrink-0 text-text-quaternary transition-colors group-hover/combobox-trigger:text-text-secondary group-data-popup-open/combobox-trigger:text-text-secondary group-data-readonly/combobox-trigger:hidden">
|
|
{icon ?? <span className="i-ri-arrow-down-s-line h-4 w-4" aria-hidden="true" />}
|
|
</BaseCombobox.Icon>
|
|
)}
|
|
</BaseCombobox.Trigger>
|
|
)
|
|
}
|
|
|
|
const comboboxInputGroupVariants = cva(
|
|
[
|
|
'group/combobox flex w-full min-w-0 items-center border border-transparent bg-components-input-bg-normal text-components-input-text-filled shadow-none outline-hidden transition-[background-color,border-color,box-shadow]',
|
|
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
|
textControlCompoundFocusClassName,
|
|
'data-focused:border-components-input-border-active data-focused:bg-components-input-bg-active data-focused:shadow-xs',
|
|
'data-popup-open:border-components-input-border-active data-popup-open:bg-components-input-bg-active',
|
|
'data-disabled:cursor-not-allowed data-disabled:border-transparent data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled',
|
|
'data-disabled:hover:border-transparent data-disabled:hover:bg-components-input-bg-disabled',
|
|
'data-readonly:shadow-none data-readonly:hover:border-transparent data-readonly:hover:bg-components-input-bg-normal',
|
|
'motion-reduce:transition-none',
|
|
],
|
|
{
|
|
variants: {
|
|
size: {
|
|
small: 'min-h-6 rounded-md',
|
|
medium: 'min-h-8 rounded-lg',
|
|
large: 'min-h-9 rounded-[10px]',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
size: 'medium',
|
|
},
|
|
},
|
|
)
|
|
|
|
export type ComboboxInputGroupProps = BaseCombobox.InputGroup.Props &
|
|
VariantProps<typeof comboboxInputGroupVariants>
|
|
|
|
export function ComboboxInputGroup({
|
|
className,
|
|
size = 'medium',
|
|
...props
|
|
}: ComboboxInputGroupProps) {
|
|
return (
|
|
<BaseCombobox.InputGroup
|
|
className={cn(comboboxInputGroupVariants({ size }), className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const comboboxInputVariants = cva(
|
|
[
|
|
'w-0 min-w-0 flex-1 appearance-none border-0 bg-transparent text-components-input-text-filled caret-primary-600 outline-hidden',
|
|
'placeholder:text-components-input-text-placeholder',
|
|
'disabled:cursor-not-allowed disabled:text-components-input-text-filled-disabled disabled:placeholder:text-components-input-text-disabled',
|
|
'data-readonly:cursor-default',
|
|
],
|
|
{
|
|
variants: {
|
|
size: {
|
|
small: 'px-2 py-1 system-xs-regular',
|
|
medium: 'px-3 py-[7px] system-sm-regular',
|
|
large: 'px-4 py-2 system-md-regular',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
size: 'medium',
|
|
},
|
|
},
|
|
)
|
|
|
|
export type ComboboxInputProps = Omit<BaseCombobox.Input.Props, 'size'> &
|
|
VariantProps<typeof comboboxInputVariants>
|
|
|
|
export function ComboboxInput({
|
|
className,
|
|
size = 'medium',
|
|
type = 'text',
|
|
autoComplete = 'off',
|
|
...props
|
|
}: ComboboxInputProps) {
|
|
return (
|
|
<BaseCombobox.Input
|
|
type={type}
|
|
autoComplete={autoComplete}
|
|
className={cn(comboboxInputVariants({ size }), className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const comboboxControlVariants = cva(
|
|
[
|
|
'flex shrink-0 touch-manipulation items-center justify-center rounded-md text-text-tertiary outline-hidden transition-colors',
|
|
'hover:bg-components-input-bg-hover hover:text-text-secondary focus-visible:bg-components-input-bg-hover focus-visible:text-text-secondary',
|
|
'focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
|
|
'disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-text-tertiary disabled:focus-visible:bg-transparent disabled:focus-visible:ring-0',
|
|
'group-data-disabled/combobox:cursor-not-allowed group-data-disabled/combobox:hover:bg-transparent group-data-disabled/combobox:focus-visible:bg-transparent group-data-disabled/combobox:focus-visible:ring-0',
|
|
'group-data-readonly/combobox:hidden',
|
|
'motion-reduce:transition-none',
|
|
],
|
|
{
|
|
variants: {
|
|
size: {
|
|
small: 'me-1 size-4',
|
|
medium: 'me-1.5 size-5',
|
|
large: 'me-2 size-5',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
size: 'medium',
|
|
},
|
|
},
|
|
)
|
|
|
|
export type ComboboxClearProps = Omit<BaseCombobox.Clear.Props, 'className'> &
|
|
VariantProps<typeof comboboxControlVariants> & { className?: string }
|
|
|
|
export function ComboboxClear({
|
|
className,
|
|
children,
|
|
size = 'medium',
|
|
type = 'button',
|
|
...props
|
|
}: ComboboxClearProps) {
|
|
return (
|
|
<BaseCombobox.Clear
|
|
type={type}
|
|
aria-label={props['aria-label'] ?? (props['aria-labelledby'] ? undefined : 'Clear combobox')}
|
|
className={cn(
|
|
comboboxControlVariants({ size }),
|
|
'data-ending-style:opacity-0 data-starting-style:opacity-0',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children ?? <span className="i-ri-close-line size-4" aria-hidden="true" />}
|
|
</BaseCombobox.Clear>
|
|
)
|
|
}
|
|
|
|
export type ComboboxInputTriggerProps = Omit<BaseCombobox.Trigger.Props, 'className'> &
|
|
VariantProps<typeof comboboxControlVariants> & { className?: string }
|
|
|
|
export function ComboboxInputTrigger({
|
|
className,
|
|
children,
|
|
size = 'medium',
|
|
type = 'button',
|
|
...props
|
|
}: ComboboxInputTriggerProps) {
|
|
return (
|
|
<BaseCombobox.Trigger
|
|
type={type}
|
|
aria-label={
|
|
props['aria-label'] ?? (props['aria-labelledby'] ? undefined : 'Open combobox options')
|
|
}
|
|
className={cn(comboboxControlVariants({ size }), className)}
|
|
{...props}
|
|
>
|
|
{children ?? <span className="i-ri-arrow-down-s-line size-4" aria-hidden="true" />}
|
|
</BaseCombobox.Trigger>
|
|
)
|
|
}
|
|
|
|
export function ComboboxIcon({ className, children, ...props }: BaseCombobox.Icon.Props) {
|
|
return (
|
|
<BaseCombobox.Icon
|
|
className={cn('flex shrink-0 items-center text-text-tertiary', className)}
|
|
{...props}
|
|
>
|
|
{children ?? <span className="i-ri-arrow-down-s-line size-4" aria-hidden="true" />}
|
|
</BaseCombobox.Icon>
|
|
)
|
|
}
|
|
|
|
type ComboboxContentProps = {
|
|
children: React.ReactNode
|
|
placement?: Placement
|
|
sideOffset?: number
|
|
alignOffset?: number
|
|
className?: string
|
|
popupClassName?: string
|
|
portalProps?: Omit<BaseCombobox.Portal.Props, 'children'>
|
|
positionerProps?: Omit<
|
|
BaseCombobox.Positioner.Props,
|
|
'children' | 'className' | 'side' | 'align' | 'sideOffset' | 'alignOffset'
|
|
>
|
|
popupProps?: Omit<BaseCombobox.Popup.Props, 'children' | 'className'>
|
|
}
|
|
|
|
export function ComboboxContent({
|
|
children,
|
|
placement = 'bottom-start',
|
|
sideOffset = 4,
|
|
alignOffset = 0,
|
|
className,
|
|
popupClassName,
|
|
portalProps,
|
|
positionerProps,
|
|
popupProps,
|
|
}: ComboboxContentProps) {
|
|
const { side, align } = parsePlacement(placement)
|
|
|
|
return (
|
|
<BaseCombobox.Portal {...portalProps}>
|
|
<BaseCombobox.Positioner
|
|
side={side}
|
|
align={align}
|
|
sideOffset={sideOffset}
|
|
alignOffset={alignOffset}
|
|
className={cn('z-50 outline-hidden', className)}
|
|
{...positionerProps}
|
|
>
|
|
<BaseCombobox.Popup
|
|
className={cn(comboboxPopupClassName, floatingPopupAnimationClassName, popupClassName)}
|
|
{...popupProps}
|
|
>
|
|
{children}
|
|
</BaseCombobox.Popup>
|
|
</BaseCombobox.Positioner>
|
|
</BaseCombobox.Portal>
|
|
)
|
|
}
|
|
|
|
export function ComboboxList({ className, ...props }: BaseCombobox.List.Props) {
|
|
return <BaseCombobox.List className={cn(comboboxListClassName, className)} {...props} />
|
|
}
|
|
|
|
export function ComboboxItem({ className, ...props }: BaseCombobox.Item.Props) {
|
|
return <BaseCombobox.Item className={cn(comboboxItemClassName, className)} {...props} />
|
|
}
|
|
|
|
export type ComboboxItemTextProps = React.ComponentProps<'span'>
|
|
|
|
export function ComboboxItemText({ className, ...props }: ComboboxItemTextProps) {
|
|
return (
|
|
<span className={cn('min-w-0 grow truncate px-1 system-sm-medium', className)} {...props} />
|
|
)
|
|
}
|
|
|
|
export function ComboboxItemIndicator({
|
|
className,
|
|
children,
|
|
...props
|
|
}: Omit<BaseCombobox.ItemIndicator.Props, 'children'> & { children?: React.ReactNode }) {
|
|
return (
|
|
<BaseCombobox.ItemIndicator
|
|
className={cn(floatingItemIndicatorClassName, className)}
|
|
{...props}
|
|
>
|
|
{children ?? <span className="i-ri-check-line h-4 w-4" aria-hidden="true" />}
|
|
</BaseCombobox.ItemIndicator>
|
|
)
|
|
}
|
|
|
|
export function ComboboxLabel({ className, ...props }: BaseCombobox.Label.Props) {
|
|
return <BaseCombobox.Label className={cn(formLabelClassName, className)} {...props} />
|
|
}
|
|
|
|
export function ComboboxGroupLabel({ className, ...props }: BaseCombobox.GroupLabel.Props) {
|
|
return (
|
|
<BaseCombobox.GroupLabel className={cn(floatingGroupLabelClassName, className)} {...props} />
|
|
)
|
|
}
|
|
|
|
export function ComboboxSeparator({ className, ...props }: BaseCombobox.Separator.Props) {
|
|
return <BaseCombobox.Separator className={cn(floatingSeparatorClassName, className)} {...props} />
|
|
}
|
|
|
|
export function ComboboxEmpty({ className, ...props }: BaseCombobox.Empty.Props) {
|
|
return (
|
|
<BaseCombobox.Empty
|
|
className={cn(
|
|
'px-3 py-2 system-sm-regular text-text-tertiary empty:h-0 empty:p-0',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function ComboboxStatus({ className, ...props }: BaseCombobox.Status.Props) {
|
|
return (
|
|
<BaseCombobox.Status
|
|
className={cn('px-3 py-2 system-sm-regular text-text-tertiary', className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function ComboboxChips({ className, ...props }: BaseCombobox.Chips.Props) {
|
|
return (
|
|
<BaseCombobox.Chips
|
|
className={cn('flex w-full min-w-0 flex-wrap items-center gap-1 px-1', className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function ComboboxChip({ className, ...props }: BaseCombobox.Chip.Props) {
|
|
return (
|
|
<BaseCombobox.Chip
|
|
className={cn(
|
|
'inline-flex max-w-full min-w-0 items-center gap-1 rounded-md bg-state-base-hover px-1.5 py-0.5 system-xs-medium text-text-secondary',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function ComboboxChipRemove({
|
|
className,
|
|
children,
|
|
type = 'button',
|
|
...props
|
|
}: BaseCombobox.ChipRemove.Props) {
|
|
return (
|
|
<BaseCombobox.ChipRemove
|
|
type={type}
|
|
aria-label={
|
|
props['aria-label'] ?? (props['aria-labelledby'] ? undefined : 'Remove selected item')
|
|
}
|
|
className={cn(
|
|
'flex size-3.5 shrink-0 items-center justify-center rounded-sm text-text-tertiary outline-hidden hover:bg-state-base-hover-alt hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children ?? <span className="i-ri-close-line size-3" aria-hidden="true" />}
|
|
</BaseCombobox.ChipRemove>
|
|
)
|
|
}
|