mirror of https://github.com/buster-so/buster.git
make tailwind modla
This commit is contained in:
parent
f4c0054301
commit
ad3709f924
|
@ -21,6 +21,7 @@
|
|||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
|
@ -2428,7 +2429,6 @@
|
|||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"extraneous": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -5800,6 +5800,42 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AppModalNew } from './Modal';
|
||||
import { Button } from '../buttons/Button';
|
||||
import React from 'react';
|
||||
import { ModalProps } from './Modal';
|
||||
const meta: Meta<typeof AppModalNew> = {
|
||||
title: 'UI/Modal/AppModal',
|
||||
component: AppModalNew,
|
||||
argTypes: {
|
||||
open: {
|
||||
control: 'boolean',
|
||||
description: 'Controls whether the modal is open or closed'
|
||||
},
|
||||
onClose: {
|
||||
action: 'closed',
|
||||
description: 'Function called when the modal is closed'
|
||||
},
|
||||
width: {
|
||||
control: { type: 'number', min: 300, max: 1200, step: 50 },
|
||||
description: 'Width of the modal in pixels'
|
||||
},
|
||||
header: {
|
||||
description: 'Header configuration with title and optional description'
|
||||
},
|
||||
footer: {
|
||||
description: 'Footer configuration with primary and optional secondary buttons'
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AppModalNew>;
|
||||
|
||||
// Helper component to control modal state in Storybook
|
||||
const ModalContainer = (args: ModalProps) => {
|
||||
const [isOpen, setIsOpen] = React.useState(true);
|
||||
|
||||
const handleOpen = () => setIsOpen(true);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
const modalProps = {
|
||||
...args,
|
||||
open: isOpen,
|
||||
onClose: handleClose,
|
||||
footer: {
|
||||
...args.footer,
|
||||
primaryButton: {
|
||||
...args.footer.primaryButton,
|
||||
onClick: () => {
|
||||
args.footer.primaryButton.onClick?.();
|
||||
handleClose();
|
||||
}
|
||||
},
|
||||
secondaryButton: args.footer.secondaryButton
|
||||
? {
|
||||
...args.footer.secondaryButton,
|
||||
onClick: () => {
|
||||
args.footer.secondaryButton?.onClick?.();
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={handleOpen}>Open Modal</Button>
|
||||
<AppModalNew {...modalProps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => <ModalContainer {...args} />,
|
||||
args: {
|
||||
header: {
|
||||
title: 'Modal Title',
|
||||
description: 'This is a description of the modal'
|
||||
},
|
||||
footer: {
|
||||
primaryButton: {
|
||||
text: 'Confirm',
|
||||
onClick: () => console.log('Primary button clicked')
|
||||
},
|
||||
secondaryButton: {
|
||||
text: 'Cancel',
|
||||
onClick: () => console.log('Secondary button clicked')
|
||||
}
|
||||
},
|
||||
width: 600,
|
||||
children: (
|
||||
<div className="">
|
||||
<p>This is the content of the modal.</p>
|
||||
<p className="mt-2">You can put any React components here.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export const WithoutDescription: Story = {
|
||||
render: (args) => <ModalContainer {...args} />,
|
||||
args: {
|
||||
header: {
|
||||
title: 'Simple Modal'
|
||||
},
|
||||
footer: {
|
||||
primaryButton: {
|
||||
text: 'OK',
|
||||
onClick: () => console.log('OK clicked')
|
||||
}
|
||||
},
|
||||
width: 500,
|
||||
children: (
|
||||
<div className="">
|
||||
<p>A modal without a description and secondary button.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export const LoadingState: Story = {
|
||||
render: (args) => <ModalContainer {...args} />,
|
||||
args: {
|
||||
header: {
|
||||
title: 'Processing Data',
|
||||
description: 'Please wait while we process your request'
|
||||
},
|
||||
footer: {
|
||||
primaryButton: {
|
||||
text: 'Submit',
|
||||
onClick: () => console.log('Submit clicked'),
|
||||
loading: true
|
||||
},
|
||||
secondaryButton: {
|
||||
text: 'Cancel',
|
||||
onClick: () => console.log('Cancel clicked'),
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
width: 550,
|
||||
children: (
|
||||
<div className="">
|
||||
<p>This modal shows loading and disabled states for buttons.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export const CustomWidth: Story = {
|
||||
render: (args) => <ModalContainer {...args} />,
|
||||
args: {
|
||||
header: {
|
||||
title: 'Wide Modal',
|
||||
description: 'This modal has a custom width'
|
||||
},
|
||||
footer: {
|
||||
primaryButton: {
|
||||
text: 'Save',
|
||||
onClick: () => console.log('Save clicked')
|
||||
},
|
||||
secondaryButton: {
|
||||
text: 'Discard',
|
||||
onClick: () => console.log('Discard clicked')
|
||||
}
|
||||
},
|
||||
width: 300,
|
||||
children: (
|
||||
<div className="">
|
||||
<p>This is a wider modal that can be used for displaying more content.</p>
|
||||
<div className="mt-4 grid grid-cols-2 gap-4">
|
||||
<div className="rounded border p-4">Column 1 content</div>
|
||||
<div className="rounded border p-4">Column 2 content</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export const WithCustomFooterLeft: Story = {
|
||||
render: (args) => <ModalContainer {...args} />,
|
||||
args: {
|
||||
header: {
|
||||
title: 'Custom Footer',
|
||||
description: 'This modal has custom content in the left side of the footer'
|
||||
},
|
||||
footer: {
|
||||
left: <span className="text-sm text-gray-500">Additional footer information</span>,
|
||||
primaryButton: {
|
||||
text: 'Continue',
|
||||
onClick: () => console.log('Continue clicked')
|
||||
},
|
||||
secondaryButton: {
|
||||
text: 'Back',
|
||||
onClick: () => console.log('Back clicked')
|
||||
}
|
||||
},
|
||||
width: 600,
|
||||
children: (
|
||||
<div className="">
|
||||
<p>Notice the additional text on the left side of the footer.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
|
@ -0,0 +1,105 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { type ButtonProps, Button } from '../buttons/Button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from './ModalBase';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
export interface ModalProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
footer: {
|
||||
left?: React.ReactNode;
|
||||
primaryButton: {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
variant?: ButtonProps['variant'];
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
secondaryButton?: {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
variant?: ButtonProps['variant'];
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
};
|
||||
header: {
|
||||
title: string;
|
||||
description?: string;
|
||||
};
|
||||
width?: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AppModalNew: React.FC<ModalProps> = React.memo(
|
||||
({ open, onClose, footer, header, width = 600, className, style, children }) => {
|
||||
const onOpenChange = useMemoizedFn((open: boolean) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
const memoizedStyle = useMemo(
|
||||
() => ({
|
||||
minWidth: width ?? 600,
|
||||
maxWidth: width ?? 600,
|
||||
...style
|
||||
}),
|
||||
[width, style]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={className} style={memoizedStyle}>
|
||||
<div className="flex flex-col gap-4 p-6">
|
||||
{header && (
|
||||
<DialogHeader>
|
||||
{header.title && <DialogTitle>{header.title}</DialogTitle>}
|
||||
{header.description && <DialogDescription>{header.description}</DialogDescription>}
|
||||
</DialogHeader>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{footer && (
|
||||
<DialogFooter
|
||||
className={cn('flex items-center', footer.left ? 'justify-between' : 'justify-end')}>
|
||||
{footer.left && footer.left}
|
||||
<div className={cn('flex items-center space-x-2')}>
|
||||
{footer.secondaryButton && (
|
||||
<Button
|
||||
onClick={footer.secondaryButton.onClick}
|
||||
variant={footer.secondaryButton.variant ?? 'ghost'}
|
||||
loading={footer.secondaryButton.loading}
|
||||
disabled={footer.secondaryButton.disabled}>
|
||||
{footer.secondaryButton.text}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={footer.primaryButton.onClick}
|
||||
variant={footer.primaryButton.variant ?? 'black'}
|
||||
loading={footer.primaryButton.loading}
|
||||
disabled={footer.primaryButton.disabled}>
|
||||
{footer.primaryButton.text}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AppModalNew.displayName = 'AppModal';
|
|
@ -0,0 +1,108 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Xmark } from '@/components/ui/icons';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '../buttons';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/60',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.memo(
|
||||
React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[20%] data-[state=open]:slide-in-from-top-[20%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]',
|
||||
'bg-background rounded',
|
||||
'border shadow-lg duration-200',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
'absolute top-4 right-4 opacity-70 transition-opacity hover:opacity-100 disabled:pointer-events-none'
|
||||
)}>
|
||||
<Button prefix={<Xmark />} variant="ghost" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
);
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||
);
|
||||
DialogHeader.displayName = 'DialogHeader';
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('space-x-2 border-t px-6 py-2.5', className)} {...props} />
|
||||
);
|
||||
DialogFooter.displayName = 'DialogFooter';
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-text-default text-xl leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-text-secondary text-md', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
};
|
Loading…
Reference in New Issue