From bdb3469ca01fd713bad6cbb5c67d2cf52b50774e Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:27:49 +0800 Subject: [PATCH] refactor(dify-ui): consolidate radio family API (#38479) --- packages/dify-ui/README.md | 35 ++- packages/dify-ui/package.json | 4 - .../src/radio-group/__tests__/index.spec.tsx | 82 ----- .../dify-ui/src/radio-group/index.stories.tsx | 295 ------------------ packages/dify-ui/src/radio-group/index.tsx | 23 -- .../src/radio/__tests__/index.spec.tsx | 97 +++--- packages/dify-ui/src/radio/index.stories.tsx | 261 ++++++++++++---- packages/dify-ui/src/radio/index.tsx | 93 +++--- .../follow-up-setting-modal.tsx | 13 +- .../base/form/components/base/base-field.tsx | 3 +- .../page-selector/virtual-page-list.tsx | 2 +- .../base/radio-card/__tests__/index.spec.tsx | 2 +- .../base/radio-card/index.stories.tsx | 4 +- web/app/components/base/radio-card/index.tsx | 11 +- .../common/retrieval-param-config/index.tsx | 2 +- .../components/parent-child-options.tsx | 2 +- .../file-list/list/__tests__/index.spec.tsx | 2 +- .../file-list/list/__tests__/item.spec.tsx | 2 +- .../online-drive/file-list/list/index.tsx | 2 +- .../__tests__/crawled-result-item.spec.tsx | 2 +- .../base/__tests__/index.spec.tsx | 2 +- .../website-crawl/base/crawled-result.tsx | 2 +- web/app/components/explore/category.tsx | 8 +- .../model-provider-page/model-modal/Form.tsx | 3 +- .../model-parameter-modal/parameter-item.tsx | 3 +- .../update-setting-dialog-form.tsx | 2 +- .../update-setting-option-card.tsx | 7 +- .../workspace-role-checkbox-list.tsx | 14 +- .../integrations/permission-quick-panel.tsx | 8 +- .../config-credentials.tsx | 3 +- .../__tests__/search-method-option.spec.tsx | 2 +- .../components/retrieval-setting/index.tsx | 2 +- .../search-method-option.tsx | 13 +- .../orchestrate/knowledge/dialog.tsx | 8 +- .../create-guide/ui/source-step.tsx | 10 +- .../create-guide/ui/target-step.tsx | 10 +- .../permissions/access-control-dialog.tsx | 8 +- 37 files changed, 394 insertions(+), 648 deletions(-) delete mode 100644 packages/dify-ui/src/radio-group/__tests__/index.spec.tsx delete mode 100644 packages/dify-ui/src/radio-group/index.stories.tsx delete mode 100644 packages/dify-ui/src/radio-group/index.tsx diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index 6e805f46a50..732e62083b0 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -43,18 +43,18 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported ## Primitives -| Category | Subpath | Notes | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | -| Actions | `./button` | Design-system CTA primitive with `cva` variants. | -| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. | -| Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. | -| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. | -| Form | `./form`, `./field`, `./fieldset`, `./input`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. | -| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. | -| Media | `./avatar` | Avatar root, image, and fallback primitives. | -| Navigation | `./file-tree`, `./pagination`, `./tabs` | FileTree for preview-oriented file disclosure lists; Pagination for page navigation; Tabs for panels. | -| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. | -| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. | +| Category | Subpath | Notes | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Actions | `./button` | Design-system CTA primitive with `cva` variants. | +| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. | +| Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. | +| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. | +| Form | `./form`, `./field`, `./fieldset`, `./input`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. | +| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. | +| Media | `./avatar` | Avatar root, image, and fallback primitives. | +| Navigation | `./file-tree`, `./pagination`, `./tabs` | FileTree for preview-oriented file disclosure lists; Pagination for page navigation; Tabs for panels. | +| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. | +| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. | Utilities: @@ -103,17 +103,22 @@ Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group ## Typed value contracts -Selection primitives should preserve the caller's domain value type instead of widening values to `string`. Use `Select`, `RadioGroup`, `Radio`, and `RadioRoot` 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`, `RadioGroup`, `Radio`, and `RadioItem` when the selected value is an enum, union, boolean, number, object, or nullable placeholder value. Root-level generics type `value`, `defaultValue`, `onValueChange`, and collection props such as `items`, but JSX children do not automatically inherit the parent generic. For non-string radio groups, type the child item too: ```tsx value={promptMode} onValueChange={setPromptMode}> - value={PROMPT_MODE.default} /> - value={PROMPT_MODE.custom} /> + value={PROMPT_MODE.default} /> + value={PROMPT_MODE.custom}> + + Custom prompt + ``` +Use `Radio` for the default Dify control. Use `RadioItem` when custom UI should be the radio item; place `RadioControl` inside it for the standard visual dot. `RadioControl` is a Dify visual part, not a Base UI anatomy export. + For select labels and display values, prefer the Base UI `items` collection pattern so the root, value display, and item list share the same typed values. Avoid helpers that stringify values only to recover labels later; convert values to strings only at real boundaries such as form submission, URL/search params, or legacy APIs that require strings. `CheckboxGroup` follows the Base UI contract and uses `string[]`. Do not add a generic checkbox-group wrapper unless the underlying primitive contract changes; if different business IDs need stronger separation, model that at the feature/domain type boundary. diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json index 9a05be1fc3c..84403f25a36 100644 --- a/packages/dify-ui/package.json +++ b/packages/dify-ui/package.json @@ -101,10 +101,6 @@ "types": "./src/radio/index.tsx", "import": "./src/radio/index.tsx" }, - "./radio-group": { - "types": "./src/radio-group/index.tsx", - "import": "./src/radio-group/index.tsx" - }, "./popover": { "types": "./src/popover/index.tsx", "import": "./src/popover/index.tsx" diff --git a/packages/dify-ui/src/radio-group/__tests__/index.spec.tsx b/packages/dify-ui/src/radio-group/__tests__/index.spec.tsx deleted file mode 100644 index dba0c1ae5e6..00000000000 --- a/packages/dify-ui/src/radio-group/__tests__/index.spec.tsx +++ /dev/null @@ -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 ( - - }> - Storage type - - - - SSD - - - - - - HDD - - - - - ) - } - - const screen = await render() - - 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( - - }> - Storage type - - - - SSD - - - - - - HDD - - - - , - ) - - 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') - }) -}) diff --git a/packages/dify-ui/src/radio-group/index.stories.tsx b/packages/dify-ui/src/radio-group/index.stories.tsx deleted file mode 100644 index be005f0491e..00000000000 --- a/packages/dify-ui/src/radio-group/index.stories.tsx +++ /dev/null @@ -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 - -export default meta -type Story = StoryObj - -function StandardFormRowsDemo() { - const [value, setValue] = React.useState('vector') - - return ( - - - )} - > - Retrieval index - {[ - { value: 'vector', label: 'Vector storage' }, - { value: 'keyword', label: 'Keyword index' }, - { value: 'hybrid', label: 'Hybrid retrieval' }, - ].map(option => ( - - - - {option.label} - - - ))} - - - ) -} - -export const StandardFormRows: Story = { - render: () => , - 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 ( - - value={value} onValueChange={setValue} className="gap-3" /> - )} - > - Streaming output -
- - - value={true} /> - True - - - - - value={false} /> - False - - -
-
-
- ) -} - -export const BooleanInline: Story = { - render: () => , - 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('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 ( - - value={value} onValueChange={setValue} className="flex-col items-stretch gap-3" /> - )} - > - Prompt mode - {options.map(option => ( - - - value={option.value} - variant="unstyled" - nativeButton - render={