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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 394 additions and 648 deletions

View File

@ -43,18 +43,18 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
## Primitives ## Primitives
| Category | Subpath | Notes | | Category | Subpath | Notes |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. | | Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. | | Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. |
| Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. | | 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. | | 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. | | 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. | | Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
| Media | `./avatar` | Avatar root, image, and fallback primitives. | | 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. | | 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. | | 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. | | Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
Utilities: Utilities:
@ -103,17 +103,22 @@ Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group
## Typed value contracts ## 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: 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 ```tsx
<RadioGroup<PromptMode> value={promptMode} onValueChange={setPromptMode}> <RadioGroup<PromptMode> value={promptMode} onValueChange={setPromptMode}>
<RadioRoot<PromptMode> value={PROMPT_MODE.default} /> <Radio<PromptMode> value={PROMPT_MODE.default} />
<RadioRoot<PromptMode> value={PROMPT_MODE.custom} /> <RadioItem<PromptMode> value={PROMPT_MODE.custom}>
<RadioControl />
Custom prompt
</RadioItem>
</RadioGroup> </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. 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. `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.

View File

@ -101,10 +101,6 @@
"types": "./src/radio/index.tsx", "types": "./src/radio/index.tsx",
"import": "./src/radio/index.tsx" "import": "./src/radio/index.tsx"
}, },
"./radio-group": {
"types": "./src/radio-group/index.tsx",
"import": "./src/radio-group/index.tsx"
},
"./popover": { "./popover": {
"types": "./src/popover/index.tsx", "types": "./src/popover/index.tsx",
"import": "./src/popover/index.tsx" "import": "./src/popover/index.tsx"

View File

@ -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')
})
})

View File

@ -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.',
},
},
},
}

View File

@ -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}
/>
)
}

View File

@ -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 { render } from 'vitest-browser-react'
import { FieldItem, FieldLabel, FieldRoot } from '../../field' import { FieldItem, FieldLabel, FieldRoot } from '../../field'
import { FieldsetLegend, FieldsetRoot } from '../../fieldset' import { FieldsetLegend, FieldsetRoot } from '../../fieldset'
import { RadioGroup } from '../../radio-group' import { Radio, RadioControl, RadioGroup, RadioItem, RadioSkeleton } from '../index'
import {
Radio,
RadioControl,
RadioIndicator,
RadioRoot,
RadioSkeleton,
} from '../index'
const clickElement = (element: HTMLElement | SVGElement) => { const clickElement = (element: HTMLElement | SVGElement) => {
element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
} }
type TestRadioGroupProps = React.ComponentProps<typeof RadioGroup> & { type TestRadioGroupProps<Value = string> = RadioGroupProps<Value> & {
children: React.ReactNode children: React.ReactNode
label: string label: string
name?: string name?: string
} }
function TestRadioGroup({ function TestRadioGroup<Value = string>({
children, children,
label, label,
name = 'radioField', name = 'radioField',
...props ...props
}: TestRadioGroupProps) { }: TestRadioGroupProps<Value>) {
return ( return (
<FieldRoot name={name}> <FieldRoot name={name}>
<FieldsetRoot render={<RadioGroup {...props} />}> <FieldsetRoot render={<RadioGroup<Value> {...props} />}>
<FieldsetLegend>{label}</FieldsetLegend> <FieldsetLegend>{label}</FieldsetLegend>
{children} {children}
</FieldsetRoot> </FieldsetRoot>
@ -59,17 +53,63 @@ function RadioTypeExamples() {
return ( return (
<RadioGroup<boolean> value={true} onValueChange={() => {}}> <RadioGroup<boolean> value={true} onValueChange={() => {}}>
<Radio<boolean> value={true} /> <Radio<boolean> value={true} />
<RadioRoot<boolean> value={false} /> <RadioItem<boolean> value={false} />
{/* @ts-expect-error boolean radio items should not accept string values */} {/* @ts-expect-error boolean radio items should not accept string values */}
<Radio<boolean> value="true" /> <Radio<boolean> value="true" />
{/* @ts-expect-error boolean radio roots should not accept string values */} {/* @ts-expect-error boolean radio items should not accept string values */}
<RadioRoot<boolean> value="false" /> <RadioItem<boolean> value="false" />
</RadioGroup> </RadioGroup>
) )
} }
void RadioTypeExamples 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', () => { describe('Radio', () => {
it('should render unchecked and checked radios with Base UI semantics', async () => { it('should render unchecked and checked radios with Base UI semantics', async () => {
const screen = await render( const screen = await render(
@ -141,36 +181,17 @@ describe('Radio', () => {
expect(data.get('storageType')).toBe('ssd') expect(data.get('storageType')).toBe('ssd')
}) })
it('should support custom compound composition with RadioRoot and RadioIndicator', async () => { it('should support custom items with a visual RadioControl', 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 () => {
const screen = await render( const screen = await render(
<RadioGroup defaultValue="card" aria-label="Card choice"> <RadioGroup defaultValue="card" aria-label="Card choice">
<RadioRoot <RadioItem
value="card" value="card"
variant="unstyled"
nativeButton nativeButton
render={<button type="button" className="custom-card" />} render={<button type="button" className="custom-card" />}
> >
<span>Card option</span> <span>Card option</span>
<RadioControl className="custom-control" /> <RadioControl className="custom-control" />
</RadioRoot> </RadioItem>
</RadioGroup>, </RadioGroup>,
) )

View File

@ -2,115 +2,239 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react' import * as React from 'react'
import { import {
Radio, Radio,
RadioControl,
RadioGroup,
RadioItem,
RadioSkeleton, RadioSkeleton,
} from '.' } from '.'
import { FieldItem, FieldLabel, FieldRoot } from '../field' import {
FieldDescription,
FieldItem,
FieldLabel,
FieldRoot,
} from '../field'
import { FieldsetLegend, FieldsetRoot } from '../fieldset' import { FieldsetLegend, FieldsetRoot } from '../fieldset'
import { RadioGroup } from '../radio-group'
const meta = { const meta = {
title: 'Base/Form/Radio', title: 'Base/Form/Radio',
component: Radio, component: RadioGroup,
parameters: { parameters: {
layout: 'centered', layout: 'centered',
docs: { docs: {
description: { 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'], tags: ['autodocs'],
args: { } satisfies Meta<typeof RadioGroup>
disabled: false,
value: 'ssd',
},
argTypes: {
disabled: {
control: 'boolean',
description: 'Disables user interaction and exposes Base UI disabled state attributes.',
},
},
} satisfies Meta<typeof Radio>
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
function RadioDemo(args: Partial<React.ComponentProps<typeof Radio>>) { function StandardFormRowsDemo() {
const [value, setValue] = React.useState('ssd') const [value, setValue] = React.useState('vector')
return ( return (
<FieldRoot name="storageType"> <FieldRoot name="retrievalIndex" className="w-80">
<FieldsetRoot <FieldsetRoot
render={( render={(
<RadioGroup value={value} onValueChange={setValue} className="flex-col items-start gap-3" /> <RadioGroup value={value} onValueChange={setValue} className="flex-col items-start gap-3" />
)} )}
> >
<FieldsetLegend>Storage type</FieldsetLegend> <FieldsetLegend>Retrieval index</FieldsetLegend>
<FieldItem> {[
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary"> { value: 'vector', label: 'Vector storage' },
<Radio {...args} value="ssd" /> { value: 'keyword', label: 'Keyword index' },
SSD { value: 'hybrid', label: 'Hybrid retrieval' },
</FieldLabel> ].map(option => (
</FieldItem> <FieldItem key={option.value}>
<FieldItem> <FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary"> <Radio value={option.value} />
<Radio {...args} value="hdd" /> {option.label}
HDD </FieldLabel>
</FieldLabel> </FieldItem>
</FieldItem> ))}
</FieldsetRoot> </FieldsetRoot>
</FieldRoot> </FieldRoot>
) )
} }
export const Default: Story = { export const StandardFormRows: Story = {
render: args => <RadioDemo {...args} />, render: () => <StandardFormRowsDemo />,
args: {
disabled: false,
},
parameters: { parameters: {
docs: { docs: {
description: { 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 = { function BooleanInlineDemo() {
args: { const [value, setValue] = React.useState(true)
value: 'checked',
}, return (
render: () => ( <FieldRoot name="streaming" className="w-80">
<FieldRoot name="disabledStates"> <FieldsetRoot
<FieldsetRoot render={<RadioGroup value="checked" className="flex-col items-start gap-3" />}> render={(
<FieldsetLegend>Disabled states</FieldsetLegend> <RadioGroup<boolean> value={value} onValueChange={setValue} className="gap-3" />
<FieldItem> )}
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary"> >
<Radio value="unchecked" disabled /> <FieldsetLegend>Streaming output</FieldsetLegend>
Disabled unchecked <div className="flex items-center gap-3">
</FieldLabel> <FieldItem>
</FieldItem> <FieldLabel className="flex items-center gap-1.5 system-sm-regular text-text-secondary">
<FieldItem> <Radio<boolean> value={true} />
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary"> True
<Radio value="checked" disabled /> </FieldLabel>
Disabled checked </FieldItem>
</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> </FieldsetRoot>
</FieldRoot> </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 = { export const StateMatrix: Story = {
args: {
value: 'checked',
},
render: () => ( render: () => (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<FieldRoot name="radioStates"> <FieldRoot name="radioStates">
<FieldsetRoot render={<RadioGroup value="checked" className="flex-col items-start gap-3" />}> <FieldsetRoot render={<RadioGroup defaultValue="checked" className="flex-col items-start gap-3" />}>
<FieldsetLegend>Radio states</FieldsetLegend> <FieldsetLegend>Interactive radio states</FieldsetLegend>
<FieldItem> <FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary"> <FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value="unchecked" /> <Radio value="unchecked" />
@ -123,15 +247,20 @@ export const StateMatrix: Story = {
Checked Checked
</FieldLabel> </FieldLabel>
</FieldItem> </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> <FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary"> <FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value="disabled-unchecked" disabled /> <Radio value="disabled-unchecked" />
Disabled unchecked Disabled unchecked
</FieldLabel> </FieldLabel>
</FieldItem> </FieldItem>
<FieldItem> <FieldItem>
<FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary"> <FieldLabel className="flex items-center gap-2 system-sm-medium text-text-secondary">
<Radio value="checked" disabled /> <Radio value="disabled-checked" />
Disabled checked Disabled checked
</FieldLabel> </FieldLabel>
</FieldItem> </FieldItem>
@ -146,7 +275,7 @@ export const StateMatrix: Story = {
parameters: { parameters: {
docs: { docs: {
description: { 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.',
}, },
}, },
}, },

View File

@ -1,68 +1,53 @@
'use client' 'use client'
import type { Radio as BaseRadioNS } from '@base-ui/react/radio' 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 type * as React from 'react'
import { Radio as BaseRadio } from '@base-ui/react/radio' import { Radio as BaseRadio } from '@base-ui/react/radio'
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group'
import { cn } from '../cn' import { cn } from '../cn'
const radioRootClassName = cn( export type RadioGroupProps<Value = string>
'inline-flex size-4 shrink-0 touch-manipulation items-center justify-center rounded-full p-0 transition-colors motion-reduce:transition-none', = Omit<BaseRadioGroupNS.Props<Value>, 'className'>
'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'>
& { & {
className?: string className?: string
variant?: 'control' | 'unstyled'
} }
export function RadioRoot<Value = string>({ export function RadioGroup<Value = string>({
className, className,
variant = 'control',
...props ...props
}: RadioRootProps<Value>) { }: RadioGroupProps<Value>) {
return ( return (
<BaseRadio.Root<Value> <BaseRadioGroup<Value>
className={cn(variant === 'control' && radioRootClassName, className)} className={cn('flex items-center gap-2', className)}
{...props} {...props}
/> />
) )
} }
export type RadioIndicatorProps export type RadioItemProps<Value = string>
= Omit<BaseRadioNS.Indicator.Props, 'className' | 'children'> = Omit<BaseRadioNS.Root.Props<Value>, 'className'>
& { & {
className?: string className?: string
} }
export function RadioIndicator({ export function RadioItem<Value = string>({
className, className,
...props ...props
}: RadioIndicatorProps) { }: RadioItemProps<Value>) {
return ( return (
<BaseRadio.Indicator <BaseRadio.Root<Value>
className={cn(radioIndicatorClassName, className)} className={className}
{...props} {...props}
/> />
) )
} }
export type RadioControlProps export type RadioControlProps
= Omit<RadioIndicatorProps, 'keepMounted'> = Omit<BaseRadioNS.Indicator.Props, 'className' | 'children' | 'keepMounted'>
& {
className?: string
}
export function RadioControl({ export function RadioControl({
className, className,
@ -70,18 +55,48 @@ export function RadioControl({
}: RadioControlProps) { }: RadioControlProps) {
return ( return (
<BaseRadio.Indicator <BaseRadio.Indicator
keepMounted
className={cn(radioControlClassName, className)}
{...props} {...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> export type RadioProps<Value = string>
= Omit<RadioRootProps<Value>, 'children'> = Omit<RadioItemProps<Value>, 'children'>
export function Radio<Value = string>(props: RadioProps<Value>) { export function Radio<Value = string>({
return <RadioRoot<Value> {...props} /> 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'> export type RadioSkeletonProps = React.ComponentProps<'div'>
@ -92,7 +107,7 @@ export function RadioSkeleton({
}: RadioSkeletonProps) { }: RadioSkeletonProps) {
return ( return (
<div <div
className={cn(radioSkeletonClassName, className)} className={cn('size-4 shrink-0 rounded-full bg-text-quaternary opacity-20', className)}
{...props} {...props}
/> />
) )

View File

@ -10,8 +10,7 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { FieldItem, FieldRoot } from '@langgenius/dify-ui/field' import { FieldItem, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset' import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio' import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Textarea } from '@langgenius/dify-ui/textarea' import { Textarea } from '@langgenius/dify-ui/textarea'
import { produce } from 'immer' import { produce } from 'immer'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
@ -162,9 +161,8 @@ const FollowUpSettingModal = ({
{t('feature.suggestedQuestionsAfterAnswer.modal.promptLabel', { ns: 'appDebug' })} {t('feature.suggestedQuestionsAfterAnswer.modal.promptLabel', { ns: 'appDebug' })}
</FieldsetLegend> </FieldsetLegend>
<FieldItem> <FieldItem>
<RadioRoot<PromptMode> <RadioItem<PromptMode>
value={PROMPT_MODE.default} value={PROMPT_MODE.default}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" />} render={<button type="button" />}
className={cn( className={cn(
@ -192,12 +190,11 @@ const FollowUpSettingModal = ({
</div> </div>
</div> </div>
)} )}
</RadioRoot> </RadioItem>
</FieldItem> </FieldItem>
<FieldItem> <FieldItem>
<RadioRoot<PromptMode> <RadioItem<PromptMode>
value={PROMPT_MODE.custom} value={PROMPT_MODE.custom}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" />} render={<button type="button" />}
className={cn( className={cn(
@ -228,7 +225,7 @@ const FollowUpSettingModal = ({
placeholder={t('feature.suggestedQuestionsAfterAnswer.modal.promptPlaceholder', { ns: 'appDebug' }) || ''} placeholder={t('feature.suggestedQuestionsAfterAnswer.modal.promptPlaceholder', { ns: 'appDebug' }) || ''}
/> />
)} )}
</RadioRoot> </RadioItem>
</FieldItem> </FieldItem>
</FieldsetRoot> </FieldsetRoot>
</FieldRoot> </FieldRoot>

View File

@ -3,8 +3,7 @@ import type { FieldState, FormSchema, TypeWithI18N } from '@/app/components/base
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset' import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { Radio } from '@langgenius/dify-ui/radio' import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { import {
Select, Select,
SelectContent, SelectContent,

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import type { NotionPageRow, NotionPageSelectionMode } from './types' 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 { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react' import { useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@ -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 { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'

View File

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite' 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 { RiDatabase2Line, RiFileList3Line, RiRocketLine } from '@remixicon/react'
import { useState } from 'react' import { useState } from 'react'
import RadioCard from '.' import RadioCard from '.'
@ -11,7 +11,7 @@ const meta = {
layout: 'centered', layout: 'centered',
docs: { docs: {
description: { 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.',
}, },
}, },
}, },

View File

@ -1,8 +1,8 @@
'use client' 'use client'
import type { RadioRootProps } from '@langgenius/dify-ui/radio' import type { RadioItemProps } from '@langgenius/dify-ui/radio'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn' 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 = { type BaseProps = {
className?: string className?: string
@ -16,7 +16,7 @@ type BaseProps = {
type SelectableRadioCardProps<Value = string> = BaseProps & { type SelectableRadioCardProps<Value = string> = BaseProps & {
noRadio?: false noRadio?: false
} & Omit<RadioRootProps<Value>, 'children' | 'className' | 'variant' | 'render' | 'nativeButton'> } & Omit<RadioItemProps<Value>, 'children' | 'className' | 'render' | 'nativeButton'>
type StaticRadioCardProps = BaseProps & { type StaticRadioCardProps = BaseProps & {
noRadio: true noRadio: true
@ -106,16 +106,15 @@ function RadioCard<Value = string>(props: Props<Value>) {
<div <div
className={rootClassName} className={rootClassName}
> >
<RadioRoot<Value> <RadioItem<Value>
{...radioRootProps} {...radioRootProps}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" />} 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" 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} {content}
<RadioControl className="absolute top-3 right-3" aria-hidden="true" /> <RadioControl className="absolute top-3 right-3" aria-hidden="true" />
</RadioRoot> </RadioItem>
{config} {config}
</div> </div>
) )

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import type { RetrievalConfig } from '@/types/app' import type { RetrievalConfig } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn' 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 { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react' import * as React from 'react'

View File

@ -5,7 +5,7 @@ import type { ParentChildConfig } from '../hooks'
import type { ParentMode, PreProcessingRule, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets' import type { ParentMode, PreProcessingRule, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox' 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 { RiSearchEyeLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'

View File

@ -1,6 +1,6 @@
import type { Mock } from 'vitest' import type { Mock } from 'vitest'
import type { OnlineDriveFile } from '@/models/pipeline' 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 { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react' import * as React from 'react'
import { OnlineDriveFileType } from '@/models/pipeline' import { OnlineDriveFileType } from '@/models/pipeline'

View File

@ -1,5 +1,5 @@
import type { OnlineDriveFile } from '@/models/pipeline' 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 { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import Item from '../item' import Item from '../item'

View File

@ -1,5 +1,5 @@
import type { OnlineDriveFile } from '@/models/pipeline' 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 { RiLoader2Line } from '@remixicon/react'
import * as React from 'react' import * as React from 'react'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'

View File

@ -1,5 +1,5 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets' 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 { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import CrawledResultItem from '../crawled-result-item' import CrawledResultItem from '../crawled-result-item'

View File

@ -1,5 +1,5 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets' 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 { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import * as React from 'react' import * as React from 'react'

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import type { CrawlResultItem } from '@/models/datasets' import type { CrawlResultItem } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn' 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 * as React from 'react'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@ -1,8 +1,7 @@
'use client' 'use client'
import type { AppCategory } from '@/models/explore' import type { AppCategory } from '@/models/explore'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { RadioRoot } from '@langgenius/dify-ui/radio' import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import exploreI18n from '@/i18n/en-US/explore.json' import exploreI18n from '@/i18n/en-US/explore.json'
@ -53,16 +52,15 @@ function Category({
isAll: false, isAll: false,
})), })),
].map(item => ( ].map(item => (
<RadioRoot <RadioItem
key={item.isAll ? 'all' : item.name} key={item.isAll ? 'all' : item.name}
value={item.name} value={item.name}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" />} 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" 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} {item.label}
</RadioRoot> </RadioItem>
))} ))}
</RadioGroup> </RadioGroup>
) )

View File

@ -15,8 +15,7 @@ import type {
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset' import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { Radio } from '@langgenius/dify-ui/radio' import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger } from '@langgenius/dify-ui/select' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger } from '@langgenius/dify-ui/select'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { Infotip } from '@/app/components/base/infotip' import { Infotip } from '@/app/components/base/infotip'

View File

@ -6,8 +6,7 @@ import type {
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset' import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { Radio } from '@langgenius/dify-ui/radio' import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select'
import { Slider } from '@langgenius/dify-ui/slider' import { Slider } from '@langgenius/dify-ui/slider'
import { Switch } from '@langgenius/dify-ui/switch' import { Switch } from '@langgenius/dify-ui/switch'

View File

@ -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 { dayjsToTimeOfDay } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
import type { PluginCategoryEnum } from '@/app/components/plugins/types' import type { PluginCategoryEnum } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn' 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 { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker' import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'

View File

@ -1,5 +1,5 @@
import { cn } from '@langgenius/dify-ui/cn' 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> = { type UpdateSettingOptionCardProps<Value extends string> = {
value: Value value: Value
@ -19,9 +19,8 @@ const UpdateSettingOptionCard = <Value extends string>({
onMouseLeave, onMouseLeave,
}: UpdateSettingOptionCardProps<Value>) => { }: UpdateSettingOptionCardProps<Value>) => {
return ( return (
<RadioRoot<Value> <RadioItem<Value>
value={value} value={value}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" />} render={<button type="button" />}
onBlur={onBlur} onBlur={onBlur}
@ -37,7 +36,7 @@ const UpdateSettingOptionCard = <Value extends string>({
)} )}
> >
<span className="max-w-full min-w-0 truncate whitespace-nowrap">{label}</span> <span className="max-w-full min-w-0 truncate whitespace-nowrap">{label}</span>
</RadioRoot> </RadioItem>
) )
} }

View File

@ -4,8 +4,7 @@ import type { Role } from '@/models/access-control'
import { Checkbox } from '@langgenius/dify-ui/checkbox' import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input' import { Input } from '@langgenius/dify-ui/input'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio' import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area' import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -288,14 +287,15 @@ const WorkspaceRoleCheckboxList = ({
return ( return (
<li key={role.id}> <li key={role.id}>
<RadioRoot <RadioItem
value={role.id} value={role.id}
disabled={disabled} disabled={disabled}
variant="unstyled" nativeButton
render={( render={(
<div <button
type="button"
className={cn( 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', checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent', disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
)} )}
@ -304,7 +304,7 @@ const WorkspaceRoleCheckboxList = ({
> >
<RadioControl className="pointer-events-none mt-0.5" /> <RadioControl className="pointer-events-none mt-0.5" />
{renderRoleText(role)} {renderRoleText(role)}
</RadioRoot> </RadioItem>
</li> </li>
) )
})} })}

View File

@ -2,8 +2,7 @@
import type { Permissions } from '@/app/components/plugins/types' import type { Permissions } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { RadioRoot } from '@langgenius/dify-ui/radio' import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PluginSidecarPanel } from '@/app/components/plugins/plugin-page/plugin-sidecar-panel' import { PluginSidecarPanel } from '@/app/components/plugins/plugin-page/plugin-sidecar-panel'
import { PermissionType } from '@/app/components/plugins/types' import { PermissionType } from '@/app/components/plugins/types'
@ -71,16 +70,15 @@ export function PermissionQuickPanel({
const optionLabel = t(`privilege.${option}`, { ns: 'plugin' }) const optionLabel = t(`privilege.${option}`, { ns: 'plugin' })
return ( return (
<RadioRoot<PermissionType> <RadioItem<PermissionType>
key={option} key={option}
value={option} value={option}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" className={permissionOptionCardClassName} />} render={<button type="button" className={permissionOptionCardClassName} />}
aria-label={`${row.label}: ${optionLabel}`} aria-label={`${row.label}: ${optionLabel}`}
> >
<span className="min-w-0 truncate">{optionLabel}</span> <span className="min-w-0 truncate">{optionLabel}</span>
</RadioRoot> </RadioItem>
) )
})} })}
</RadioGroup> </RadioGroup>

View File

@ -15,8 +15,7 @@ import {
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset' import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { Input } from '@langgenius/dify-ui/input' import { Input } from '@langgenius/dify-ui/input'
import { Radio } from '@langgenius/dify-ui/radio' import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area' import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@ -1,7 +1,7 @@
import type { ComponentType, SVGProps } from 'react' import type { ComponentType, SVGProps } from 'react'
import { FieldRoot } from '@langgenius/dify-ui/field' import { FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset' import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioGroup } from '@langgenius/dify-ui/radio-group' import { RadioGroup } from '@langgenius/dify-ui/radio'
import { import {
fireEvent, fireEvent,
render, render,

View File

@ -11,7 +11,7 @@ import type {
} from './top-k-and-score-threshold' } from './top-k-and-score-threshold'
import { FieldRoot } from '@langgenius/dify-ui/field' import { FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset' import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioGroup } from '@langgenius/dify-ui/radio-group' import { RadioGroup } from '@langgenius/dify-ui/radio'
import { import {
memo, memo,
} from 'react' } from 'react'

View File

@ -14,8 +14,7 @@ import type {
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset' import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio' import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Switch } from '@langgenius/dify-ui/switch' import { Switch } from '@langgenius/dify-ui/switch'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import WeightedScoreComponent from '@/app/components/app/configuration/dataset-config/params-config/weighted-score' import WeightedScoreComponent from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
@ -150,9 +149,8 @@ function SearchMethodRadioCard({
readonly && 'cursor-not-allowed', readonly && 'cursor-not-allowed',
)} )}
> >
<RadioRoot<RetrievalSearchMethodEnum> <RadioItem<RetrievalSearchMethodEnum>
value={option.id} value={option.id}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" />} render={<button type="button" />}
disabled={readonly} disabled={readonly}
@ -186,7 +184,7 @@ function SearchMethodRadioCard({
) )
: null} : null}
</div> </div>
</RadioRoot> </RadioItem>
{!!(children && isActive) && ( {!!(children && isActive) && (
<div className="relative rounded-b-xl bg-components-panel-bg p-3"> <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" /> <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 ( return (
<FieldItem> <FieldItem>
<RadioRoot<HybridSearchModeEnum> <RadioItem<HybridSearchModeEnum>
value={option.id} value={option.id}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" />} render={<button type="button" />}
disabled={readonly} disabled={readonly}
@ -230,7 +227,7 @@ function HybridSearchModeRadioCard({
</div> </div>
<RadioControl className="mt-0.5" aria-hidden="true" /> <RadioControl className="mt-0.5" aria-hidden="true" />
</div> </div>
</RadioRoot> </RadioItem>
</FieldItem> </FieldItem>
) )
} }

View File

@ -14,8 +14,7 @@ import type { DataSet, MetadataInDoc } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Input } from '@langgenius/dify-ui/input' import { Input } from '@langgenius/dify-ui/input'
import { RadioRoot } from '@langgenius/dify-ui/radio' import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Textarea } from '@langgenius/dify-ui/textarea' import { Textarea } from '@langgenius/dify-ui/textarea'
import { intersectionBy } from 'es-toolkit/compat' import { intersectionBy } from 'es-toolkit/compat'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
@ -500,17 +499,16 @@ export function AgentKnowledgeRetrievalDialog({
}} }}
> >
{queryModeOptions.map(mode => ( {queryModeOptions.map(mode => (
<RadioRoot<KnowledgeRetrievalQueryMode> <RadioItem<KnowledgeRetrievalQueryMode>
key={mode} key={mode}
value={mode} value={mode}
variant="unstyled"
nativeButton nativeButton
render={<button type="button" className={optionCardClassName} />} render={<button type="button" className={optionCardClassName} />}
> >
<span className="min-w-0 truncate"> <span className="min-w-0 truncate">
{t(`agentDetail.configure.knowledgeRetrieval.dialog.query.${mode}`)} {t(`agentDetail.configure.knowledgeRetrieval.dialog.query.${mode}`)}
</span> </span>
</RadioRoot> </RadioItem>
))} ))}
</RadioGroup> </RadioGroup>
{queryMode === 'custom' {queryMode === 'custom'

View File

@ -4,8 +4,7 @@ import type { GuideMethod, WorkflowSourceApp } from '@/features/deployments/crea
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input' import { Input } from '@langgenius/dify-ui/input'
import { RadioRoot } from '@langgenius/dify-ui/radio' import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader' import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
@ -103,9 +102,10 @@ function SourceMethodCard({ value, icon, title, description, badge }: {
badge?: string badge?: string
}) { }) {
return ( return (
<RadioRoot<GuideMethod> <RadioItem<GuideMethod>
value={value} value={value}
variant="unstyled" nativeButton
render={<button type="button" />}
className={cn( className={cn(
`relative box-content h-[84px] w-full cursor-pointer rounded-xl border-[0.5px] `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 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> </span>
</TitleTooltip> </TitleTooltip>
</span> </span>
</RadioRoot> </RadioItem>
) )
} }

View File

@ -3,8 +3,7 @@
import type { Environment } from '@dify/contracts/enterprise/types.gen' import type { Environment } from '@dify/contracts/enterprise/types.gen'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio' import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next' 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}`)}` const summary = environment.description.trim() || `${t(`mode.${environment.mode}`)} · ${t(`backend.${environment.backend}`)}`
return ( return (
<RadioRoot<string> <RadioItem<string>
value={environment.id} value={environment.id}
variant="unstyled" nativeButton
render={<button type="button" />}
className={cn( className={cn(
'group flex cursor-pointer items-center gap-3 rounded-xl border p-3 outline-hidden', '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', '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> </span>
</TitleTooltip> </TitleTooltip>
</span> </span>
</RadioRoot> </RadioItem>
) )
} }

View File

@ -12,8 +12,7 @@ import {
DialogDescription, DialogDescription,
DialogTitle, DialogTitle,
} from '@langgenius/dify-ui/dialog' } from '@langgenius/dify-ui/dialog'
import { RadioRoot } from '@langgenius/dify-ui/radio' import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AccessMode as AppAccessMode } from '@/models/access-control' import { AccessMode as AppAccessMode } from '@/models/access-control'
@ -166,9 +165,8 @@ function AccessControlItem({ type, children }: PropsWithChildren<{
type: AppAccessMode type: AppAccessMode
}>) { }>) {
return ( return (
<RadioRoot<AppAccessMode> <RadioItem<AppAccessMode>
value={type} value={type}
variant="unstyled"
render={<div />} render={<div />}
className={cn( 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', '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} {children}
</RadioRoot> </RadioItem>
) )
} }