diff --git a/packages/ui/src/components/composites/ConfirmDialog/index.tsx b/packages/ui/src/components/composites/ConfirmDialog/index.tsx new file mode 100644 index 0000000000..c0d8d8ee32 --- /dev/null +++ b/packages/ui/src/components/composites/ConfirmDialog/index.tsx @@ -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 + /** 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 ( + + + + {title} + {description && {description}} + + {content} + + + + + + + + + ) +} + +export { ConfirmDialog, type ConfirmDialogProps } diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 4545e19ce3..7381afc1ba 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -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' diff --git a/packages/ui/stories/components/composites/ConfirmDialog.stories.tsx b/packages/ui/stories/components/composites/ConfirmDialog.stories.tsx new file mode 100644 index 0000000000..3cb823e985 --- /dev/null +++ b/packages/ui/stories/components/composites/ConfirmDialog.stories.tsx @@ -0,0 +1,182 @@ +import { Button, ConfirmDialog } from '@cherrystudio/ui' +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' + +const meta: Meta = { + 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 + +function DefaultDemo() { + const [open, setOpen] = useState(false) + return ( + <> + + console.log('Confirmed')} + /> + + ) +} + +export const Default: Story = { + render: () => +} + +function DestructiveDemo() { + const [open, setOpen] = useState(false) + return ( + <> + + console.log('Deleted')} + /> + + ) +} + +export const Destructive: Story = { + render: () => +} + +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 ( + <> + + + + ) +} + +export const WithLoading: Story = { + render: () => +} + +function WithCustomContentDemo() { + const [open, setOpen] = useState(false) + return ( + <> + + + + + + + } + confirmText="Export" + onConfirm={() => console.log('Exported')} + /> + + ) +} + +export const WithCustomContent: Story = { + render: () => +} + +function CustomButtonTextDemo() { + const [open, setOpen] = useState(false) + return ( + <> + + console.log('Logged out')} + /> + + ) +} + +export const CustomButtonText: Story = { + render: () => +}