refactor(web): replace Button destructive boolean with tone semantic axis (#35176)

This commit is contained in:
yyh 2026-04-14 22:16:39 +08:00 committed by GitHub
parent 648dde5e96
commit ebf741114d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 73 additions and 59 deletions

View File

@ -159,7 +159,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" destructive className="w-full" onClick={handleDeleteAvatar}>
<Button variant="primary" tone="destructive" className="w-full" onClick={handleDeleteAvatar}>
{t('operation.delete', { ns: 'common' })}
</Button>
</div>

View File

@ -53,7 +53,7 @@ export default function VerifyEmail(props: DeleteAccountProps) {
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="primary" destructive onClick={handleConfirm}>{t('account.permanentlyDeleteButton', { ns: 'common' })}</Button>
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="primary" tone="destructive" onClick={handleConfirm}>{t('account.permanentlyDeleteButton', { ns: 'common' })}</Button>
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
<Countdown onResend={sendEmail} />
</div>

View File

@ -165,10 +165,10 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton destructive={false}>
<AlertDialogCancelButton tone="default">
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton variant="primary" destructive={false} onClick={onConfirmUseGPT4}>
<AlertDialogConfirmButton variant="primary" tone="default" onClick={onConfirmUseGPT4}>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>

View File

@ -43,7 +43,7 @@ const DSLConfirmModal = ({
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" destructive onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
)

View File

@ -322,7 +322,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={() => setShowErrorModal(false)}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" destructive onClick={onDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
</>

View File

@ -152,7 +152,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
</div>
<div className="flex items-center">
<Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button className="border-red-700" disabled={isAppsFull || !name} variant="primary" destructive onClick={goStart}>{t('switchStart', { ns: 'app' })}</Button>
<Button className="border-red-700" disabled={isAppsFull || !name} variant="primary" tone="destructive" onClick={goStart}>{t('switchStart', { ns: 'app' })}</Button>
</div>
</div>
</Modal>

View File

@ -152,7 +152,7 @@ function Confirm({
</div>
<div className="flex items-start justify-end gap-2 self-stretch p-6">
{showCancel && <Button onClick={onCancel}>{cancelTxt}</Button>}
{showConfirm && <Button variant="primary" destructive={type !== 'info'} loading={isLoading} disabled={isConfirmDisabled} onClick={onConfirm}>{confirmTxt}</Button>}
{showConfirm && <Button variant="primary" tone={type !== 'info' ? 'destructive' : 'default'} loading={isLoading} disabled={isConfirmDisabled} onClick={onConfirm}>{confirmTxt}</Button>}
</div>
</div>
</div>

View File

@ -93,7 +93,7 @@ describe('InlineDeleteConfirm', () => {
)
const confirmButton = getByText('Yes').closest('button')
expect(confirmButton?.className).toContain('btn-destructive')
expect(confirmButton?.className).toContain('btn-destructive-primary')
})
it('should render without destructive class for warning variant', () => {
@ -108,7 +108,7 @@ describe('InlineDeleteConfirm', () => {
)
const confirmButton = getByText('Yes').closest('button')
expect(confirmButton?.className).not.toContain('btn-destructive')
expect(confirmButton?.className).not.toContain('btn-destructive-primary')
})
it('should render without destructive class for info variant', () => {
@ -123,7 +123,7 @@ describe('InlineDeleteConfirm', () => {
)
const confirmButton = getByText('Yes').closest('button')
expect(confirmButton?.className).not.toContain('btn-destructive')
expect(confirmButton?.className).not.toContain('btn-destructive-primary')
})
})

View File

@ -62,7 +62,7 @@ const InlineDeleteConfirm: FC<InlineDeleteConfirmProps> = ({
<Button
size="small"
variant="primary"
destructive={variant === 'delete'}
tone={variant === 'delete' ? 'destructive' : 'default'}
onClick={onConfirm}
aria-label={confirmTxt}
className="flex-1"

View File

@ -39,7 +39,7 @@ const TagRemoveModal = ({ show, tag, onConfirm, onClose }: TagRemoveModalProps)
</div>
<div className="flex items-center justify-end pt-6">
<Button className="mr-2" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button className="border-red-700" variant="primary" destructive onClick={onConfirm}>{t('operation.delete', { ns: 'common' })}</Button>
<Button className="border-red-700" variant="primary" tone="destructive" onClick={onConfirm}>{t('operation.delete', { ns: 'common' })}</Button>
</div>
</Modal>
)

View File

@ -110,7 +110,7 @@ describe('AlertDialog wrapper', () => {
expect(screen.getByTestId('actions')).toHaveClass('flex', 'items-start', 'justify-end', 'gap-2', 'self-stretch', 'p-6', 'custom-actions')
const confirmButton = screen.getByRole('button', { name: 'Confirm' })
expect(confirmButton).toHaveClass('btn-primary')
expect(confirmButton).toHaveClass('btn-destructive')
expect(confirmButton).toHaveClass('btn-destructive-primary')
})
it('should keep dialog open after confirm click and close via cancel helper', async () => {

View File

@ -86,13 +86,13 @@ type AlertDialogConfirmButtonProps = ButtonProps
export function AlertDialogConfirmButton({
variant = 'primary',
destructive = true,
tone = 'destructive',
...props
}: AlertDialogConfirmButtonProps) {
return (
<Button
variant={variant}
destructive={destructive}
tone={tone}
{...props}
/>
)

View File

@ -51,9 +51,14 @@ describe('Button', () => {
expect(screen.getByRole('button').className).toContain(`btn-${variant}`)
})
it('applies destructive modifier', () => {
render(<Button destructive>Click me</Button>)
expect(screen.getByRole('button').className).toContain('btn-destructive')
it('applies destructive tone with default variant', () => {
render(<Button tone="destructive">Click me</Button>)
expect(screen.getByRole('button').className).toContain('btn-destructive-secondary')
})
it('applies destructive tone with primary variant', () => {
render(<Button variant="primary" tone="destructive">Click me</Button>)
expect(screen.getByRole('button').className).toContain('btn-destructive-primary')
})
})

View File

@ -98,53 +98,51 @@
}
}
@utility btn-destructive {
&.btn-primary {
@apply bg-components-button-destructive-primary-bg
@utility btn-destructive-primary {
@apply bg-components-button-destructive-primary-bg
border-components-button-destructive-primary-border
hover:bg-components-button-destructive-primary-bg-hover
hover:border-components-button-destructive-primary-border-hover
text-components-button-destructive-primary-text;
}
&.btn-primary:is(:disabled, [data-disabled]) {
&:is(:disabled, [data-disabled]) {
@apply shadow-none
bg-components-button-destructive-primary-bg-disabled
border-components-button-destructive-primary-border-disabled
text-components-button-destructive-primary-text-disabled;
}
}
&.btn-secondary {
@apply bg-components-button-destructive-secondary-bg
@utility btn-destructive-secondary {
@apply bg-components-button-destructive-secondary-bg
border-components-button-destructive-secondary-border
hover:bg-components-button-destructive-secondary-bg-hover
hover:border-components-button-destructive-secondary-border-hover
text-components-button-destructive-secondary-text;
}
&.btn-secondary:is(:disabled, [data-disabled]) {
&:is(:disabled, [data-disabled]) {
@apply bg-components-button-destructive-secondary-bg-disabled
border-components-button-destructive-secondary-border-disabled
text-components-button-destructive-secondary-text-disabled;
}
}
&.btn-tertiary {
@apply bg-components-button-destructive-tertiary-bg
@utility btn-destructive-tertiary {
@apply bg-components-button-destructive-tertiary-bg
hover:bg-components-button-destructive-tertiary-bg-hover
text-components-button-destructive-tertiary-text;
}
&.btn-tertiary:is(:disabled, [data-disabled]) {
&:is(:disabled, [data-disabled]) {
@apply bg-components-button-destructive-tertiary-bg-disabled
text-components-button-destructive-tertiary-text-disabled;
}
}
&.btn-ghost {
@apply hover:bg-components-button-destructive-ghost-bg-hover
@utility btn-destructive-ghost {
@apply hover:bg-components-button-destructive-ghost-bg-hover
text-components-button-destructive-ghost-text;
}
&.btn-ghost:is(:disabled, [data-disabled]) {
&:is(:disabled, [data-disabled]) {
@apply text-components-button-destructive-ghost-text-disabled;
}
}

View File

@ -11,7 +11,10 @@ const meta = {
tags: ['autodocs'],
argTypes: {
loading: { control: 'boolean' },
destructive: { control: 'boolean' },
tone: {
control: 'select',
options: ['default', 'destructive'],
},
disabled: { control: 'boolean' },
variant: {
control: 'select',
@ -92,7 +95,7 @@ export const Loading: Story = {
export const Destructive: Story = {
args: {
variant: 'primary',
destructive: true,
tone: 'destructive',
children: 'Delete',
},
}

View File

@ -21,13 +21,21 @@ const buttonVariants = cva(
medium: 'btn-medium',
large: 'btn-large',
},
destructive: {
true: 'btn-destructive',
tone: {
default: '',
destructive: '',
},
},
compoundVariants: [
{ variant: 'primary', tone: 'destructive', class: 'btn-destructive-primary' },
{ variant: 'secondary', tone: 'destructive', class: 'btn-destructive-secondary' },
{ variant: 'tertiary', tone: 'destructive', class: 'btn-destructive-tertiary' },
{ variant: 'ghost', tone: 'destructive', class: 'btn-destructive-ghost' },
],
defaultVariants: {
variant: 'secondary',
size: 'medium',
tone: 'default',
},
},
)
@ -43,7 +51,7 @@ export function Button({
className,
variant,
size,
destructive,
tone,
loading,
disabled,
type = 'button',
@ -53,7 +61,7 @@ export function Button({
return (
<BaseButton
type={type}
className={cn(buttonVariants({ variant, size, destructive, className }))}
className={cn(buttonVariants({ variant, size, tone, className }))}
disabled={disabled || loading}
aria-busy={loading || undefined}
{...props}

View File

@ -43,7 +43,7 @@ const DSLConfirmModal = ({
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" destructive onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
)

View File

@ -130,7 +130,7 @@ const BatchAction: FC<IBatchActionProps> = ({
)}
<Button
variant="ghost"
destructive
tone="destructive"
className="gap-x-0.5 px-3"
onClick={showDeleteConfirm}
>

View File

@ -30,7 +30,7 @@ const DefaultContent: FC<IDefaultContentProps> = React.memo(({
<Button onClick={onCancel}>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" destructive onClick={onConfirm}>
<Button variant="primary" tone="destructive" onClick={onConfirm}>
{t('operation.regenerate', { ns: 'common' })}
</Button>
</div>
@ -50,7 +50,7 @@ const RegeneratingContent: FC = React.memo(() => {
<p className="system-md-regular text-text-secondary">{t('segment.regeneratingMessage', { ns: 'datasetDocuments' })}</p>
</div>
<div className="flex justify-end pt-6">
<Button variant="primary" destructive disabled className="inline-flex items-center gap-x-0.5">
<Button variant="primary" tone="destructive" disabled className="inline-flex items-center gap-x-0.5">
<RiLoader2Line className="h-4 w-4 animate-spin text-components-button-destructive-primary-text-disabled" />
<span>{t('operation.regenerate', { ns: 'common' })}</span>
</Button>

View File

@ -173,7 +173,7 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
<MemberSelector exclude={[userProfile.id]} value={newOwner} onSelect={setNewOwner} />
</div>
<div className="mt-4 space-y-2">
<Button data-testid="transfer-modal-submit" disabled={!newOwner || isTransfer} className="w-full!" variant="primary" destructive onClick={handleTransfer}>
<Button data-testid="transfer-modal-submit" disabled={!newOwner || isTransfer} className="w-full!" variant="primary" tone="destructive" onClick={handleTransfer}>
{t('members.transferModal.transfer', { ns: 'common' })}
</Button>
<Button data-testid="transfer-modal-cancel" className="w-full!" onClick={onClose}>

View File

@ -387,7 +387,7 @@ const ModelModal: FC<ModelModalProps> = ({
isEditMode && (
<Button
variant="primary"
destructive
tone="destructive"
onClick={() => openConfirmDelete(credential, model)}
>
{t('operation.remove', { ns: 'common' })}

View File

@ -25,7 +25,7 @@ const DowngradeWarningModal = ({
</div>
<div className="mt-9 flex items-start justify-end space-x-2 self-stretch">
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="secondary" destructive onClick={onJustDowngrade}>{t(`${i18nPrefix}.downgrade`, { ns: 'plugin' })}</Button>
<Button variant="secondary" tone="destructive" onClick={onJustDowngrade}>{t(`${i18nPrefix}.downgrade`, { ns: 'plugin' })}</Button>
<Button variant="primary" onClick={onExcludeAndDowngrade}>{t(`${i18nPrefix}.exclude`, { ns: 'plugin' })}</Button>
</div>
</>

View File

@ -101,7 +101,7 @@ describe('VersionMismatchModal', () => {
const confirmBtn = screen.getByRole('button', { name: /app\.newApp\.Confirm/ })
expect(confirmBtn).toHaveClass('btn-primary')
expect(confirmBtn).toHaveClass('btn-destructive')
expect(confirmBtn).toHaveClass('btn-destructive-primary')
})
})

View File

@ -91,7 +91,7 @@ const UpdateDSLModal = ({
<Button
disabled={!currentFile || loading}
variant="primary"
destructive
tone="destructive"
onClick={handleImport}
loading={loading}
>

View File

@ -45,7 +45,7 @@ const VersionMismatchModal = ({
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" destructive onClick={onConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
)

View File

@ -344,7 +344,7 @@ const EditCustomCollectionModal: FC<Props> = ({
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 flex shrink-0 rounded-b-[10px] border-t border-divider-regular bg-background-section-burn px-6 py-4')}>
{
isEdit && (
<Button variant="primary" destructive onClick={onRemove}>{t('operation.delete', { ns: 'common' })}</Button>
<Button variant="primary" tone="destructive" onClick={onRemove}>{t('operation.delete', { ns: 'common' })}</Button>
)
}
<div className="flex space-x-2">

View File

@ -165,7 +165,7 @@ describe('ConfirmModal', () => {
// Assert
const confirmButton = screen.getByText('common.operation.confirm')
expect(confirmButton).toHaveClass('btn-primary')
expect(confirmButton).toHaveClass('btn-destructive')
expect(confirmButton).toHaveClass('btn-destructive-primary')
})
})

View File

@ -36,7 +36,7 @@ const ConfirmModal = ({ show, onConfirm, onClose }: ConfirmModalProps) => {
<div className="flex items-center justify-end pt-6">
<div className="flex items-center">
<Button className="mr-2" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button variant="primary" destructive onClick={onConfirm}>{t('operation.confirm', { ns: 'common' })}</Button>
<Button variant="primary" tone="destructive" onClick={onConfirm}>{t('operation.confirm', { ns: 'common' })}</Button>
</div>
</div>
</Modal>

View File

@ -318,7 +318,7 @@ const WorkflowToolAsModal: FC<Props> = ({
</div>
<div className={cn((!isAdd && onRemove) ? 'justify-between' : 'justify-end', 'mt-2 flex shrink-0 rounded-b-[10px] border-t border-divider-regular bg-background-section-burn px-6 py-4')}>
{!isAdd && onRemove && (
<Button variant="primary" destructive onClick={onRemove}>{t('operation.delete', { ns: 'common' })}</Button>
<Button variant="primary" tone="destructive" onClick={onRemove}>{t('operation.delete', { ns: 'common' })}</Button>
)}
<div className="flex space-x-2">
<Button onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>

View File

@ -34,7 +34,7 @@ const DeleteConfirmModal: FC<DeleteConfirmModalProps> = ({
<Button onClick={onClose}>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" destructive onClick={onDelete.bind(null, versionInfo.id)}>
<Button variant="primary" tone="destructive" onClick={onDelete.bind(null, versionInfo.id)}>
{t('operation.delete', { ns: 'common' })}
</Button>
</div>

View File

@ -247,7 +247,7 @@ const UpdateDSLModal = ({
<Button
disabled={!currentFile || loading}
variant="primary"
destructive
tone="destructive"
onClick={handleImport}
loading={loading}
>
@ -278,7 +278,7 @@ const UpdateDSLModal = ({
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={() => setShowErrorModal(false)}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" destructive onClick={onUpdateDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onUpdateDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
</>