feat(confirm-dialog): add ConfirmDialog component with comprehensive Storybook examples

- Introduced a new ConfirmDialog component for confirmation scenarios, integrating Dialog and Button primitives.
- Added props for customizable titles, descriptions, and button texts, including support for loading states and destructive actions.
- Created Storybook stories demonstrating various use cases, including default, destructive, and custom content scenarios.
This commit is contained in:
MyPrototypeWhat 2025-12-02 18:32:28 +08:00
parent 8006fbd667
commit 1a6263cf7f
3 changed files with 258 additions and 0 deletions

View File

@ -0,0 +1,75 @@
import * as React from 'react'
import { Button } from '../../primitives/button'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '../../primitives/dialog'
interface ConfirmDialogProps {
/** Controls the open state of the dialog */
open?: boolean
/** Callback when open state changes */
onOpenChange?: (open: boolean) => void
/** Dialog title */
title: React.ReactNode
/** Dialog description */
description?: React.ReactNode
/** Custom content below description */
content?: React.ReactNode
/** Confirm button text */
confirmText?: string
/** Cancel button text */
cancelText?: string
/** Callback when confirm button is clicked */
onConfirm?: () => void | Promise<void>
/** Whether this is a destructive action (e.g., delete) */
destructive?: boolean
/** Loading state for confirm button */
confirmLoading?: boolean
}
function ConfirmDialog({
open,
onOpenChange,
title,
description,
content,
confirmText = 'Confirm',
cancelText = 'Cancel',
onConfirm,
destructive = false,
confirmLoading = false
}: ConfirmDialogProps) {
const handleConfirm = React.useCallback(async () => {
await onConfirm?.()
onOpenChange?.(false)
}, [onConfirm, onOpenChange])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{description && <DialogDescription>{description}</DialogDescription>}
</DialogHeader>
{content}
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{cancelText}</Button>
</DialogClose>
<Button variant={destructive ? 'destructive' : 'default'} onClick={handleConfirm} loading={confirmLoading}>
{confirmText}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export { ConfirmDialog, type ConfirmDialogProps }

View File

@ -12,6 +12,7 @@ export { DescriptionSwitch, Switch } from './primitives/switch'
export { Tooltip, type TooltipProps } from './primitives/tooltip'
// Composite Components
export { ConfirmDialog, type ConfirmDialogProps } from './composites/ConfirmDialog'
export { default as Ellipsis } from './composites/Ellipsis'
export { default as ExpandableText } from './composites/ExpandableText'
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './composites/Flex'

View File

@ -0,0 +1,182 @@
import { Button, ConfirmDialog } from '@cherrystudio/ui'
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
const meta: Meta<typeof ConfirmDialog> = {
title: 'Components/Composites/ConfirmDialog',
component: ConfirmDialog,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'A pre-composed confirm dialog component that combines Dialog, Button, and other primitives for quick confirmation scenarios.'
}
}
},
tags: ['autodocs'],
argTypes: {
title: {
control: { type: 'text' },
description: 'Dialog title'
},
description: {
control: { type: 'text' },
description: 'Dialog description'
},
confirmText: {
control: { type: 'text' },
description: 'Confirm button text'
},
cancelText: {
control: { type: 'text' },
description: 'Cancel button text'
},
destructive: {
control: { type: 'boolean' },
description: 'Whether this is a destructive action'
},
confirmLoading: {
control: { type: 'boolean' },
description: 'Loading state for confirm button'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
function DefaultDemo() {
const [open, setOpen] = useState(false)
return (
<>
<Button onClick={() => setOpen(true)}>Open Dialog</Button>
<ConfirmDialog
open={open}
onOpenChange={setOpen}
title="Confirm Action"
description="Are you sure you want to proceed with this action?"
onConfirm={() => console.log('Confirmed')}
/>
</>
)
}
export const Default: Story = {
render: () => <DefaultDemo />
}
function DestructiveDemo() {
const [open, setOpen] = useState(false)
return (
<>
<Button variant="destructive" onClick={() => setOpen(true)}>
Delete Item
</Button>
<ConfirmDialog
open={open}
onOpenChange={setOpen}
title="Delete Item"
description="This action cannot be undone. This will permanently delete the item."
destructive
confirmText="Delete"
onConfirm={() => console.log('Deleted')}
/>
</>
)
}
export const Destructive: Story = {
render: () => <DestructiveDemo />
}
function WithLoadingDemo() {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const handleConfirm = async () => {
setLoading(true)
await new Promise((resolve) => setTimeout(resolve, 2000))
setLoading(false)
}
return (
<>
<Button onClick={() => setOpen(true)}>Save Changes</Button>
<ConfirmDialog
open={open}
onOpenChange={setOpen}
title="Save Changes"
description="Do you want to save your changes?"
confirmText="Save"
confirmLoading={loading}
onConfirm={handleConfirm}
/>
</>
)
}
export const WithLoading: Story = {
render: () => <WithLoadingDemo />
}
function WithCustomContentDemo() {
const [open, setOpen] = useState(false)
return (
<>
<Button variant="outline" onClick={() => setOpen(true)}>
Export Data
</Button>
<ConfirmDialog
open={open}
onOpenChange={setOpen}
title="Export Data"
description="Select the format for your export:"
content={
<div className="flex flex-col gap-2 py-2">
<label className="flex items-center gap-2">
<input type="radio" name="format" defaultChecked />
<span className="text-sm">CSV</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="format" />
<span className="text-sm">JSON</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="format" />
<span className="text-sm">Excel</span>
</label>
</div>
}
confirmText="Export"
onConfirm={() => console.log('Exported')}
/>
</>
)
}
export const WithCustomContent: Story = {
render: () => <WithCustomContentDemo />
}
function CustomButtonTextDemo() {
const [open, setOpen] = useState(false)
return (
<>
<Button onClick={() => setOpen(true)}>Logout</Button>
<ConfirmDialog
open={open}
onOpenChange={setOpen}
title="Logout"
description="Are you sure you want to logout?"
confirmText="Yes, Logout"
cancelText="Stay Logged In"
onConfirm={() => console.log('Logged out')}
/>
</>
)
}
export const CustomButtonText: Story = {
render: () => <CustomButtonTextDemo />
}