dify
This commit is contained in:
97
dify/web/app/components/base/input-number/index.spec.tsx
Normal file
97
dify/web/app/components/base/input-number/index.spec.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { InputNumber } from './index'
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('InputNumber Component', () => {
|
||||
const defaultProps = {
|
||||
onChange: jest.fn(),
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders input with default values', () => {
|
||||
render(<InputNumber {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles increment button click', () => {
|
||||
render(<InputNumber {...defaultProps} value={5} />)
|
||||
const incrementBtn = screen.getByRole('button', { name: /increment/i })
|
||||
|
||||
fireEvent.click(incrementBtn)
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith(6)
|
||||
})
|
||||
|
||||
it('handles decrement button click', () => {
|
||||
render(<InputNumber {...defaultProps} value={5} />)
|
||||
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
|
||||
|
||||
fireEvent.click(decrementBtn)
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith(4)
|
||||
})
|
||||
|
||||
it('respects max value constraint', () => {
|
||||
render(<InputNumber {...defaultProps} value={10} max={10} />)
|
||||
const incrementBtn = screen.getByRole('button', { name: /increment/i })
|
||||
|
||||
fireEvent.click(incrementBtn)
|
||||
expect(defaultProps.onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('respects min value constraint', () => {
|
||||
render(<InputNumber {...defaultProps} value={0} min={0} />)
|
||||
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
|
||||
|
||||
fireEvent.click(decrementBtn)
|
||||
expect(defaultProps.onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles direct input changes', () => {
|
||||
render(<InputNumber {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
fireEvent.change(input, { target: { value: '42' } })
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith(42)
|
||||
})
|
||||
|
||||
it('handles empty input', () => {
|
||||
render(<InputNumber {...defaultProps} value={1} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
it('handles invalid input', () => {
|
||||
render(<InputNumber {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
fireEvent.change(input, { target: { value: 'abc' } })
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
it('displays unit when provided', () => {
|
||||
const unit = 'px'
|
||||
render(<InputNumber {...defaultProps} unit={unit} />)
|
||||
expect(screen.getByText(unit)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('disables controls when disabled prop is true', () => {
|
||||
render(<InputNumber {...defaultProps} disabled />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
const incrementBtn = screen.getByRole('button', { name: /increment/i })
|
||||
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
|
||||
|
||||
expect(input).toBeDisabled()
|
||||
expect(incrementBtn).toBeDisabled()
|
||||
expect(decrementBtn).toBeDisabled()
|
||||
})
|
||||
})
|
||||
449
dify/web/app/components/base/input-number/index.stories.tsx
Normal file
449
dify/web/app/components/base/input-number/index.stories.tsx
Normal file
@@ -0,0 +1,449 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import { useState } from 'react'
|
||||
import { InputNumber } from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Data Entry/InputNumber',
|
||||
component: InputNumber,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Number input component with increment/decrement buttons. Supports min/max constraints, custom step amounts, and units display.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
value: {
|
||||
control: 'number',
|
||||
description: 'Current value',
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['regular', 'large'],
|
||||
description: 'Input size',
|
||||
},
|
||||
min: {
|
||||
control: 'number',
|
||||
description: 'Minimum value',
|
||||
},
|
||||
max: {
|
||||
control: 'number',
|
||||
description: 'Maximum value',
|
||||
},
|
||||
amount: {
|
||||
control: 'number',
|
||||
description: 'Step amount for increment/decrement',
|
||||
},
|
||||
unit: {
|
||||
control: 'text',
|
||||
description: 'Unit text displayed (e.g., "px", "ms")',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disabled state',
|
||||
},
|
||||
defaultValue: {
|
||||
control: 'number',
|
||||
description: 'Default value when undefined',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
onChange: (value) => {
|
||||
console.log('Value changed:', value)
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof InputNumber>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Interactive demo wrapper
|
||||
const InputNumberDemo = (args: any) => {
|
||||
const [value, setValue] = useState(args.value ?? 0)
|
||||
|
||||
return (
|
||||
<div style={{ width: '300px' }}>
|
||||
<InputNumber
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
setValue(newValue)
|
||||
console.log('Value changed:', newValue)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
Current value: <span className="font-semibold">{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Default state
|
||||
export const Default: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 0,
|
||||
size: 'regular',
|
||||
},
|
||||
}
|
||||
|
||||
// Large size
|
||||
export const LargeSize: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 10,
|
||||
size: 'large',
|
||||
},
|
||||
}
|
||||
|
||||
// With min/max constraints
|
||||
export const WithMinMax: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 5,
|
||||
min: 0,
|
||||
max: 10,
|
||||
size: 'regular',
|
||||
},
|
||||
}
|
||||
|
||||
// With custom step amount
|
||||
export const CustomStepAmount: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 50,
|
||||
amount: 5,
|
||||
min: 0,
|
||||
max: 100,
|
||||
size: 'regular',
|
||||
},
|
||||
}
|
||||
|
||||
// With unit
|
||||
export const WithUnit: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 100,
|
||||
unit: 'px',
|
||||
min: 0,
|
||||
max: 1000,
|
||||
amount: 10,
|
||||
size: 'regular',
|
||||
},
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
export const Disabled: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 42,
|
||||
disabled: true,
|
||||
size: 'regular',
|
||||
},
|
||||
}
|
||||
|
||||
// Decimal values
|
||||
export const DecimalValues: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 2.5,
|
||||
amount: 0.5,
|
||||
min: 0,
|
||||
max: 10,
|
||||
size: 'regular',
|
||||
},
|
||||
}
|
||||
|
||||
// Negative values allowed
|
||||
export const NegativeValues: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100,
|
||||
amount: 10,
|
||||
size: 'regular',
|
||||
},
|
||||
}
|
||||
|
||||
// Size comparison
|
||||
const SizeComparisonDemo = () => {
|
||||
const [regularValue, setRegularValue] = useState(10)
|
||||
const [largeValue, setLargeValue] = useState(20)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6" style={{ width: '300px' }}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Regular Size</label>
|
||||
<InputNumber
|
||||
size="regular"
|
||||
value={regularValue}
|
||||
onChange={setRegularValue}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Large Size</label>
|
||||
<InputNumber
|
||||
size="large"
|
||||
value={largeValue}
|
||||
onChange={setLargeValue}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SizeComparison: Story = {
|
||||
render: () => <SizeComparisonDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Font size picker
|
||||
const FontSizePickerDemo = () => {
|
||||
const [fontSize, setFontSize] = useState(16)
|
||||
|
||||
return (
|
||||
<div style={{ width: '350px' }} className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Font Size</label>
|
||||
<InputNumber
|
||||
value={fontSize}
|
||||
onChange={setFontSize}
|
||||
min={8}
|
||||
max={72}
|
||||
amount={2}
|
||||
unit="px"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg bg-gray-50 p-4">
|
||||
<p style={{ fontSize: `${fontSize}px` }} className="text-gray-900">
|
||||
Preview Text
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FontSizePicker: Story = {
|
||||
render: () => <FontSizePickerDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Quantity selector
|
||||
const QuantitySelectorDemo = () => {
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
const pricePerItem = 29.99
|
||||
const total = (quantity * pricePerItem).toFixed(2)
|
||||
|
||||
return (
|
||||
<div style={{ width: '350px' }} className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900">Product Name</h3>
|
||||
<p className="text-sm text-gray-500">${pricePerItem} each</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Quantity</label>
|
||||
<InputNumber
|
||||
value={quantity}
|
||||
onChange={setQuantity}
|
||||
min={1}
|
||||
max={99}
|
||||
amount={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">Total</span>
|
||||
<span className="text-lg font-semibold text-gray-900">${total}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const QuantitySelector: Story = {
|
||||
render: () => <QuantitySelectorDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Timer settings
|
||||
const TimerSettingsDemo = () => {
|
||||
const [hours, setHours] = useState(0)
|
||||
const [minutes, setMinutes] = useState(15)
|
||||
const [seconds, setSeconds] = useState(30)
|
||||
|
||||
const totalSeconds = hours * 3600 + minutes * 60 + seconds
|
||||
|
||||
return (
|
||||
<div style={{ width: '400px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">Timer Configuration</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Hours</label>
|
||||
<InputNumber
|
||||
value={hours}
|
||||
onChange={setHours}
|
||||
min={0}
|
||||
max={23}
|
||||
unit="h"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Minutes</label>
|
||||
<InputNumber
|
||||
value={minutes}
|
||||
onChange={setMinutes}
|
||||
min={0}
|
||||
max={59}
|
||||
unit="m"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Seconds</label>
|
||||
<InputNumber
|
||||
value={seconds}
|
||||
onChange={setSeconds}
|
||||
min={0}
|
||||
max={59}
|
||||
unit="s"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 rounded-lg bg-blue-50 p-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
Total duration: <span className="font-semibold">{totalSeconds} seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TimerSettings: Story = {
|
||||
render: () => <TimerSettingsDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Animation settings
|
||||
const AnimationSettingsDemo = () => {
|
||||
const [duration, setDuration] = useState(300)
|
||||
const [delay, setDelay] = useState(0)
|
||||
const [iterations, setIterations] = useState(1)
|
||||
|
||||
return (
|
||||
<div style={{ width: '400px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">Animation Properties</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Duration</label>
|
||||
<InputNumber
|
||||
value={duration}
|
||||
onChange={setDuration}
|
||||
min={0}
|
||||
max={5000}
|
||||
amount={50}
|
||||
unit="ms"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Delay</label>
|
||||
<InputNumber
|
||||
value={delay}
|
||||
onChange={setDelay}
|
||||
min={0}
|
||||
max={2000}
|
||||
amount={50}
|
||||
unit="ms"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Iterations</label>
|
||||
<InputNumber
|
||||
value={iterations}
|
||||
onChange={setIterations}
|
||||
min={1}
|
||||
max={10}
|
||||
amount={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 rounded-lg bg-gray-50 p-4">
|
||||
<div className="font-mono text-xs text-gray-700">
|
||||
animation: {duration}ms {delay}ms {iterations}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const AnimationSettings: Story = {
|
||||
render: () => <AnimationSettingsDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Temperature control
|
||||
const TemperatureControlDemo = () => {
|
||||
const [temperature, setTemperature] = useState(20)
|
||||
const fahrenheit = ((temperature * 9) / 5 + 32).toFixed(1)
|
||||
|
||||
return (
|
||||
<div style={{ width: '350px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">Temperature Control</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Set Temperature</label>
|
||||
<InputNumber
|
||||
size="large"
|
||||
value={temperature}
|
||||
onChange={setTemperature}
|
||||
min={16}
|
||||
max={30}
|
||||
amount={0.5}
|
||||
unit="°C"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 rounded-lg bg-gray-50 p-4">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">Celsius</div>
|
||||
<div className="text-2xl font-semibold text-gray-900">{temperature}°C</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">Fahrenheit</div>
|
||||
<div className="text-2xl font-semibold text-gray-900">{fahrenheit}°F</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TemperatureControl: Story = {
|
||||
render: () => <TemperatureControlDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Interactive playground
|
||||
export const Playground: Story = {
|
||||
render: args => <InputNumberDemo {...args} />,
|
||||
args: {
|
||||
value: 10,
|
||||
size: 'regular',
|
||||
min: 0,
|
||||
max: 100,
|
||||
amount: 1,
|
||||
unit: '',
|
||||
disabled: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
}
|
||||
132
dify/web/app/components/base/input-number/index.tsx
Normal file
132
dify/web/app/components/base/input-number/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { type FC, useCallback } from 'react'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
|
||||
import Input, { type InputProps } from '../input'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
export type InputNumberProps = {
|
||||
unit?: string
|
||||
value?: number
|
||||
onChange: (value: number) => void
|
||||
amount?: number
|
||||
size?: 'regular' | 'large'
|
||||
max?: number
|
||||
min?: number
|
||||
defaultValue?: number
|
||||
disabled?: boolean
|
||||
wrapClassName?: string
|
||||
controlWrapClassName?: string
|
||||
controlClassName?: string
|
||||
} & Omit<InputProps, 'value' | 'onChange' | 'size' | 'min' | 'max' | 'defaultValue'>
|
||||
|
||||
export const InputNumber: FC<InputNumberProps> = (props) => {
|
||||
const {
|
||||
unit,
|
||||
className,
|
||||
onChange,
|
||||
amount = 1,
|
||||
value,
|
||||
size = 'regular',
|
||||
max,
|
||||
min,
|
||||
defaultValue,
|
||||
wrapClassName,
|
||||
controlWrapClassName,
|
||||
controlClassName,
|
||||
disabled,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const isValidValue = useCallback((v: number) => {
|
||||
if (typeof max === 'number' && v > max)
|
||||
return false
|
||||
return !(typeof min === 'number' && v < min)
|
||||
}, [max, min])
|
||||
|
||||
const inc = () => {
|
||||
if (disabled) return
|
||||
|
||||
if (value === undefined) {
|
||||
onChange(defaultValue ?? 0)
|
||||
return
|
||||
}
|
||||
const newValue = value + amount
|
||||
if (!isValidValue(newValue))
|
||||
return
|
||||
onChange(newValue)
|
||||
}
|
||||
const dec = () => {
|
||||
if (disabled) return
|
||||
|
||||
if (value === undefined) {
|
||||
onChange(defaultValue ?? 0)
|
||||
return
|
||||
}
|
||||
const newValue = value - amount
|
||||
if (!isValidValue(newValue))
|
||||
return
|
||||
onChange(newValue)
|
||||
}
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value === '') {
|
||||
onChange(0)
|
||||
return
|
||||
}
|
||||
const parsed = Number(e.target.value)
|
||||
if (Number.isNaN(parsed))
|
||||
return
|
||||
|
||||
if (!isValidValue(parsed))
|
||||
return
|
||||
onChange(parsed)
|
||||
}, [isValidValue, onChange])
|
||||
|
||||
return <div className={classNames('flex', wrapClassName)}>
|
||||
<Input {...rest}
|
||||
// disable default controller
|
||||
type='number'
|
||||
className={classNames('no-spinner rounded-r-none', className)}
|
||||
value={value ?? 0}
|
||||
max={max}
|
||||
min={min}
|
||||
disabled={disabled}
|
||||
onChange={handleInputChange}
|
||||
unit={unit}
|
||||
size={size}
|
||||
/>
|
||||
<div className={classNames(
|
||||
'flex flex-col rounded-r-md border-l border-divider-subtle bg-components-input-bg-normal text-text-tertiary focus:shadow-xs',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
controlWrapClassName)}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
onClick={inc}
|
||||
disabled={disabled}
|
||||
aria-label='increment'
|
||||
className={classNames(
|
||||
size === 'regular' ? 'pt-1' : 'pt-1.5',
|
||||
'px-1.5 hover:bg-components-input-bg-hover',
|
||||
disabled && 'cursor-not-allowed hover:bg-transparent',
|
||||
controlClassName,
|
||||
)}
|
||||
>
|
||||
<RiArrowUpSLine className='size-3' />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
onClick={dec}
|
||||
disabled={disabled}
|
||||
aria-label='decrement'
|
||||
className={classNames(
|
||||
size === 'regular' ? 'pb-1' : 'pb-1.5',
|
||||
'px-1.5 hover:bg-components-input-bg-hover',
|
||||
disabled && 'cursor-not-allowed hover:bg-transparent',
|
||||
controlClassName,
|
||||
)}
|
||||
>
|
||||
<RiArrowDownSLine className='size-3' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
Reference in New Issue
Block a user