mirror of
https://github.com/langgenius/dify.git
synced 2026-06-16 14:01:10 +08:00
refactor: normalize search input and dify-ui focus states (#37413)
This commit is contained in:
parent
fbfbbda245
commit
8eb6a19784
@ -2579,11 +2579,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/base/search-input/index.tsx": {
|
||||
"jsx-a11y/no-autofocus": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/svg-gallery/index.tsx": {
|
||||
"node/prefer-global/buffer": {
|
||||
"count": 1
|
||||
@ -3511,11 +3506,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/list/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/metadata/edit-metadata-batch/add-row.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
|
||||
@ -6,6 +6,7 @@ import type { Placement } from '../placement'
|
||||
import { Autocomplete as BaseAutocomplete } from '@base-ui/react/autocomplete'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '../cn'
|
||||
import { textControlCompoundFocusClassName } from '../form-control-shared'
|
||||
import {
|
||||
overlayIndicatorClassName,
|
||||
overlayLabelClassName,
|
||||
@ -49,7 +50,7 @@ const autocompleteInputGroupVariants = cva(
|
||||
[
|
||||
'group/autocomplete 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',
|
||||
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
|
||||
textControlCompoundFocusClassName,
|
||||
'data-focused:border-components-input-border-active data-focused:bg-components-input-bg-active data-focused:shadow-xs',
|
||||
'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',
|
||||
|
||||
@ -6,7 +6,7 @@ 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 } from '../form-control-shared'
|
||||
import { formLabelClassName, textControlCompoundFocusClassName } from '../form-control-shared'
|
||||
import {
|
||||
overlayIndicatorClassName,
|
||||
overlayLabelClassName,
|
||||
@ -113,7 +113,7 @@ 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',
|
||||
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
|
||||
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',
|
||||
|
||||
@ -2,12 +2,16 @@ import { cva } from 'class-variance-authority'
|
||||
|
||||
export const formLabelClassName = 'w-fit py-1 text-text-secondary system-sm-medium data-disabled:cursor-not-allowed'
|
||||
|
||||
export const textControlFocusClassName = 'focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
|
||||
|
||||
export const textControlCompoundFocusClassName = 'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs'
|
||||
|
||||
export const textControlVariants = cva(
|
||||
[
|
||||
'w-full appearance-none border border-transparent bg-components-input-bg-normal text-components-input-text-filled caret-primary-600 outline-hidden transition-[background-color,border-color,box-shadow]',
|
||||
'placeholder:text-components-input-text-placeholder',
|
||||
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
|
||||
textControlFocusClassName,
|
||||
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
|
||||
'read-only:cursor-default read-only:shadow-none read-only:hover:border-transparent read-only:hover:bg-components-input-bg-normal read-only:focus:border-transparent read-only:focus:bg-components-input-bg-normal read-only:focus:shadow-none',
|
||||
'disabled:cursor-not-allowed disabled:border-transparent disabled:bg-components-input-bg-disabled disabled:text-components-input-text-filled-disabled',
|
||||
|
||||
@ -7,6 +7,10 @@ import type {
|
||||
} from '../index'
|
||||
import * as React from 'react'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import {
|
||||
FieldLabel,
|
||||
FieldRoot,
|
||||
} from '../../field'
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldControls,
|
||||
@ -44,11 +48,7 @@ const renderNumberField = ({
|
||||
return render(
|
||||
<NumberField defaultValue={defaultValue}>
|
||||
<NumberFieldGroup data-testid="group" {...groupProps}>
|
||||
<NumberFieldInput
|
||||
aria-label="Amount"
|
||||
data-testid="input"
|
||||
{...inputProps}
|
||||
/>
|
||||
<NumberFieldInput aria-label="Amount" {...inputProps} />
|
||||
{unitProps && (
|
||||
<NumberFieldUnit data-testid="unit" {...restUnitProps}>
|
||||
{unitChildren}
|
||||
@ -75,6 +75,7 @@ describe('NumberField wrapper', () => {
|
||||
})
|
||||
|
||||
await expect.element(screen.getByTestId('group')).toHaveClass('rounded-lg')
|
||||
await expect.element(screen.getByTestId('group')).toHaveClass('focus-within:border-components-input-border-active')
|
||||
await expect.element(screen.getByTestId('group')).toHaveClass('custom-group')
|
||||
})
|
||||
|
||||
@ -89,8 +90,25 @@ describe('NumberField wrapper', () => {
|
||||
})
|
||||
|
||||
await expect.element(screen.getByTestId('group')).toHaveClass('rounded-[10px]')
|
||||
await expect.element(screen.getByTestId('input')).toHaveClass('px-4')
|
||||
await expect.element(screen.getByTestId('input')).toHaveClass('py-2')
|
||||
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('px-4')
|
||||
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveClass('py-2')
|
||||
})
|
||||
|
||||
it('should surface field invalid state on the visual group', async () => {
|
||||
const screen = await render(
|
||||
<FieldRoot name="amount" invalid>
|
||||
<FieldLabel>Amount</FieldLabel>
|
||||
<NumberField defaultValue={8}>
|
||||
<NumberFieldGroup data-testid="group">
|
||||
<NumberFieldInput />
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</FieldRoot>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByTestId('group')).toHaveAttribute('data-invalid')
|
||||
await expect.element(screen.getByTestId('group')).toHaveClass('data-invalid:border-components-input-border-destructive')
|
||||
await expect.element(screen.getByRole('textbox', { name: 'Amount' })).toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
|
||||
it('should set input defaults and forward passthrough props', async () => {
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { Button } from '../button'
|
||||
import {
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
FieldRoot,
|
||||
} from '../field'
|
||||
import { Form } from '../form'
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldControls,
|
||||
@ -8,104 +16,7 @@ import {
|
||||
NumberFieldIncrement,
|
||||
NumberFieldInput,
|
||||
NumberFieldUnit,
|
||||
} from '.'
|
||||
import { cn } from '../cn'
|
||||
|
||||
type DemoFieldProps = {
|
||||
label: string
|
||||
helperText: string
|
||||
placeholder: string
|
||||
size: 'medium' | 'large'
|
||||
unit?: string
|
||||
defaultValue?: number | null
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
disabled?: boolean
|
||||
readOnly?: boolean
|
||||
showCurrentValue?: boolean
|
||||
widthClassName?: string
|
||||
formatValue?: (value: number | null) => string
|
||||
}
|
||||
|
||||
const formatNumericValue = (value: number | null, unit?: string) => {
|
||||
if (value === null)
|
||||
return 'Empty'
|
||||
|
||||
if (!unit)
|
||||
return String(value)
|
||||
|
||||
return `${value} ${unit}`
|
||||
}
|
||||
|
||||
const FieldLabel = ({
|
||||
inputId,
|
||||
label,
|
||||
helperText,
|
||||
}: Pick<DemoFieldProps, 'label' | 'helperText'> & { inputId: string }) => (
|
||||
<div className="space-y-1">
|
||||
<label htmlFor={inputId} className="system-sm-medium text-text-secondary">
|
||||
{label}
|
||||
</label>
|
||||
<p className="system-xs-regular text-text-tertiary">{helperText}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
const DemoField = ({
|
||||
label,
|
||||
helperText,
|
||||
placeholder,
|
||||
size,
|
||||
unit,
|
||||
defaultValue,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
disabled,
|
||||
readOnly,
|
||||
showCurrentValue,
|
||||
widthClassName,
|
||||
formatValue,
|
||||
}: DemoFieldProps) => {
|
||||
const inputId = React.useId()
|
||||
const [value, setValue] = React.useState<number | null>(defaultValue ?? null)
|
||||
|
||||
return (
|
||||
<div className={cn('flex w-full max-w-80 flex-col gap-2', widthClassName)}>
|
||||
<FieldLabel inputId={inputId} label={label} helperText={helperText} />
|
||||
<NumberField
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
onValueChange={setValue}
|
||||
>
|
||||
<NumberFieldGroup size={size}>
|
||||
<NumberFieldInput
|
||||
id={inputId}
|
||||
aria-label={label}
|
||||
placeholder={placeholder}
|
||||
size={size}
|
||||
/>
|
||||
{unit && <NumberFieldUnit size={size}>{unit}</NumberFieldUnit>}
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement size={size} />
|
||||
<NumberFieldDecrement size={size} />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
{showCurrentValue && (
|
||||
<p className="system-xs-regular text-text-quaternary">
|
||||
Current value:
|
||||
{' '}
|
||||
{formatValue ? formatValue(value) : formatNumericValue(value, unit)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
} from './index'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Form/NumberField',
|
||||
@ -114,7 +25,7 @@ const meta = {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compound numeric input built on Base UI NumberField. Stories explicitly enumerate the shipped CVA variants, then cover realistic numeric-entry cases such as decimals, empty values, range limits, read-only, and disabled states.',
|
||||
component: 'Compound numeric input built on Base UI NumberField. Use it with FieldRoot for labelled, described, and validated form fields.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -122,164 +33,312 @@ const meta = {
|
||||
} satisfies Meta<typeof NumberField>
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const VariantMatrix: Story = {
|
||||
type NumberFieldExampleProps = {
|
||||
id: string
|
||||
label: string
|
||||
name: string
|
||||
defaultValue?: number
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number | 'any'
|
||||
placeholder?: string
|
||||
unit?: string
|
||||
size?: 'medium' | 'large'
|
||||
disabled?: boolean
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
function NumberFieldExample({
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
defaultValue,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
placeholder,
|
||||
unit,
|
||||
size = 'medium',
|
||||
disabled,
|
||||
readOnly,
|
||||
}: NumberFieldExampleProps) {
|
||||
return (
|
||||
<div className="grid w-80 gap-1">
|
||||
<label htmlFor={id} className="text-text-secondary system-sm-medium">
|
||||
{label}
|
||||
</label>
|
||||
<NumberField
|
||||
id={id}
|
||||
name={name}
|
||||
defaultValue={defaultValue}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
>
|
||||
<NumberFieldGroup size={size}>
|
||||
<NumberFieldInput placeholder={placeholder} size={size} />
|
||||
{unit && <NumberFieldUnit size={size}>{unit}</NumberFieldUnit>}
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement size={size} />
|
||||
<NumberFieldDecrement size={size} />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-6 md:grid-cols-2">
|
||||
<DemoField
|
||||
label="Top K"
|
||||
helperText="Regular size without suffix. Covers the regular group, input, and control button spacing."
|
||||
placeholder="Set top K"
|
||||
size="medium"
|
||||
defaultValue={3}
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
/>
|
||||
<DemoField
|
||||
label="Score threshold"
|
||||
helperText="Regular size with a suffix so the regular unit variant is visible."
|
||||
placeholder="Set threshold"
|
||||
size="medium"
|
||||
unit="%"
|
||||
defaultValue={85}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
<DemoField
|
||||
label="Chunk overlap"
|
||||
helperText="Large size without suffix. Matches the larger dataset form treatment."
|
||||
placeholder="Set overlap"
|
||||
size="large"
|
||||
<NumberFieldExample
|
||||
id="top-k-basic"
|
||||
name="topK"
|
||||
label="Top K"
|
||||
defaultValue={3}
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
placeholder="Set top K…"
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-80 gap-3">
|
||||
<NumberFieldExample
|
||||
id="number-field-medium"
|
||||
name="mediumNumber"
|
||||
label="Medium"
|
||||
defaultValue={64}
|
||||
min={0}
|
||||
max={512}
|
||||
step={16}
|
||||
placeholder="Set value…"
|
||||
/>
|
||||
<DemoField
|
||||
label="Max segment length"
|
||||
helperText="Large size with suffix so the large unit variant is also enumerated."
|
||||
placeholder="Set length"
|
||||
size="large"
|
||||
unit="tokens"
|
||||
<NumberFieldExample
|
||||
id="number-field-large"
|
||||
name="largeNumber"
|
||||
label="Large with unit"
|
||||
defaultValue={512}
|
||||
min={1}
|
||||
max={4000}
|
||||
step={32}
|
||||
placeholder="Set length…"
|
||||
unit="tokens"
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const DecimalInputs: Story = {
|
||||
export const States: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-6 md:grid-cols-2">
|
||||
<DemoField
|
||||
label="Score threshold"
|
||||
helperText="Two-decimal precision with a 0.01 step, like retrieval tuning fields."
|
||||
placeholder="0.00"
|
||||
size="medium"
|
||||
defaultValue={0.82}
|
||||
<div className="grid w-80 gap-3">
|
||||
<FieldRoot name="placeholderState">
|
||||
<FieldLabel>Placeholder</FieldLabel>
|
||||
<NumberField min={0} max={100}>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput placeholder="Set threshold…" />
|
||||
<NumberFieldUnit>%</NumberFieldUnit>
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="filledState">
|
||||
<FieldLabel>Filled</FieldLabel>
|
||||
<NumberField defaultValue={85} min={0} max={100}>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldUnit>%</NumberFieldUnit>
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="invalidState" invalid>
|
||||
<FieldLabel>Invalid</FieldLabel>
|
||||
<NumberField defaultValue={120} min={0} max={100}>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldUnit>%</NumberFieldUnit>
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
<FieldError match>Use a value from 0 to 100.</FieldError>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="disabledState">
|
||||
<FieldLabel>Disabled</FieldLabel>
|
||||
<NumberField defaultValue={5} min={0} max={10} disabled>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="readonlyState">
|
||||
<FieldLabel>Read-only</FieldLabel>
|
||||
<NumberField defaultValue={92} min={0} max={100} readOnly>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldUnit>%</NumberFieldUnit>
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</FieldRoot>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
function ControlledDemo() {
|
||||
const [value, setValue] = React.useState<number | null>(0.82)
|
||||
|
||||
return (
|
||||
<FieldRoot name="controlledThreshold">
|
||||
<FieldLabel>Score threshold</FieldLabel>
|
||||
<NumberField
|
||||
value={value}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
showCurrentValue
|
||||
formatValue={value => value === null ? 'Empty' : value.toFixed(2)}
|
||||
/>
|
||||
<DemoField
|
||||
label="Temperature"
|
||||
helperText="One-decimal stepping for generation parameters."
|
||||
placeholder="0.0"
|
||||
size="large"
|
||||
defaultValue={0.7}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
showCurrentValue
|
||||
formatValue={value => value === null ? 'Empty' : value.toFixed(1)}
|
||||
/>
|
||||
<DemoField
|
||||
label="Penalty"
|
||||
helperText="Starts empty so the placeholder and empty numeric state are both visible."
|
||||
placeholder="Optional"
|
||||
size="medium"
|
||||
defaultValue={null}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.05}
|
||||
showCurrentValue
|
||||
formatValue={value => value === null ? 'Empty' : value.toFixed(2)}
|
||||
/>
|
||||
<DemoField
|
||||
label="Latency budget"
|
||||
helperText="Decimal input with a unit suffix and larger spacing."
|
||||
placeholder="0.0"
|
||||
size="large"
|
||||
unit="s"
|
||||
defaultValue={1.5}
|
||||
min={0.5}
|
||||
max={10}
|
||||
step={0.5}
|
||||
showCurrentValue
|
||||
formatValue={value => value === null ? 'Empty' : `${value.toFixed(1)} s`}
|
||||
/>
|
||||
format={{
|
||||
maximumFractionDigits: 2,
|
||||
}}
|
||||
onValueChange={setValue}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput placeholder="0.00" />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
<FieldDescription>
|
||||
Current value:
|
||||
{' '}
|
||||
{value === null ? 'Empty' : value.toFixed(2)}
|
||||
</FieldDescription>
|
||||
</FieldRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => (
|
||||
<div className="w-80">
|
||||
<ControlledDemo />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const BoundariesAndStates: Story = {
|
||||
function FormDemo() {
|
||||
const [savedValue, setSavedValue] = React.useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<Form
|
||||
aria-label="Retrieval settings"
|
||||
className="grid w-80 gap-4"
|
||||
onFormSubmit={(values) => {
|
||||
setSavedValue(String(values.topK ?? ''))
|
||||
}}
|
||||
>
|
||||
<FieldRoot name="topK">
|
||||
<FieldLabel>Top K</FieldLabel>
|
||||
<NumberField required defaultValue={3} min={1} max={10} step={1}>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
<FieldDescription>Choose how many chunks are returned.</FieldDescription>
|
||||
<FieldError match="valueMissing">Top K is required.</FieldError>
|
||||
<FieldError match="rangeUnderflow">Use at least 1.</FieldError>
|
||||
<FieldError match="rangeOverflow">Use 10 or fewer.</FieldError>
|
||||
</FieldRoot>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" variant="primary">Save Settings</Button>
|
||||
</div>
|
||||
{savedValue && (
|
||||
<div className="rounded-lg bg-background-section px-3 py-2 text-text-secondary system-xs-regular">
|
||||
Saved:
|
||||
{' '}
|
||||
{savedValue}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithField: Story = {
|
||||
render: () => <FormDemo />,
|
||||
}
|
||||
|
||||
export const Formatting: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-6 md:grid-cols-2">
|
||||
<DemoField
|
||||
label="HTTP status code"
|
||||
helperText="Integer-only style usage with tighter bounds from 100 to 599."
|
||||
placeholder="200"
|
||||
size="medium"
|
||||
defaultValue={200}
|
||||
min={100}
|
||||
max={599}
|
||||
step={1}
|
||||
showCurrentValue
|
||||
/>
|
||||
<DemoField
|
||||
label="Request timeout"
|
||||
helperText="Bounded regular input with suffix, common in system settings."
|
||||
placeholder="Set timeout"
|
||||
size="medium"
|
||||
unit="ms"
|
||||
defaultValue={1200}
|
||||
min={100}
|
||||
max={10000}
|
||||
step={100}
|
||||
showCurrentValue
|
||||
/>
|
||||
<DemoField
|
||||
label="Retry count"
|
||||
helperText="Disabled state preserves the layout while switching to disabled tokens."
|
||||
placeholder="Retry count"
|
||||
size="large"
|
||||
defaultValue={5}
|
||||
min={0}
|
||||
max={10}
|
||||
step={1}
|
||||
disabled
|
||||
showCurrentValue
|
||||
/>
|
||||
<DemoField
|
||||
label="Archived score threshold"
|
||||
helperText="Read-only state keeps the same structure but removes interactive affordances."
|
||||
placeholder="0.00"
|
||||
size="large"
|
||||
unit="%"
|
||||
defaultValue={92}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
readOnly
|
||||
showCurrentValue
|
||||
/>
|
||||
<div className="grid w-80 gap-3">
|
||||
<FieldRoot name="currencyBudget">
|
||||
<FieldLabel>Budget</FieldLabel>
|
||||
<NumberField
|
||||
defaultValue={1200}
|
||||
min={0}
|
||||
step={100}
|
||||
format={{
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
}}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="temperature">
|
||||
<FieldLabel>Temperature</FieldLabel>
|
||||
<NumberField
|
||||
defaultValue={0.7}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
format={{
|
||||
maximumFractionDigits: 1,
|
||||
}}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</FieldRoot>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import type * as React from 'react'
|
||||
import { NumberField as BaseNumberField } from '@base-ui/react/number-field'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '../cn'
|
||||
import { textControlCompoundFocusClassName } from '../form-control-shared'
|
||||
|
||||
export const NumberField = BaseNumberField.Root
|
||||
export type NumberFieldRootProps = BaseNumberField.Root.Props
|
||||
@ -13,7 +14,9 @@ export const numberFieldGroupVariants = cva(
|
||||
[
|
||||
'group/number-field flex w-full min-w-0 items-stretch overflow-hidden 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-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
|
||||
'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',
|
||||
@ -32,7 +35,12 @@ export const numberFieldGroupVariants = cva(
|
||||
)
|
||||
export type NumberFieldSize = NonNullable<VariantProps<typeof numberFieldGroupVariants>['size']>
|
||||
|
||||
export type NumberFieldGroupProps = BaseNumberField.Group.Props & VariantProps<typeof numberFieldGroupVariants>
|
||||
export type NumberFieldGroupProps
|
||||
= Omit<BaseNumberField.Group.Props, 'className'>
|
||||
& VariantProps<typeof numberFieldGroupVariants>
|
||||
& {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function NumberFieldGroup({
|
||||
className,
|
||||
@ -67,7 +75,12 @@ export const numberFieldInputVariants = cva(
|
||||
},
|
||||
)
|
||||
|
||||
export type NumberFieldInputProps = Omit<BaseNumberField.Input.Props, 'size'> & VariantProps<typeof numberFieldInputVariants>
|
||||
export type NumberFieldInputProps
|
||||
= Omit<BaseNumberField.Input.Props, 'className' | 'size'>
|
||||
& VariantProps<typeof numberFieldInputVariants>
|
||||
& {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function NumberFieldInput({
|
||||
className,
|
||||
@ -185,7 +198,12 @@ type NumberFieldButtonVariantProps = Omit<
|
||||
'direction'
|
||||
>
|
||||
|
||||
export type NumberFieldButtonProps = BaseNumberField.Increment.Props & NumberFieldButtonVariantProps
|
||||
export type NumberFieldButtonProps
|
||||
= Omit<BaseNumberField.Increment.Props, 'className'>
|
||||
& NumberFieldButtonVariantProps
|
||||
& {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const incrementAriaLabel = 'Increment value'
|
||||
const decrementAriaLabel = 'Decrement value'
|
||||
|
||||
@ -6,13 +6,14 @@ import type * as React from 'react'
|
||||
import { Field as BaseField } from '@base-ui/react/field'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '../cn'
|
||||
import { textControlFocusClassName } from '../form-control-shared'
|
||||
|
||||
const textareaVariants = cva(
|
||||
[
|
||||
'min-h-20 w-full appearance-none overflow-auto border border-transparent bg-components-input-bg-normal text-components-input-text-filled caret-primary-600 outline-hidden transition-[background-color,border-color,box-shadow]',
|
||||
'placeholder:text-components-input-text-placeholder',
|
||||
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
|
||||
textControlFocusClassName,
|
||||
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
|
||||
'read-only:cursor-default read-only:shadow-none read-only:hover:border-transparent read-only:hover:bg-components-input-bg-normal read-only:focus:border-transparent read-only:focus:bg-components-input-bg-normal read-only:focus:shadow-none',
|
||||
'disabled:cursor-not-allowed disabled:border-transparent disabled:bg-components-input-bg-disabled disabled:text-components-input-text-filled-disabled',
|
||||
|
||||
@ -217,16 +217,6 @@ function List({
|
||||
placeholder={t('operation.search', { ns: 'common' })}
|
||||
aria-label={t('gotoAnything.actions.searchApplications', { ns: 'app' })}
|
||||
/>
|
||||
{!!keywords && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
className="absolute top-1/2 right-2 flex size-4 -translate-y-1/2 items-center justify-center text-components-input-text-placeholder hover:text-components-input-text-filled"
|
||||
onClick={() => setKeywords('')}
|
||||
>
|
||||
<span aria-hidden className="i-ri-close-circle-fill size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
|
||||
@ -71,7 +71,7 @@ export function Infotip({
|
||||
aria-label={ariaLabel}
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -35,6 +35,23 @@ describe('SearchInput', () => {
|
||||
const clearButton = screen.getByLabelText('common.operation.clear')
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses the design-system focus treatment for the clear button', () => {
|
||||
render(<SearchInput value="has value" onValueChange={() => {}} />)
|
||||
|
||||
const clearButton = screen.getByRole('button', { name: 'common.operation.clear' })
|
||||
expect(clearButton).toHaveClass(
|
||||
'right-1.5',
|
||||
'size-5',
|
||||
'focus-visible:bg-components-input-bg-hover',
|
||||
'focus-visible:ring-2',
|
||||
'focus-visible:ring-state-accent-solid',
|
||||
'focus-visible:ring-inset',
|
||||
)
|
||||
expect(clearButton).not.toHaveClass('size-4')
|
||||
expect(clearButton).not.toHaveClass('focus-visible:ring-1')
|
||||
expect(clearButton).not.toHaveClass('focus-visible:ring-components-input-border-active')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interaction', () => {
|
||||
|
||||
@ -84,6 +84,7 @@ export function SearchInput({
|
||||
onValueChange(e.currentTarget.value)
|
||||
}}
|
||||
autoComplete="off"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autoFocus}
|
||||
enterKeyHint="search"
|
||||
/>
|
||||
@ -91,7 +92,7 @@ export function SearchInput({
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
className="group/clear absolute top-1/2 right-2 flex size-4 -translate-y-1/2 cursor-pointer touch-manipulation items-center justify-center rounded-md border-none bg-transparent p-0 outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset"
|
||||
className="group/clear absolute top-1/2 right-1.5 flex size-5 -translate-y-1/2 cursor-pointer touch-manipulation items-center justify-center rounded-md border-none bg-transparent p-0 outline-hidden focus-visible:bg-components-input-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<span className="i-ri-close-circle-fill size-4 text-text-quaternary group-hover/clear:text-text-tertiary" aria-hidden="true" />
|
||||
|
||||
@ -146,7 +146,7 @@ describe('List', () => {
|
||||
|
||||
it('should render the search input', () => {
|
||||
render(<List />)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
expect(screen.getByRole('searchbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tag filter', () => {
|
||||
@ -195,7 +195,7 @@ describe('List', () => {
|
||||
it('should update search input value', () => {
|
||||
render(<List />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('searchbox')
|
||||
fireEvent.change(input, { target: { value: 'test search' } })
|
||||
|
||||
expect(input).toHaveValue('test search')
|
||||
@ -259,7 +259,7 @@ describe('List', () => {
|
||||
it('should clear search input when onClear is called', () => {
|
||||
render(<List />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('searchbox')
|
||||
// First set a value
|
||||
fireEvent.change(input, { target: { value: 'test search' } })
|
||||
expect(input).toHaveValue('test search')
|
||||
|
||||
@ -7,7 +7,7 @@ import { useBoolean, useDebounceFn } from 'ahooks'
|
||||
// Libraries
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SearchInput } from '@/app/components/base/search-input'
|
||||
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
||||
import { useAppContext, useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { useExternalApiPanel } from '@/context/external-api-panel-context'
|
||||
@ -69,13 +69,10 @@ const List = () => {
|
||||
/>
|
||||
)}
|
||||
<TagFilter type="knowledge" value={tagFilterValue} onChange={handleTagsChange} onOpenTagManagement={() => setShowTagManagementModal(true)} />
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName="w-[200px]"
|
||||
<SearchInput
|
||||
className="w-50"
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
onValueChange={handleKeywordsChange}
|
||||
/>
|
||||
{
|
||||
isCurrentWorkspaceManager && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user