diff --git a/src/components/ui/Alert.stories.tsx b/src/components/ui/Alert.stories.tsx index c8df6b2..438524e 100644 --- a/src/components/ui/Alert.stories.tsx +++ b/src/components/ui/Alert.stories.tsx @@ -1,5 +1,29 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Alert, AlertTitle, AlertDescription } from "./alert"; +import React from "react"; +import { + AlertTriangle, + CheckCircle, + Info as InfoIcon, + X, + XCircle, +} from "lucide-react"; +import { Alert, AlertAction, AlertDescription, AlertTitle } from "./alert"; +import { Button } from "./button"; + +const VARIANT_ICONS = { + default: InfoIcon, + destructive: XCircle, + success: CheckCircle, + warning: AlertTriangle, + info: InfoIcon, +} as const; + +type AlertVariant = keyof typeof VARIANT_ICONS; + +function AlertIcon({ variant }: { variant: AlertVariant }) { + const Icon = VARIANT_ICONS[variant] ?? InfoIcon; + return ; +} const meta = { title: "UI/Alert", @@ -19,8 +43,9 @@ export default meta; type Story = StoryObj; export const Default: Story = { - render: () => ( - + args: { variant: "default" }, + render: (args) => ( + Title Description text goes here. @@ -28,8 +53,9 @@ export const Default: Story = { }; export const Destructive: Story = { - render: () => ( - + args: { variant: "destructive" }, + render: (args) => ( + Error Something went wrong. @@ -37,21 +63,19 @@ export const Destructive: Story = { }; export const Success: Story = { - args: { - variant: "default" - }, - - render: () => ( - + args: { variant: "success" }, + render: (args) => ( + Success Your changes have been saved. - ) + ), }; export const Warning: Story = { - render: () => ( - + args: { variant: "warning" }, + render: (args) => ( + Warning Please review before continuing. @@ -59,10 +83,90 @@ export const Warning: Story = { }; export const Info: Story = { - render: () => ( - + args: { variant: "info" }, + render: (args) => ( + Info New update available. ), }; + +const defaultVariant: AlertVariant = "default"; + +export const WithDescription: Story = { + args: { variant: defaultVariant }, + render: (args) => ( + + + Hold on I need at least a few minutes! + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. + + + + + + ), +}; + +export const WithoutDescription: Story = { + args: { variant: defaultVariant }, + render: (args) => ( + + + Hold on I need at least a few minutes! + + + + + ), +}; + +export const WithButtons: Story = { + args: { variant: defaultVariant }, + render: (args) => ( + +
+ + Hold on I need at least a few minutes! +
+ + +
+
+ + + +
+ ), +}; + +export const WithDescriptionAndButtons: Story = { + args: { variant: defaultVariant }, + render: (args) => ( + + + Hold on I need at least a few minutes! + + This process may take a few minutes to complete. See the{" "} + documentation for more details. + +
+ + +
+ + + +
+ ), +}; diff --git a/src/components/ui/Confirmation.stories.tsx b/src/components/ui/Confirmation.stories.tsx new file mode 100644 index 0000000..169356a --- /dev/null +++ b/src/components/ui/Confirmation.stories.tsx @@ -0,0 +1,128 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React, { useState } from "react"; +import { Confirmation, Button, Notice } from "./index"; +import { CircleMinus } from "lucide-react"; + +function LeaveWithUnsavedChangesDemo() { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} + title="Are you sure you want to leave?" + body="You have unsaved changes." + confirmLabel="Leave" + cancelLabel="Stay in page" + onConfirm={() => setOpen(false)} + /> + + ); +} + +function CancelSubscriptionDemo() { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} + title="Are you sure you want to cancel the subscription plan?" + body={ + +

+ Next billing date is 25th March 2024 and reassign + is not possible for recurring subscription +

+
+ } + icon={} + confirmLabel="Yes, Cancel" + cancelLabel="No" + onConfirm={() => setOpen(false)} + /> + + ); +} + +function DiscardDraftDemo() { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} + title="Are you sure you want to discard this draft?" + confirmLabel="Yes, Cancel" + cancelLabel="No" + onConfirm={() => setOpen(false)} + /> + + ); +} + +function DestructiveDemo() { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} + title="Delete this item?" + body="This action cannot be undone." + variant="destructive" + confirmLabel="Delete" + cancelLabel="Cancel" + onConfirm={() => setOpen(false)} + /> + + ); +} + +const meta = { + title: "UI/Confirmation", + component: Confirmation, + parameters: { layout: "centered" }, + tags: ["autodocs"], + args: { + open: false, + onClose: () => {}, + title: "Confirm action", + confirmLabel: "Confirm", + cancelLabel: "Cancel", + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const LeaveWithUnsavedChanges: Story = { + render: () => , + args: { title: "Are you sure you want to leave?", body: "You have unsaved changes." }, +}; + +export const CancelSubscription: Story = { + render: () => , + args: { title: "Cancel subscription?" }, +}; + +export const DiscardDraft: Story = { + render: () => , + args: { title: "Discard this draft?" }, +}; + +export const Destructive: Story = { + render: () => , + args: { title: "Delete this item?", variant: "destructive" }, +}; diff --git a/src/components/ui/Modal.stories.tsx b/src/components/ui/Modal.stories.tsx index a01a1a7..3d0ef94 100644 --- a/src/components/ui/Modal.stories.tsx +++ b/src/components/ui/Modal.stories.tsx @@ -1,13 +1,35 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { useState } from "react"; +import React, { useState } from "react"; import { Modal, ModalHeader, ModalTitle, - ModalDescription, ModalFooter, Button, + Input, + LabeledSwitch, + Field, + FieldLabel, + FieldDescription, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + ModalDescription, } from "./index"; +import { ChevronDownIcon } from "lucide-react"; + +const ZONE_NAME_HELPER = "Give a meaningful name for your reference"; + +const COUNTRIES = [ + "Bangladesh", + "United States", + "United Kingdom", + "Canada", + "Australia", + "Germany", + "France", +] as const; function ModalDemo() { const [open, setOpen] = useState(false); @@ -17,11 +39,12 @@ function ModalDemo() { setOpen(false)}> Modal Title - Description text here. -
Modal content.
+ Modal content. - +
@@ -29,11 +52,68 @@ function ModalDemo() { ); } +function CreateShippingZoneDemo() { + const [open, setOpen] = useState(false); + const [restOfWorld, setRestOfWorld] = useState(false); + + return ( + <> + + setOpen(false)} size="default"> + + Create Shipping Zone + +
+ + Zone Name + + {ZONE_NAME_HELPER} + + + setRestOfWorld(checked)} + /> + + + Countries + + + + + + {COUNTRIES.map((country) => ( + {country} + ))} + + + {ZONE_NAME_HELPER} + +
+ + + + +
+ + ); +} + const meta = { title: "UI/Modal", component: Modal, parameters: { layout: "centered" }, tags: ["autodocs"], + args: { open: false, onClose: () => {} }, } satisfies Meta; export default meta; @@ -41,3 +121,7 @@ export default meta; type Story = StoryObj; export const Default: Story = { render: () => }; + +export const CreateShippingZone: Story = { + render: () => , +}; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 3d4a72a..2e17ca1 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const alertVariants = cva( - "grid gap-2.5 rounded-lg border px-5 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert", + "grid gap-2.5 rounded-lg border p-5 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert", { variants: { variant: { diff --git a/src/components/ui/confirmation.tsx b/src/components/ui/confirmation.tsx new file mode 100644 index 0000000..481c415 --- /dev/null +++ b/src/components/ui/confirmation.tsx @@ -0,0 +1,103 @@ +import type { ReactNode } from "react"; +import { TriangleAlert } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "./button"; +import { + Modal, + ModalDescription, + ModalFooter, + ModalTitle, +} from "./modal"; + +const DEFAULT_CONFIRM_LABEL = "Confirm"; +const DEFAULT_CANCEL_LABEL = "Cancel"; + +export type ConfirmationVariant = "default" | "destructive"; + +const iconContainerVariants: Record = { + default: "bg-primary/10 text-primary", + destructive: "bg-destructive/10 text-destructive", +}; + +export interface ConfirmationProps { + open: boolean; + onClose: () => void; + title: ReactNode; + body?: ReactNode; + icon?: ReactNode; + confirmLabel?: string; + cancelLabel?: string; + onConfirm?: () => void; + onCancel?: () => void; + variant?: ConfirmationVariant; + showCloseButton?: boolean; + closeOnOverlayClick?: boolean; +} + +export function Confirmation({ + open, + onClose, + title, + body, + icon, + confirmLabel = DEFAULT_CONFIRM_LABEL, + cancelLabel = DEFAULT_CANCEL_LABEL, + onConfirm, + onCancel, + variant = "default", + showCloseButton = true, + closeOnOverlayClick = true, +}: ConfirmationProps) { + const handleConfirm = () => { + onClose(); + onConfirm?.(); + }; + + const handleCancel = () => { + onClose(); + onCancel?.(); + }; + + const iconContent = icon ?? ; + + return ( + +
+
+ {iconContent} +
+ + {title} + +
+ {body != null ? ( + + {body} + + ) : ( + + )} + + + + +
+ ); +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 68be726..ad84888 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -51,6 +51,11 @@ export { Modal, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalTitle, type ModalProps } from "./modal"; +export { + Confirmation, + type ConfirmationProps, + type ConfirmationVariant, +} from "./confirmation"; export { ComponentPreview, DesignSystemSection, diff --git a/src/components/ui/modal.tsx b/src/components/ui/modal.tsx index d55ddd9..5a22d0b 100644 --- a/src/components/ui/modal.tsx +++ b/src/components/ui/modal.tsx @@ -1,6 +1,10 @@ import { + createContext, forwardRef, + useContext, useEffect, + useId, + useLayoutEffect, useRef, useState, type HTMLAttributes, @@ -8,6 +12,7 @@ import { } from "react"; import { createPortal } from "react-dom"; import { cn } from "@/lib/utils"; +import { getThemeStyles, useThemeOptional } from "@/providers"; /* ============================================ Modal Overlay @@ -15,12 +20,15 @@ import { cn } from "@/lib/utils"; interface ModalOverlayProps extends HTMLAttributes { onClose?: () => void; + "data-state"?: "open" | "closed"; } const ModalOverlay = forwardRef( - ({ className, onClose, ...props }, ref) => ( + ({ className, onClose, "data-state": dataState, ...props }, ref) => (