mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
refactor(dify-ui): consolidate radio family API (#38479)
This commit is contained in:
parent
c47663d77b
commit
bdb3469ca0
@ -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.
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 { 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>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user