docs(dify-ui): clarify radio composition stories (#38456)

This commit is contained in:
yyh 2026-07-06 14:40:17 +08:00 committed by GitHub
parent d9c99daf29
commit 8208b786ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 106 additions and 28 deletions

View File

@ -8,7 +8,7 @@ import {
FieldRoot,
} from '../field'
import { FieldsetLegend, FieldsetRoot } from '../fieldset'
import { Radio, RadioControl, RadioRoot } from '../radio'
import { Radio, RadioControl, RadioIndicator, RadioRoot } from '../radio'
const meta = {
title: 'Base/Form/RadioGroup',
@ -17,7 +17,7 @@ const meta = {
layout: 'centered',
docs: {
description: {
component: 'RadioGroup primitive built on Base UI. For normal form rows, compose FieldRoot, FieldsetRoot, FieldLabel, RadioGroup, and Radio. For option cards, wrap each option in FieldItem and make the card itself a RadioRoot with variant="unstyled".',
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.',
},
},
},
@ -60,7 +60,7 @@ export const StandardFormRows: Story = {
parameters: {
docs: {
description: {
story: 'Default form composition. Most product code should use this shape: RadioGroup owns value, FieldsetLegend names the group, and FieldLabel makes each row clickable.',
story: 'Plain form-row composition. `RadioGroup` owns value, `FieldsetLegend` names the group, `FieldLabel` makes each option label clickable, and `Radio` renders the default dot.',
},
},
},
@ -107,31 +107,39 @@ export const BooleanInline: Story = {
},
}
type PromptMode = 'default' | 'custom'
function OptionCardsDemo() {
const [value, setValue] = React.useState('default')
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 value={value} onValueChange={setValue} className="flex-col items-stretch gap-3" />
<RadioGroup<PromptMode> value={value} onValueChange={setValue} className="flex-col items-stretch gap-3" />
)}
>
<FieldsetLegend>Prompt mode</FieldsetLegend>
{[
{
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.',
},
].map(option => (
{options.map(option => (
<FieldItem key={option.value}>
<RadioRoot
<RadioRoot<PromptMode>
value={option.value}
variant="unstyled"
nativeButton
@ -162,7 +170,76 @@ export const OptionCards: Story = {
parameters: {
docs: {
description: {
story: 'Wrap each option card in FieldItem, then use RadioRoot with variant="unstyled" when the entire card is the radio. RadioControl renders the visual dot inside the card.',
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.',
},
},
},

View File

@ -15,7 +15,7 @@ const meta = {
layout: 'centered',
docs: {
description: {
component: 'Radio primitive built on Base UI. It preserves RadioGroup selection, hidden input, disabled, and form semantics while applying the Dify 16px radio design from Figma. Import from `@langgenius/dify-ui/radio` and place radios inside `RadioGroup` from `@langgenius/dify-ui/radio-group`.',
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.',
},
},
},
@ -68,6 +68,13 @@ export const Default: Story = {
args: {
disabled: false,
},
parameters: {
docs: {
description: {
story: '`Radio` renders the standard visual control. `FieldLabel` owns the clickable text label, and `RadioGroup` owns the selected value.',
},
},
},
}
export const Disabled: Story = {

View File

@ -80,17 +80,11 @@ export function RadioControl({
export type RadioProps<Value = string>
= Omit<RadioRootProps<Value>, 'children'>
export function Radio<Value = string>({
...props
}: RadioProps<Value>) {
export function Radio<Value = string>(props: RadioProps<Value>) {
return <RadioRoot<Value> {...props} />
}
export type RadioSkeletonProps
= Omit<React.ComponentProps<'div'>, 'className'>
& {
className?: string
}
export type RadioSkeletonProps = React.ComponentProps<'div'>
export function RadioSkeleton({
className,