mirror of
https://github.com/langgenius/dify.git
synced 2026-07-18 15:54:33 -04:00
refactor(dify-ui): consolidate radio family API (#38479)
This commit is contained in:
+20
-15
@@ -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.
|
||||
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
+1
-2
@@ -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
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
+5
-8
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+3
-5
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user