refactor(dify-ui): consolidate radio family API (#38479)

This commit is contained in:
yyh
2026-07-06 20:27:49 +08:00
committed by GitHub
parent c47663d77b
commit bdb3469ca0
37 changed files with 394 additions and 648 deletions
+20 -15
View File
@@ -43,18 +43,18 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
## Primitives
| Category | Subpath | Notes |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. |
| Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. |
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
| Navigation | `./file-tree`, `./pagination`, `./tabs` | FileTree for preview-oriented file disclosure lists; Pagination for page navigation; Tabs for panels. |
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
| Category | Subpath | Notes |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. |
| Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. |
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
| Navigation | `./file-tree`, `./pagination`, `./tabs` | FileTree for preview-oriented file disclosure lists; Pagination for page navigation; Tabs for panels. |
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
Utilities:
@@ -103,17 +103,22 @@ Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group
## Typed value contracts
Selection primitives should preserve the caller's domain value type instead of widening values to `string`. Use `Select<Value, Multiple>`, `RadioGroup<Value>`, `Radio<Value>`, and `RadioRoot<Value>` when the selected value is an enum, union, boolean, number, object, or nullable placeholder value.
Selection primitives should preserve the caller's domain value type instead of widening values to `string`. Use `Select<Value, Multiple>`, `RadioGroup<Value>`, `Radio<Value>`, and `RadioItem<Value>` when the selected value is an enum, union, boolean, number, object, or nullable placeholder value.
Root-level generics type `value`, `defaultValue`, `onValueChange`, and collection props such as `items`, but JSX children do not automatically inherit the parent generic. For non-string radio groups, type the child item too:
```tsx
<RadioGroup<PromptMode> value={promptMode} onValueChange={setPromptMode}>
<RadioRoot<PromptMode> value={PROMPT_MODE.default} />
<RadioRoot<PromptMode> value={PROMPT_MODE.custom} />
<Radio<PromptMode> value={PROMPT_MODE.default} />
<RadioItem<PromptMode> value={PROMPT_MODE.custom}>
<RadioControl />
Custom prompt
</RadioItem>
</RadioGroup>
```
Use `Radio` for the default Dify control. Use `RadioItem` when custom UI should be the radio item; place `RadioControl` inside it for the standard visual dot. `RadioControl` is a Dify visual part, not a Base UI anatomy export.
For select labels and display values, prefer the Base UI `items` collection pattern so the root, value display, and item list share the same typed values. Avoid helpers that stringify values only to recover labels later; convert values to strings only at real boundaries such as form submission, URL/search params, or legacy APIs that require strings.
`CheckboxGroup` follows the Base UI contract and uses `string[]`. Do not add a generic checkbox-group wrapper unless the underlying primitive contract changes; if different business IDs need stronger separation, model that at the feature/domain type boundary.
-4
View File
@@ -101,10 +101,6 @@
"types": "./src/radio/index.tsx",
"import": "./src/radio/index.tsx"
},
"./radio-group": {
"types": "./src/radio-group/index.tsx",
"import": "./src/radio-group/index.tsx"
},
"./popover": {
"types": "./src/popover/index.tsx",
"import": "./src/popover/index.tsx"
@@ -1,82 +0,0 @@
import * as React from 'react'
import { render } from 'vitest-browser-react'
import { FieldItem, FieldLabel, FieldRoot } from '../../field'
import { FieldsetLegend, FieldsetRoot } from '../../fieldset'
import { Radio } from '../../radio'
import { RadioGroup } from '../index'
const clickElement = (element: HTMLElement | SVGElement) => {
element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
}
describe('RadioGroup', () => {
it('should manage a controlled single selection', async () => {
function StorageDemo() {
const [value, setValue] = React.useState('ssd')
return (
<FieldRoot name="storageType">
<FieldsetRoot render={<RadioGroup value={value} onValueChange={setValue} />}>
<FieldsetLegend>Storage type</FieldsetLegend>
<FieldItem>
<FieldLabel>
<Radio value="ssd" />
SSD
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Radio value="hdd" />
HDD
</FieldLabel>
</FieldItem>
</FieldsetRoot>
</FieldRoot>
)
}
const screen = await render(<StorageDemo />)
await expect.element(screen.getByRole('radio', { name: 'SSD' })).toHaveAttribute('aria-checked', 'true')
clickElement(screen.getByRole('radio', { name: 'HDD' }).element())
await vi.waitFor(async () => {
await expect.element(screen.getByRole('radio', { name: 'SSD' })).toHaveAttribute('aria-checked', 'false')
await expect.element(screen.getByRole('radio', { name: 'HDD' })).toHaveAttribute('aria-checked', 'true')
})
})
it('should compose with Dify UI Field and Fieldset without losing labels', async () => {
const onValueChange = vi.fn()
const screen = await render(
<FieldRoot name="storageType">
<FieldsetRoot render={<RadioGroup value="ssd" onValueChange={onValueChange} />}>
<FieldsetLegend>Storage type</FieldsetLegend>
<FieldItem>
<FieldLabel>
<Radio value="ssd" />
SSD
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Radio value="hdd" />
HDD
</FieldLabel>
</FieldItem>
</FieldsetRoot>
</FieldRoot>,
)
await expect.element(screen.getByRole('radiogroup', { name: 'Storage type' })).toBeInTheDocument()
const hdd = screen.getByRole('radio', { name: 'HDD' })
await expect.element(hdd).toHaveAttribute('aria-checked', 'false')
clickElement(hdd.element())
expect(onValueChange).toHaveBeenCalledTimes(1)
expect(onValueChange.mock.calls[0]?.[0]).toBe('hdd')
})
})
@@ -1,295 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { RadioGroup } from '.'
import {
FieldDescription,
FieldItem,
FieldLabel,
FieldRoot,
} from '../field'
import { FieldsetLegend, FieldsetRoot } from '../fieldset'
import { Radio, RadioControl, RadioIndicator, RadioRoot } from '../radio'
const meta = {
title: 'Base/Form/RadioGroup',
component: RadioGroup,
parameters: {
layout: 'centered',
docs: {
description: {
component: '`RadioGroup` owns single-selection state. Use `Radio` for plain form rows, `RadioRoot` when an entire row or card is the radio item, `RadioControl` for the standard visual dot inside custom roots, and `RadioIndicator` only when the design owns a custom control shell.',
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof RadioGroup>
export default meta
type Story = StoryObj<typeof meta>
function StandardFormRowsDemo() {
const [value, setValue] = React.useState('vector')
return (
<FieldRoot name="retrievalIndex" className="w-80">
<FieldsetRoot
render={(
<RadioGroup value={value} onValueChange={setValue} className="flex-col items-start gap-3" />
)}
>
<FieldsetLegend>Retrieval index</FieldsetLegend>
{[
{ value: 'vector', label: 'Vector storage' },
{ value: 'keyword', label: 'Keyword index' },
{ value: 'hybrid', label: 'Hybrid retrieval' },
].map(option => (
<FieldItem key={option.value}>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value={option.value} />
{option.label}
</FieldLabel>
</FieldItem>
))}
</FieldsetRoot>
</FieldRoot>
)
}
export const StandardFormRows: Story = {
render: () => <StandardFormRowsDemo />,
parameters: {
docs: {
description: {
story: 'Plain form-row composition. `RadioGroup` owns value, `FieldsetLegend` names the group, `FieldLabel` makes each option label clickable, and `Radio` renders the default dot.',
},
},
},
}
function BooleanInlineDemo() {
const [value, setValue] = React.useState(true)
return (
<FieldRoot name="streaming" className="w-80">
<FieldsetRoot
render={(
<RadioGroup<boolean> value={value} onValueChange={setValue} className="gap-3" />
)}
>
<FieldsetLegend>Streaming output</FieldsetLegend>
<div className="flex items-center gap-3">
<FieldItem>
<FieldLabel className="flex items-center gap-1.5 system-sm-regular text-text-secondary">
<Radio<boolean> value={true} />
True
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel className="flex items-center gap-1.5 system-sm-regular text-text-secondary">
<Radio<boolean> value={false} />
False
</FieldLabel>
</FieldItem>
</div>
</FieldsetRoot>
</FieldRoot>
)
}
export const BooleanInline: Story = {
render: () => <BooleanInlineDemo />,
parameters: {
docs: {
description: {
story: 'Compact boolean radio fields. This is the pattern used by model parameters and dynamic boolean schema fields.',
},
},
},
}
type PromptMode = 'default' | 'custom'
function OptionCardsDemo() {
const [value, setValue] = React.useState<PromptMode>('default')
const options = [
{
value: 'default',
title: 'Default prompt',
description: 'Use the built-in prompt for consistent output.',
},
{
value: 'custom',
title: 'Custom prompt',
description: 'Write a prompt for this app and keep full control.',
},
] satisfies Array<{
value: PromptMode
title: string
description: string
}>
return (
<FieldRoot name="promptMode" className="w-100">
<FieldsetRoot
render={(
<RadioGroup<PromptMode> value={value} onValueChange={setValue} className="flex-col items-stretch gap-3" />
)}
>
<FieldsetLegend>Prompt mode</FieldsetLegend>
{options.map(option => (
<FieldItem key={option.value}>
<RadioRoot<PromptMode>
value={option.value}
variant="unstyled"
nativeButton
render={<button type="button" />}
className="w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-4 text-left outline-hidden transition-colors hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg"
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="system-sm-semibold text-text-primary">
{option.title}
</div>
<div className="mt-1 system-xs-regular text-text-tertiary">
{option.description}
</div>
</div>
<RadioControl aria-hidden="true" />
</div>
</RadioRoot>
</FieldItem>
))}
</FieldsetRoot>
</FieldRoot>
)
}
export const OptionCards: Story = {
render: () => <OptionCardsDemo />,
parameters: {
docs: {
description: {
story: 'Product option cards should make the whole card the radio item with `RadioRoot variant="unstyled"`. `RadioControl` renders the standard visual dot inside the custom root.',
},
},
},
}
type ApprovalMode = 'automatic' | 'manual'
function CustomIndicatorPartDemo() {
const [value, setValue] = React.useState<ApprovalMode>('automatic')
const options = [
{
value: 'automatic',
title: 'Automatic approval',
description: 'Approve requests that match policy.',
},
{
value: 'manual',
title: 'Manual review',
description: 'Ask an admin to review each request.',
},
] satisfies Array<{
value: ApprovalMode
title: string
description: string
}>
return (
<FieldRoot name="approvalMode" className="w-100">
<FieldsetRoot
render={(
<RadioGroup<ApprovalMode> value={value} onValueChange={setValue} className="flex-col items-stretch gap-2" />
)}
>
<FieldsetLegend>Approval mode</FieldsetLegend>
{options.map(option => (
<FieldItem key={option.value}>
<RadioRoot<ApprovalMode>
value={option.value}
variant="unstyled"
nativeButton
render={<button type="button" />}
className="flex w-full items-center gap-3 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2 text-left outline-hidden transition-colors hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg"
>
<span className="flex size-4 shrink-0 items-center justify-center rounded-full border border-components-radio-border bg-components-radio-bg">
<RadioIndicator className="text-components-radio-border-checked" />
</span>
<span className="min-w-0 grow">
<span className="block truncate system-sm-semibold text-text-primary">
{option.title}
</span>
<span className="block truncate system-xs-regular text-text-tertiary">
{option.description}
</span>
</span>
</RadioRoot>
</FieldItem>
))}
</FieldsetRoot>
</FieldRoot>
)
}
export const CustomIndicatorPart: Story = {
render: () => <CustomIndicatorPartDemo />,
parameters: {
docs: {
description: {
story: '`RadioIndicator` is the low-level indicator part. Use it only when a custom root owns the outer control shell; otherwise prefer `Radio` for form rows or `RadioControl` inside option cards.',
},
},
},
}
function DynamicFormFieldDemo() {
const options = [
{ value: 'automatic', label: 'Automatic' },
{ value: 'high_quality', label: 'High quality' },
{ value: 'economy', label: 'Economy' },
]
const [selected, setSelected] = React.useState('automatic')
return (
<FieldRoot name="generation_mode" className="flex w-80 flex-col gap-2">
<FieldDescription className="body-xs-regular text-text-tertiary">
This mirrors Dify dynamic form fields where radio options are controlled by schema and persisted as a single value.
</FieldDescription>
<FieldsetRoot
render={(
<RadioGroup
value={selected}
onValueChange={setSelected}
className="flex-col items-start gap-2 rounded-lg border border-components-panel-border bg-components-panel-bg p-3"
/>
)}
>
<FieldsetLegend className="system-sm-medium text-text-secondary">
Generation mode
</FieldsetLegend>
{options.map(option => (
<FieldItem key={option.value}>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value={option.value} />
{option.label}
</FieldLabel>
</FieldItem>
))}
</FieldsetRoot>
</FieldRoot>
)
}
export const DynamicFormField: Story = {
render: () => <DynamicFormFieldDemo />,
parameters: {
docs: {
description: {
story: 'Matches Dify form composition: Field and Fieldset provide group labeling while RadioGroup owns controlled single-selection state.',
},
},
},
}
@@ -1,23 +0,0 @@
'use client'
import type { RadioGroup as BaseRadioGroupNS } from '@base-ui/react/radio-group'
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group'
import { cn } from '../cn'
export type RadioGroupProps<Value = string>
= Omit<BaseRadioGroupNS.Props<Value>, 'className'>
& {
className?: string
}
export function RadioGroup<Value = string>({
className,
...props
}: RadioGroupProps<Value>) {
return (
<BaseRadioGroup<Value>
className={cn('flex items-center gap-2', className)}
{...props}
/>
)
}
@@ -1,35 +1,29 @@
import type * as React from 'react'
import type { RadioGroupProps } from '../index'
import * as React from 'react'
import { render } from 'vitest-browser-react'
import { FieldItem, FieldLabel, FieldRoot } from '../../field'
import { FieldsetLegend, FieldsetRoot } from '../../fieldset'
import { RadioGroup } from '../../radio-group'
import {
Radio,
RadioControl,
RadioIndicator,
RadioRoot,
RadioSkeleton,
} from '../index'
import { Radio, RadioControl, RadioGroup, RadioItem, RadioSkeleton } from '../index'
const clickElement = (element: HTMLElement | SVGElement) => {
element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
}
type TestRadioGroupProps = React.ComponentProps<typeof RadioGroup> & {
type TestRadioGroupProps<Value = string> = RadioGroupProps<Value> & {
children: React.ReactNode
label: string
name?: string
}
function TestRadioGroup({
function TestRadioGroup<Value = string>({
children,
label,
name = 'radioField',
...props
}: TestRadioGroupProps) {
}: TestRadioGroupProps<Value>) {
return (
<FieldRoot name={name}>
<FieldsetRoot render={<RadioGroup {...props} />}>
<FieldsetRoot render={<RadioGroup<Value> {...props} />}>
<FieldsetLegend>{label}</FieldsetLegend>
{children}
</FieldsetRoot>
@@ -59,17 +53,63 @@ function RadioTypeExamples() {
return (
<RadioGroup<boolean> value={true} onValueChange={() => {}}>
<Radio<boolean> value={true} />
<RadioRoot<boolean> value={false} />
<RadioItem<boolean> value={false} />
{/* @ts-expect-error boolean radio items should not accept string values */}
<Radio<boolean> value="true" />
{/* @ts-expect-error boolean radio roots should not accept string values */}
<RadioRoot<boolean> value="false" />
{/* @ts-expect-error boolean radio items should not accept string values */}
<RadioItem<boolean> value="false" />
</RadioGroup>
)
}
void RadioTypeExamples
describe('RadioGroup', () => {
it('should manage a controlled single selection', async () => {
function StorageDemo() {
const [value, setValue] = React.useState('ssd')
return (
<TestRadioGroup value={value} onValueChange={setValue} label="Storage type">
<TestRadioOption value="ssd">SSD</TestRadioOption>
<TestRadioOption value="hdd">HDD</TestRadioOption>
</TestRadioGroup>
)
}
const screen = await render(<StorageDemo />)
await expect.element(screen.getByRole('radio', { name: 'SSD' })).toHaveAttribute('aria-checked', 'true')
clickElement(screen.getByRole('radio', { name: 'HDD' }).element())
await vi.waitFor(async () => {
await expect.element(screen.getByRole('radio', { name: 'SSD' })).toHaveAttribute('aria-checked', 'false')
await expect.element(screen.getByRole('radio', { name: 'HDD' })).toHaveAttribute('aria-checked', 'true')
})
})
it('should compose with Dify UI Field and Fieldset without losing labels', async () => {
const onValueChange = vi.fn()
const screen = await render(
<TestRadioGroup value="ssd" onValueChange={onValueChange} label="Storage type">
<TestRadioOption value="ssd">SSD</TestRadioOption>
<TestRadioOption value="hdd">HDD</TestRadioOption>
</TestRadioGroup>,
)
await expect.element(screen.getByRole('radiogroup', { name: 'Storage type' })).toBeInTheDocument()
const hdd = screen.getByRole('radio', { name: 'HDD' })
await expect.element(hdd).toHaveAttribute('aria-checked', 'false')
clickElement(hdd.element())
expect(onValueChange).toHaveBeenCalledTimes(1)
expect(onValueChange.mock.calls[0]?.[0]).toBe('hdd')
})
})
describe('Radio', () => {
it('should render unchecked and checked radios with Base UI semantics', async () => {
const screen = await render(
@@ -141,36 +181,17 @@ describe('Radio', () => {
expect(data.get('storageType')).toBe('ssd')
})
it('should support custom compound composition with RadioRoot and RadioIndicator', async () => {
const screen = await render(
<TestRadioGroup defaultValue="custom" label="Custom">
<FieldItem>
<FieldLabel>
<RadioRoot value="custom" className="custom-root">
<RadioIndicator className="custom-indicator" keepMounted />
</RadioRoot>
Custom
</FieldLabel>
</FieldItem>
</TestRadioGroup>,
)
await expect.element(screen.getByRole('radio', { name: 'Custom' })).toHaveClass('custom-root')
expect(screen.container.querySelector('.custom-indicator')).toBeInTheDocument()
})
it('should support unstyled roots with a visual RadioControl for option cards', async () => {
it('should support custom items with a visual RadioControl', async () => {
const screen = await render(
<RadioGroup defaultValue="card" aria-label="Card choice">
<RadioRoot
<RadioItem
value="card"
variant="unstyled"
nativeButton
render={<button type="button" className="custom-card" />}
>
<span>Card option</span>
<RadioControl className="custom-control" />
</RadioRoot>
</RadioItem>
</RadioGroup>,
)
+195 -66
View File
@@ -2,115 +2,239 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import {
Radio,
RadioControl,
RadioGroup,
RadioItem,
RadioSkeleton,
} from '.'
import { FieldItem, FieldLabel, FieldRoot } from '../field'
import {
FieldDescription,
FieldItem,
FieldLabel,
FieldRoot,
} from '../field'
import { FieldsetLegend, FieldsetRoot } from '../fieldset'
import { RadioGroup } from '../radio-group'
const meta = {
title: 'Base/Form/Radio',
component: Radio,
component: RadioGroup,
parameters: {
layout: 'centered',
docs: {
description: {
component: '`Radio` is the default Dify 16px radio control, intended for plain form rows inside `RadioGroup`. It does not accept children. For option cards or rich rows, use `RadioRoot` as the item root and place `RadioControl` inside it; see the `RadioGroup` stories.',
component: '`@langgenius/dify-ui/radio` exports the complete radio family. `RadioGroup` owns single-selection state, `Radio` is the default Dify control for plain form rows, `RadioItem` makes custom UI the radio item, and `RadioControl` renders the standard visual dot inside custom items.',
},
},
},
tags: ['autodocs'],
args: {
disabled: false,
value: 'ssd',
},
argTypes: {
disabled: {
control: 'boolean',
description: 'Disables user interaction and exposes Base UI disabled state attributes.',
},
},
} satisfies Meta<typeof Radio>
} satisfies Meta<typeof RadioGroup>
export default meta
type Story = StoryObj<typeof meta>
function RadioDemo(args: Partial<React.ComponentProps<typeof Radio>>) {
const [value, setValue] = React.useState('ssd')
function StandardFormRowsDemo() {
const [value, setValue] = React.useState('vector')
return (
<FieldRoot name="storageType">
<FieldRoot name="retrievalIndex" className="w-80">
<FieldsetRoot
render={(
<RadioGroup value={value} onValueChange={setValue} className="flex-col items-start gap-3" />
)}
>
<FieldsetLegend>Storage type</FieldsetLegend>
<FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio {...args} value="ssd" />
SSD
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio {...args} value="hdd" />
HDD
</FieldLabel>
</FieldItem>
<FieldsetLegend>Retrieval index</FieldsetLegend>
{[
{ value: 'vector', label: 'Vector storage' },
{ value: 'keyword', label: 'Keyword index' },
{ value: 'hybrid', label: 'Hybrid retrieval' },
].map(option => (
<FieldItem key={option.value}>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value={option.value} />
{option.label}
</FieldLabel>
</FieldItem>
))}
</FieldsetRoot>
</FieldRoot>
)
}
export const Default: Story = {
render: args => <RadioDemo {...args} />,
args: {
disabled: false,
},
export const StandardFormRows: Story = {
render: () => <StandardFormRowsDemo />,
parameters: {
docs: {
description: {
story: '`Radio` renders the standard visual control. `FieldLabel` owns the clickable text label, and `RadioGroup` owns the selected value.',
story: 'Plain form-row composition. `RadioGroup` owns value, `FieldsetLegend` names the group, `FieldLabel` makes each option label clickable, and `Radio` renders the default dot.',
},
},
},
}
export const Disabled: Story = {
args: {
value: 'checked',
},
render: () => (
<FieldRoot name="disabledStates">
<FieldsetRoot render={<RadioGroup value="checked" className="flex-col items-start gap-3" />}>
<FieldsetLegend>Disabled states</FieldsetLegend>
<FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value="unchecked" disabled />
Disabled unchecked
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value="checked" disabled />
Disabled checked
</FieldLabel>
</FieldItem>
function BooleanInlineDemo() {
const [value, setValue] = React.useState(true)
return (
<FieldRoot name="streaming" className="w-80">
<FieldsetRoot
render={(
<RadioGroup<boolean> value={value} onValueChange={setValue} className="gap-3" />
)}
>
<FieldsetLegend>Streaming output</FieldsetLegend>
<div className="flex items-center gap-3">
<FieldItem>
<FieldLabel className="flex items-center gap-1.5 system-sm-regular text-text-secondary">
<Radio<boolean> value={true} />
True
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel className="flex items-center gap-1.5 system-sm-regular text-text-secondary">
<Radio<boolean> value={false} />
False
</FieldLabel>
</FieldItem>
</div>
</FieldsetRoot>
</FieldRoot>
),
)
}
export const BooleanInline: Story = {
render: () => <BooleanInlineDemo />,
parameters: {
docs: {
description: {
story: 'Compact boolean radio fields. Type `RadioGroup<boolean>` and each child `Radio<boolean>` when the selected value is not a string.',
},
},
},
}
type PromptMode = 'default' | 'custom'
function OptionCardsDemo() {
const [value, setValue] = React.useState<PromptMode>('default')
const options = [
{
value: 'default',
title: 'Default prompt',
description: 'Use the built-in prompt for consistent output.',
},
{
value: 'custom',
title: 'Custom prompt',
description: 'Write a prompt for this app and keep full control.',
},
] satisfies Array<{
value: PromptMode
title: string
description: string
}>
return (
<FieldRoot name="promptMode" className="w-100">
<FieldsetRoot
render={(
<RadioGroup<PromptMode> value={value} onValueChange={setValue} className="flex-col items-stretch gap-3" />
)}
>
<FieldsetLegend>Prompt mode</FieldsetLegend>
{options.map(option => (
<FieldItem key={option.value}>
<RadioItem<PromptMode>
value={option.value}
nativeButton
render={<button type="button" />}
className="w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-4 text-left outline-hidden transition-colors hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg"
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="system-sm-semibold text-text-primary">
{option.title}
</div>
<div className="mt-1 system-xs-regular text-text-tertiary">
{option.description}
</div>
</div>
<RadioControl aria-hidden="true" />
</div>
</RadioItem>
</FieldItem>
))}
</FieldsetRoot>
</FieldRoot>
)
}
export const OptionCards: Story = {
render: () => <OptionCardsDemo />,
parameters: {
docs: {
description: {
story: 'Custom option UIs should make the whole interactive surface the radio item with `RadioItem`. `RadioControl` is the Dify visual dot; the radio semantics stay on `RadioItem`.',
},
},
},
}
function DynamicFormFieldDemo() {
const options = [
{ value: 'automatic', label: 'Automatic' },
{ value: 'high_quality', label: 'High quality' },
{ value: 'economy', label: 'Economy' },
]
const [selected, setSelected] = React.useState('automatic')
return (
<FieldRoot name="generation_mode" className="flex w-80 flex-col gap-2">
<FieldDescription className="body-xs-regular text-text-tertiary">
This mirrors Dify dynamic form fields where radio options are controlled by schema and persisted as a single value.
</FieldDescription>
<FieldsetRoot
render={(
<RadioGroup
value={selected}
onValueChange={setSelected}
className="flex-col items-start gap-2 rounded-lg border border-components-panel-border bg-components-panel-bg p-3"
/>
)}
>
<FieldsetLegend className="system-sm-medium text-text-secondary">
Generation mode
</FieldsetLegend>
{options.map(option => (
<FieldItem key={option.value}>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value={option.value} />
{option.label}
</FieldLabel>
</FieldItem>
))}
</FieldsetRoot>
</FieldRoot>
)
}
export const DynamicFormField: Story = {
render: () => <DynamicFormFieldDemo />,
parameters: {
docs: {
description: {
story: 'Matches Dify form composition: Field and Fieldset provide group labeling while `RadioGroup` owns controlled single-selection state.',
},
},
},
}
export const StateMatrix: Story = {
args: {
value: 'checked',
},
render: () => (
<div className="flex flex-col gap-3">
<FieldRoot name="radioStates">
<FieldsetRoot render={<RadioGroup value="checked" className="flex-col items-start gap-3" />}>
<FieldsetLegend>Radio states</FieldsetLegend>
<FieldsetRoot render={<RadioGroup defaultValue="checked" className="flex-col items-start gap-3" />}>
<FieldsetLegend>Interactive radio states</FieldsetLegend>
<FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value="unchecked" />
@@ -123,15 +247,20 @@ export const StateMatrix: Story = {
Checked
</FieldLabel>
</FieldItem>
</FieldsetRoot>
</FieldRoot>
<FieldRoot name="disabledRadioStates">
<FieldsetRoot render={<RadioGroup defaultValue="disabled-checked" disabled className="flex-col items-start gap-3" />}>
<FieldsetLegend>Disabled radio states</FieldsetLegend>
<FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value="disabled-unchecked" disabled />
<Radio value="disabled-unchecked" />
Disabled unchecked
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value="checked" disabled />
<Radio value="disabled-checked" />
Disabled checked
</FieldLabel>
</FieldItem>
@@ -146,7 +275,7 @@ export const StateMatrix: Story = {
parameters: {
docs: {
description: {
story: 'The full visual matrix for Dify radio states. State styling comes from Base UI data attributes such as data-checked and data-disabled.',
story: 'The visual matrix for Dify radio states. State styling comes from Base UI data attributes such as data-checked and data-disabled.',
},
},
},
+54 -39
View File
@@ -1,68 +1,53 @@
'use client'
import type { Radio as BaseRadioNS } from '@base-ui/react/radio'
import type { RadioGroup as BaseRadioGroupNS } from '@base-ui/react/radio-group'
import type * as React from 'react'
import { Radio as BaseRadio } from '@base-ui/react/radio'
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group'
import { cn } from '../cn'
const radioRootClassName = cn(
'inline-flex size-4 shrink-0 touch-manipulation items-center justify-center rounded-full p-0 transition-colors motion-reduce:transition-none',
'border border-components-radio-border bg-components-radio-bg shadow-xs shadow-shadow-shadow-3',
'hover:border-components-radio-border-hover hover:bg-components-radio-bg-hover',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-offset-0',
'data-checked:border-[5px] data-checked:border-components-radio-border-checked data-checked:hover:border-components-radio-border-checked-hover',
'data-disabled:cursor-not-allowed data-disabled:border-components-radio-border-disabled data-disabled:bg-components-radio-bg-disabled',
'data-disabled:hover:border-components-radio-border-disabled data-disabled:hover:bg-components-radio-bg-disabled',
'data-disabled:data-checked:border-[5px] data-disabled:data-checked:border-components-radio-border-checked-disabled',
'data-disabled:data-checked:hover:border-components-radio-border-checked-disabled',
)
const radioIndicatorClassName = 'flex items-center justify-center data-unchecked:hidden before:size-1.5 before:rounded-full before:bg-current'
const radioControlClassName = radioRootClassName
const radioSkeletonClassName = 'size-4 shrink-0 rounded-full bg-text-quaternary opacity-20'
export type RadioRootProps<Value = string>
= Omit<BaseRadioNS.Root.Props<Value>, 'className'>
export type RadioGroupProps<Value = string>
= Omit<BaseRadioGroupNS.Props<Value>, 'className'>
& {
className?: string
variant?: 'control' | 'unstyled'
}
export function RadioRoot<Value = string>({
export function RadioGroup<Value = string>({
className,
variant = 'control',
...props
}: RadioRootProps<Value>) {
}: RadioGroupProps<Value>) {
return (
<BaseRadio.Root<Value>
className={cn(variant === 'control' && radioRootClassName, className)}
<BaseRadioGroup<Value>
className={cn('flex items-center gap-2', className)}
{...props}
/>
)
}
export type RadioIndicatorProps
= Omit<BaseRadioNS.Indicator.Props, 'className' | 'children'>
export type RadioItemProps<Value = string>
= Omit<BaseRadioNS.Root.Props<Value>, 'className'>
& {
className?: string
}
export function RadioIndicator({
export function RadioItem<Value = string>({
className,
...props
}: RadioIndicatorProps) {
}: RadioItemProps<Value>) {
return (
<BaseRadio.Indicator
className={cn(radioIndicatorClassName, className)}
<BaseRadio.Root<Value>
className={className}
{...props}
/>
)
}
export type RadioControlProps
= Omit<RadioIndicatorProps, 'keepMounted'>
= Omit<BaseRadioNS.Indicator.Props, 'className' | 'children' | 'keepMounted'>
& {
className?: string
}
export function RadioControl({
className,
@@ -70,18 +55,48 @@ export function RadioControl({
}: RadioControlProps) {
return (
<BaseRadio.Indicator
keepMounted
className={cn(radioControlClassName, className)}
{...props}
keepMounted
className={cn(
'inline-flex size-4 shrink-0 touch-manipulation items-center justify-center rounded-full p-0 transition-colors motion-reduce:transition-none',
'border border-components-radio-border bg-components-radio-bg shadow-xs shadow-shadow-shadow-3',
'hover:border-components-radio-border-hover hover:bg-components-radio-bg-hover',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-offset-0',
'data-checked:border-[5px] data-checked:border-components-radio-border-checked data-checked:hover:border-components-radio-border-checked-hover',
'data-disabled:cursor-not-allowed data-disabled:border-components-radio-border-disabled data-disabled:bg-components-radio-bg-disabled',
'data-disabled:hover:border-components-radio-border-disabled data-disabled:hover:bg-components-radio-bg-disabled',
'data-disabled:data-checked:border-[5px] data-disabled:data-checked:border-components-radio-border-checked-disabled',
'data-disabled:data-checked:hover:border-components-radio-border-checked-disabled',
className,
)}
/>
)
}
export type RadioProps<Value = string>
= Omit<RadioRootProps<Value>, 'children'>
= Omit<RadioItemProps<Value>, 'children'>
export function Radio<Value = string>(props: RadioProps<Value>) {
return <RadioRoot<Value> {...props} />
export function Radio<Value = string>({
className,
...props
}: RadioProps<Value>) {
return (
<BaseRadio.Root<Value>
className={cn(
'inline-flex size-4 shrink-0 touch-manipulation items-center justify-center rounded-full p-0 transition-colors motion-reduce:transition-none',
'border border-components-radio-border bg-components-radio-bg shadow-xs shadow-shadow-shadow-3',
'hover:border-components-radio-border-hover hover:bg-components-radio-bg-hover',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-offset-0',
'data-checked:border-[5px] data-checked:border-components-radio-border-checked data-checked:hover:border-components-radio-border-checked-hover',
'data-disabled:cursor-not-allowed data-disabled:border-components-radio-border-disabled data-disabled:bg-components-radio-bg-disabled',
'data-disabled:hover:border-components-radio-border-disabled data-disabled:hover:bg-components-radio-bg-disabled',
'data-disabled:data-checked:border-[5px] data-disabled:data-checked:border-components-radio-border-checked-disabled',
'data-disabled:data-checked:hover:border-components-radio-border-checked-disabled',
className,
)}
{...props}
/>
)
}
export type RadioSkeletonProps = React.ComponentProps<'div'>
@@ -92,7 +107,7 @@ export function RadioSkeleton({
}: RadioSkeletonProps) {
return (
<div
className={cn(radioSkeletonClassName, className)}
className={cn('size-4 shrink-0 rounded-full bg-text-quaternary opacity-20', className)}
{...props}
/>
)
@@ -10,8 +10,7 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { FieldItem, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { produce } from 'immer'
import { useCallback, useMemo, useState } from 'react'
@@ -162,9 +161,8 @@ const FollowUpSettingModal = ({
{t('feature.suggestedQuestionsAfterAnswer.modal.promptLabel', { ns: 'appDebug' })}
</FieldsetLegend>
<FieldItem>
<RadioRoot<PromptMode>
<RadioItem<PromptMode>
value={PROMPT_MODE.default}
variant="unstyled"
nativeButton
render={<button type="button" />}
className={cn(
@@ -192,12 +190,11 @@ const FollowUpSettingModal = ({
</div>
</div>
)}
</RadioRoot>
</RadioItem>
</FieldItem>
<FieldItem>
<RadioRoot<PromptMode>
<RadioItem<PromptMode>
value={PROMPT_MODE.custom}
variant="unstyled"
nativeButton
render={<button type="button" />}
className={cn(
@@ -228,7 +225,7 @@ const FollowUpSettingModal = ({
placeholder={t('feature.suggestedQuestionsAfterAnswer.modal.promptPlaceholder', { ns: 'appDebug' }) || ''}
/>
)}
</RadioRoot>
</RadioItem>
</FieldItem>
</FieldsetRoot>
</FieldRoot>
@@ -3,8 +3,7 @@ import type { FieldState, FormSchema, TypeWithI18N } from '@/app/components/base
import { cn } from '@langgenius/dify-ui/cn'
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { Radio } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import {
Select,
SelectContent,
@@ -1,7 +1,7 @@
'use client'
import type { NotionPageRow, NotionPageSelectionMode } from './types'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
import { useTranslation } from 'react-i18next'
@@ -1,4 +1,4 @@
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { RiDatabase2Line, RiFileList3Line, RiRocketLine } from '@remixicon/react'
import { useState } from 'react'
import RadioCard from '.'
@@ -11,7 +11,7 @@ const meta = {
layout: 'centered',
docs: {
description: {
component: 'Radio card for rich single-choice options. Put selectable cards inside `RadioGroup`; the card passes radio props through to `RadioRoot` and uses `RadioControl` for the visual dot.',
component: 'Radio card for rich single-choice options. Put selectable cards inside `RadioGroup`; the card passes radio props through to `RadioItem` and uses `RadioControl` for the visual dot.',
},
},
},
+5 -6
View File
@@ -1,8 +1,8 @@
'use client'
import type { RadioRootProps } from '@langgenius/dify-ui/radio'
import type { RadioItemProps } from '@langgenius/dify-ui/radio'
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioControl, RadioItem } from '@langgenius/dify-ui/radio'
type BaseProps = {
className?: string
@@ -16,7 +16,7 @@ type BaseProps = {
type SelectableRadioCardProps<Value = string> = BaseProps & {
noRadio?: false
} & Omit<RadioRootProps<Value>, 'children' | 'className' | 'variant' | 'render' | 'nativeButton'>
} & Omit<RadioItemProps<Value>, 'children' | 'className' | 'render' | 'nativeButton'>
type StaticRadioCardProps = BaseProps & {
noRadio: true
@@ -106,16 +106,15 @@ function RadioCard<Value = string>(props: Props<Value>) {
<div
className={rootClassName}
>
<RadioRoot<Value>
<RadioItem<Value>
{...radioRootProps}
variant="unstyled"
nativeButton
render={<button type="button" />}
className="flex w-full cursor-pointer gap-x-2 border-none bg-transparent p-0 text-left outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-active"
>
{content}
<RadioControl className="absolute top-3 right-3" aria-hidden="true" />
</RadioRoot>
</RadioItem>
{config}
</div>
)
@@ -3,7 +3,7 @@ import type { FC } from 'react'
import type { RetrievalConfig } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
@@ -5,7 +5,7 @@ import type { ParentChildConfig } from '../hooks'
import type { ParentMode, PreProcessingRule, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { RiSearchEyeLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
@@ -1,6 +1,6 @@
import type { Mock } from 'vitest'
import type { OnlineDriveFile } from '@/models/pipeline'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { OnlineDriveFileType } from '@/models/pipeline'
@@ -1,5 +1,5 @@
import type { OnlineDriveFile } from '@/models/pipeline'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Item from '../item'
@@ -1,5 +1,5 @@
import type { OnlineDriveFile } from '@/models/pipeline'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { RiLoader2Line } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useRef } from 'react'
@@ -1,5 +1,5 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CrawledResultItem from '../crawled-result-item'
@@ -1,5 +1,5 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
@@ -1,7 +1,7 @@
'use client'
import type { CrawlResultItem } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
+3 -5
View File
@@ -1,8 +1,7 @@
'use client'
import type { AppCategory } from '@/models/explore'
import { cn } from '@langgenius/dify-ui/cn'
import { RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { useTranslation } from 'react-i18next'
import exploreI18n from '@/i18n/en-US/explore.json'
@@ -53,16 +52,15 @@ function Category({
isAll: false,
})),
].map(item => (
<RadioRoot
<RadioItem
key={item.isAll ? 'all' : item.name}
value={item.name}
variant="unstyled"
nativeButton
render={<button type="button" />}
className="h-8 min-w-12 shrink-0 cursor-pointer touch-manipulation rounded-lg border-0 px-2.5 py-2 text-center system-sm-medium text-text-tertiary shadow-none transition-colors hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-checked:border-0 data-checked:bg-state-base-active data-checked:system-sm-semibold data-checked:text-text-secondary data-checked:shadow-none motion-reduce:transition-none"
>
{item.label}
</RadioRoot>
</RadioItem>
))}
</RadioGroup>
)
@@ -15,8 +15,7 @@ import type {
import { cn } from '@langgenius/dify-ui/cn'
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { Radio } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger } from '@langgenius/dify-ui/select'
import { useCallback, useState } from 'react'
import { Infotip } from '@/app/components/base/infotip'
@@ -6,8 +6,7 @@ import type {
import { cn } from '@langgenius/dify-ui/cn'
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { Radio } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select'
import { Slider } from '@langgenius/dify-ui/slider'
import { Switch } from '@langgenius/dify-ui/switch'
@@ -4,7 +4,7 @@ import type { AutoUpdateConfig } from '@/app/components/plugins/reference-settin
import type { dayjsToTimeOfDay } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
import type { PluginCategoryEnum } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
@@ -1,5 +1,5 @@
import { cn } from '@langgenius/dify-ui/cn'
import { RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioItem } from '@langgenius/dify-ui/radio'
type UpdateSettingOptionCardProps<Value extends string> = {
value: Value
@@ -19,9 +19,8 @@ const UpdateSettingOptionCard = <Value extends string>({
onMouseLeave,
}: UpdateSettingOptionCardProps<Value>) => {
return (
<RadioRoot<Value>
<RadioItem<Value>
value={value}
variant="unstyled"
nativeButton
render={<button type="button" />}
onBlur={onBlur}
@@ -37,7 +36,7 @@ const UpdateSettingOptionCard = <Value extends string>({
)}
>
<span className="max-w-full min-w-0 truncate whitespace-nowrap">{label}</span>
</RadioRoot>
</RadioItem>
)
}
@@ -4,8 +4,7 @@ import type { Role } from '@/models/access-control'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -288,14 +287,15 @@ const WorkspaceRoleCheckboxList = ({
return (
<li key={role.id}>
<RadioRoot
<RadioItem
value={role.id}
disabled={disabled}
variant="unstyled"
nativeButton
render={(
<div
<button
type="button"
className={cn(
'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
'flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent px-3 py-2.5 text-left hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
)}
@@ -304,7 +304,7 @@ const WorkspaceRoleCheckboxList = ({
>
<RadioControl className="pointer-events-none mt-0.5" />
{renderRoleText(role)}
</RadioRoot>
</RadioItem>
</li>
)
})}
@@ -2,8 +2,7 @@
import type { Permissions } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn'
import { RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { useTranslation } from 'react-i18next'
import { PluginSidecarPanel } from '@/app/components/plugins/plugin-page/plugin-sidecar-panel'
import { PermissionType } from '@/app/components/plugins/types'
@@ -71,16 +70,15 @@ export function PermissionQuickPanel({
const optionLabel = t(`privilege.${option}`, { ns: 'plugin' })
return (
<RadioRoot<PermissionType>
<RadioItem<PermissionType>
key={option}
value={option}
variant="unstyled"
nativeButton
render={<button type="button" className={permissionOptionCardClassName} />}
aria-label={`${row.label}: ${optionLabel}`}
>
<span className="min-w-0 truncate">{optionLabel}</span>
</RadioRoot>
</RadioItem>
)
})}
</RadioGroup>
@@ -15,8 +15,7 @@ import {
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { Input } from '@langgenius/dify-ui/input'
import { Radio } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -1,7 +1,7 @@
import type { ComponentType, SVGProps } from 'react'
import { FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import {
fireEvent,
render,
@@ -11,7 +11,7 @@ import type {
} from './top-k-and-score-threshold'
import { FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup } from '@langgenius/dify-ui/radio'
import {
memo,
} from 'react'
@@ -14,8 +14,7 @@ import type {
import { cn } from '@langgenius/dify-ui/cn'
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { Switch } from '@langgenius/dify-ui/switch'
import { useTranslation } from 'react-i18next'
import WeightedScoreComponent from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
@@ -150,9 +149,8 @@ function SearchMethodRadioCard({
readonly && 'cursor-not-allowed',
)}
>
<RadioRoot<RetrievalSearchMethodEnum>
<RadioItem<RetrievalSearchMethodEnum>
value={option.id}
variant="unstyled"
nativeButton
render={<button type="button" />}
disabled={readonly}
@@ -186,7 +184,7 @@ function SearchMethodRadioCard({
)
: null}
</div>
</RadioRoot>
</RadioItem>
{!!(children && isActive) && (
<div className="relative rounded-b-xl bg-components-panel-bg p-3">
<div className="absolute -top-2.75 left-3.5 i-custom-vender-knowledge-arrow-shape h-4 w-4 text-components-panel-bg" />
@@ -206,9 +204,8 @@ function HybridSearchModeRadioCard({
}) {
return (
<FieldItem>
<RadioRoot<HybridSearchModeEnum>
<RadioItem<HybridSearchModeEnum>
value={option.id}
variant="unstyled"
nativeButton
render={<button type="button" />}
disabled={readonly}
@@ -230,7 +227,7 @@ function HybridSearchModeRadioCard({
</div>
<RadioControl className="mt-0.5" aria-hidden="true" />
</div>
</RadioRoot>
</RadioItem>
</FieldItem>
)
}
@@ -14,8 +14,7 @@ import type { DataSet, MetadataInDoc } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Input } from '@langgenius/dify-ui/input'
import { RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { intersectionBy } from 'es-toolkit/compat'
import { useAtomValue } from 'jotai'
@@ -500,17 +499,16 @@ export function AgentKnowledgeRetrievalDialog({
}}
>
{queryModeOptions.map(mode => (
<RadioRoot<KnowledgeRetrievalQueryMode>
<RadioItem<KnowledgeRetrievalQueryMode>
key={mode}
value={mode}
variant="unstyled"
nativeButton
render={<button type="button" className={optionCardClassName} />}
>
<span className="min-w-0 truncate">
{t(`agentDetail.configure.knowledgeRetrieval.dialog.query.${mode}`)}
</span>
</RadioRoot>
</RadioItem>
))}
</RadioGroup>
{queryMode === 'custom'
@@ -4,8 +4,7 @@ import type { GuideMethod, WorkflowSourceApp } from '@/features/deployments/crea
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import { RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
@@ -103,9 +102,10 @@ function SourceMethodCard({ value, icon, title, description, badge }: {
badge?: string
}) {
return (
<RadioRoot<GuideMethod>
<RadioItem<GuideMethod>
value={value}
variant="unstyled"
nativeButton
render={<button type="button" />}
className={cn(
`relative box-content h-[84px] w-full cursor-pointer rounded-xl border-[0.5px]
border-components-option-card-option-border bg-components-panel-on-panel-item-bg p-3
@@ -132,7 +132,7 @@ function SourceMethodCard({ value, icon, title, description, badge }: {
</span>
</TitleTooltip>
</span>
</RadioRoot>
</RadioItem>
)
}
@@ -3,8 +3,7 @@
import type { Environment } from '@dify/contracts/enterprise/types.gen'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
@@ -119,9 +118,10 @@ function EnvironmentOptionRow({ environment }: {
const summary = environment.description.trim() || `${t(`mode.${environment.mode}`)} · ${t(`backend.${environment.backend}`)}`
return (
<RadioRoot<string>
<RadioItem<string>
value={environment.id}
variant="unstyled"
nativeButton
render={<button type="button" />}
className={cn(
'group flex cursor-pointer items-center gap-3 rounded-xl border p-3 outline-hidden',
'border-components-option-card-option-border bg-components-option-card-option-bg hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
@@ -138,7 +138,7 @@ function EnvironmentOptionRow({ environment }: {
</span>
</TitleTooltip>
</span>
</RadioRoot>
</RadioItem>
)
}
@@ -12,8 +12,7 @@ import {
DialogDescription,
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AccessMode as AppAccessMode } from '@/models/access-control'
@@ -166,9 +165,8 @@ function AccessControlItem({ type, children }: PropsWithChildren<{
type: AppAccessMode
}>) {
return (
<RadioRoot<AppAccessMode>
<RadioItem<AppAccessMode>
value={type}
variant="unstyled"
render={<div />}
className={cn(
'cursor-pointer rounded-[10px] border-[0.5px] border-components-option-card-option-border bg-components-option-card-option-bg shadow-xs transition-colors',
@@ -179,7 +177,7 @@ function AccessControlItem({ type, children }: PropsWithChildren<{
)}
>
{children}
</RadioRoot>
</RadioItem>
)
}