mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 23:59:45 +08:00
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:
parent
8006fbd667
commit
1a6263cf7f
@ -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 }
|
||||||
@ -12,6 +12,7 @@ export { DescriptionSwitch, Switch } from './primitives/switch'
|
|||||||
export { Tooltip, type TooltipProps } from './primitives/tooltip'
|
export { Tooltip, type TooltipProps } from './primitives/tooltip'
|
||||||
|
|
||||||
// Composite Components
|
// Composite Components
|
||||||
|
export { ConfirmDialog, type ConfirmDialogProps } from './composites/ConfirmDialog'
|
||||||
export { default as Ellipsis } from './composites/Ellipsis'
|
export { default as Ellipsis } from './composites/Ellipsis'
|
||||||
export { default as ExpandableText } from './composites/ExpandableText'
|
export { default as ExpandableText } from './composites/ExpandableText'
|
||||||
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './composites/Flex'
|
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './composites/Flex'
|
||||||
|
|||||||
@ -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 />
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user