Dialog

14 blocks

Dialog 01

Preview
npx nateui@latest add DialogPaymentSuccess
Dark
import { useState } from 'react'
import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import EmptyState from '@/components/composites/EmptyState'
import { PiCheckBold } from 'react-icons/pi'

const dialogTitleId = 'dialog-payment-success-title'
const dialogDescriptionId = 'dialog-payment-success-description'

export default function DialogPaymentSuccess() {
    const [isOpen, setIsOpen] = useState(true)

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={() => setIsOpen(true)}>
                Open dialog
            </Button>

            <Dialog
                aria-describedby={dialogDescriptionId}
                aria-labelledby={dialogTitleId}
                className="w-full p-0"
                closable={false}
                isOpen={isOpen}
                lockScroll
                width={380}
                onClose={() => setIsOpen(false)}
            >
                <div className="flex flex-col items-center px-8 pb-8 pt-2 text-center">
                    <EmptyState
                        illustration={
                            <div className="rounded-full bg-palette-emerald-soft p-2">
                                <div
                                    className="flex items-center justify-center rounded-full bg-palette-emerald text-white size-15"
                                >
                                    <PiCheckBold
                                        aria-hidden="true"
                                        className="text-3xl"
                                    />
                                </div>
                            </div>
                        }
                        size={220}
                        offset={-40}
                    >
                        <h5 
                            id={dialogTitleId}
                            className="font-semibold text-lg"
                        >
                            Payment complete
                        </h5>
                        <p
                            className="mt-1 text-muted-foreground"
                            id={dialogDescriptionId}
                        >
                            A receipt for this charge is ready to open.
                        </p>
                    </EmptyState>

                    <div className="mt-8 grid w-full grid-cols-1 gap-2">
                        <Button
                            type="button"
                            variant="solid"
                            onClick={() => setIsOpen(false)}
                        >
                            View receipt
                        </Button>
                        <Button
                            type="button"
                            onClick={() => setIsOpen(false)}
                        >
                            Back to home
                        </Button>
                    </div>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 02

Preview
npx nateui@latest add DialogPolicyNotice
Dark
import { useState } from 'react'
import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import Link from '@/components/ui/Link'
import { PiFileText } from 'react-icons/pi'

const dialogTitleId = 'dialog-policy-notice-title'
const dialogDescriptionId = 'dialog-policy-notice-description'

export default function DialogPolicyNotice() {
    const [isOpen, setIsOpen] = useState(true)

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={() => setIsOpen(true)}>
                Open dialog
            </Button>

            <Dialog
                aria-describedby={dialogDescriptionId}
                aria-labelledby={dialogTitleId}
                isOpen={isOpen}
                lockScroll
                width={460}
                onClose={() => setIsOpen(false)}
            >
                <PiFileText
                    aria-hidden="true"
                    className="text-2xl text-muted-foreground"
                />

                <h5
                    className="mt-4 text-lg font-semibold"
                    id={dialogTitleId}
                >
                    Terms of service update
                </h5>
                <p
                    className="mt-1 text-muted-foreground"
                    id={dialogDescriptionId}
                >
                    The terms that govern access to this service have
                    changed. Review them before you continue, since they may
                    affect what you&apos;re able to do here.
                </p>

                <div className="mt-8 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
                    <div className="flex shrink-0 items-center gap-2">
                        <Button
                            type="button"
                            onClick={() => setIsOpen(false)}
                        >
                            Dismiss
                        </Button>
                        <Button
                            type="button"
                            variant="solid"
                            onClick={() => setIsOpen(false)}
                        >
                            Open terms
                        </Button>
                    </div>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 03

Preview
npx nateui@latest add DialogDateTimePicker
Dark
import { useState } from 'react'
import Calendar from '@/components/ui/Calendar'
import Dialog from '@/components/ui/Dialog'
import Button from '@/components/ui/Button'
import CloseButton from '@/components/ui/CloseButton'
import Menu from '@/components/ui/Menu'
import TimeInput from '@/components/ui/TimeInput'
import Divider from '@/components/composites/Divider'
import IconFrame from '@/components/composites/IconFrame'
import useResponsive from '@/hooks/useResponsive'
import { PiClock } from 'react-icons/pi'

const dialogDateTimePickerTitleId = 'dialog-date-time-picker-title'
const dialogDateTimePickerDescriptionId = 'dialog-date-time-picker-description'

const dateFromIso = (value: string) => {
    const [year, month, day] = value.split('-').map(Number)
    return new Date(year, month - 1, day)
}

const dateWithTime = (value: string, hours: number, minutes: number) => {
    const date = dateFromIso(value)
    date.setHours(hours, minutes, 0, 0)
    return date
}

const presetRanges = {
    Today: [dateFromIso('2026-08-15'), dateFromIso('2026-08-15')],
    Tomorrow: [dateFromIso('2026-08-16'), dateFromIso('2026-08-16')],
    'This week': [dateFromIso('2026-08-10'), dateFromIso('2026-08-16')],
    'Next week': [dateFromIso('2026-08-17'), dateFromIso('2026-08-23')],
    'This weekend': [dateFromIso('2026-08-15'), dateFromIso('2026-08-16')],
    'This month': [dateFromIso('2026-08-01'), dateFromIso('2026-08-31')],
    'Next month': [dateFromIso('2026-09-01'), dateFromIso('2026-09-30')],
    'Next 30 days': [dateFromIso('2026-08-15'), dateFromIso('2026-09-13')],
} satisfies Record<string, [Date, Date]>

type PresetName = keyof typeof presetRanges
type DateRange = [Date | null, Date | null]

export default function DialogDateTimePicker() {
    const [isOpen, setIsOpen] = useState(true)
    const [activePreset, setActivePreset] = useState<PresetName | null>('Today')
    const [selectedRange, setSelectedRange] = useState<DateRange>(
        presetRanges.Today,
    )
    const [meetingTime, setMeetingTime] = useState<Date | null>(() =>
        dateWithTime('2026-08-15', 9, 0),
    )
    const [meetingTimeRange, setMeetingTimeRange] = useState<DateRange>(() => [
        dateWithTime('2026-08-15', 9, 0),
        dateWithTime('2026-08-15', 17, 0),
    ])
    const { smaller } = useResponsive()
    const isCompact = smaller.md

    const isSingleDay =
        selectedRange[0] &&
        (!selectedRange[1] ||
            selectedRange[0].toDateString() === selectedRange[1].toDateString())

    const applyPreset = (preset: PresetName) => {
        const [start, end] = presetRanges[preset]
        setActivePreset(preset)
        setSelectedRange([new Date(start), new Date(end)])
    }

    const handleRangeChange = (range: DateRange) => {
        setActivePreset(null)
        setSelectedRange(range)
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={() => setIsOpen(true)}>
                Open dialog
            </Button>

            <Dialog
                aria-describedby={dialogDateTimePickerDescriptionId}
                aria-labelledby={dialogDateTimePickerTitleId}
                className="p-0"
                closable={false}
                isOpen={isOpen}
                lockScroll
                width={780}
                onClose={() => setIsOpen(false)}
            >
                <header className="flex justify-between gap-4 p-4 border-b">
                    <div className="flex items-center gap-2">
                        <IconFrame
                            aria-hidden="true"
                            className="shrink-0 text-xl"
                            size={40}
                        >
                            <PiClock />
                        </IconFrame>
                        <div className="min-w-0">
                            <h5
                                className="text-lg font-semibold"
                                id={dialogDateTimePickerTitleId}
                            >
                                Pick a date and time
                            </h5>
                            <p
                                className="text-muted-foreground"
                                id={dialogDateTimePickerDescriptionId}
                            >
                                Choose a preset, or drag across the calendar.
                            </p>
                        </div>
                    </div>
                    <div>
                        <CloseButton onClick={() => setIsOpen(false)} />
                    </div>
                </header>

                <div className="flex flex-col md:flex-row border-b">
                    <aside className="w-full shrink-0 border-b p-4 md:w-44 md:border-b-0 md:border-r">
                        <nav aria-label="Date range presets">
                            <Menu variant="subtle">
                                {(Object.keys(presetRanges) as PresetName[]).map(
                                    (preset) => {
                                        const isActive = activePreset === preset

                                        return (
                                            <Menu.MenuItem
                                                key={preset}
                                                eventKey={preset}
                                                isActive={isActive}
                                                role="button"
                                                tabIndex={0}
                                                aria-current={
                                                    isActive ? 'true' : undefined
                                                }
                                                onClick={() =>
                                                    applyPreset(preset)
                                                }
                                                onKeyDown={(event: React.KeyboardEvent) => {
                                                    if (
                                                        event.key === 'Enter' ||
                                                        event.key === ' '
                                                    ) {
                                                        event.preventDefault()
                                                        applyPreset(preset)
                                                    }
                                                }}
                                                className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
                                            >
                                                {preset}
                                            </Menu.MenuItem>
                                        )
                                    },
                                )}
                            </Menu>
                        </nav>
                    </aside>

                    <section className="min-w-0 flex-1">
                        <div className="p-4 border-b">
                            <Calendar.Range
                                className="w-full"
                                dateViewCount={isCompact ? 1 : 2}
                                defaultMonth={dateFromIso('2026-08-01')}
                                firstDayOfWeek="monday"
                                value={selectedRange}
                                onChange={handleRangeChange}
                            />
                        </div>
                        <footer className="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
                            <div className="flex items-center gap-2">
                                <span>Time: </span>
                                {isSingleDay ? (
                                    <TimeInput
                                        aria-label="Meeting time"
                                        value={meetingTime}
                                        onChange={setMeetingTime}
                                    />
                                ) : (
                                    <TimeInput.TimeInputRange
                                        aria-label="Meeting start and end times"
                                        value={meetingTimeRange}
                                        onChange={setMeetingTimeRange}
                                    />
                                )}
                            </div>

                            <div className="flex shrink-0 justify-end gap-2">
                                <Button type="button" onClick={() => setIsOpen(false)}>
                                    Cancel
                                </Button>
                                <Button
                                    type="button"
                                    variant="solid"
                                    onClick={() => setIsOpen(false)}
                                >
                                    Apply
                                </Button>
                            </div>
                        </footer>
                    </section>
                    
                </div>

                
            </Dialog>
        </main>
    )
}

Dialog 04

Preview
npx nateui@latest add DialogBulkImport
Dark
import { useState } from 'react'
import Button from '@/components/ui/Button'
import CloseButton from '@/components/ui/CloseButton'
import Dialog from '@/components/ui/Dialog'
import Input from '@/components/ui/Input'
import Link from '@/components/ui/Link'
import Upload from '@/components/ui/Upload'
import Divider from '@/components/composites/Divider'
import FileIcon from '@/components/composites/FileIcon'
import { PiArrowRight, PiLifebuoy, PiUploadSimple } from 'react-icons/pi'

const dialogTitleId = 'dialog-bulk-import-title'
const urlFieldId = 'dialog-bulk-import-url'

type ImportRow = {
    id: string
    name: string
    size: string
    type: string
}

const initialRows: ImportRow[] = [
    {
        id: 'contacts',
        name: 'contacts-export.csv',
        size: '4.2 MB',
        type: 'csv',
    },
    {
        id: 'accounts',
        name: 'account-records.pdf',
        size: '1.8 MB',
        type: 'pdf',
    },
]

export default function DialogBulkImport() {
    const [isOpen, setIsOpen] = useState(true)
    const [rows, setRows] = useState<ImportRow[]>(initialRows)
    const [importUrl, setImportUrl] = useState('')

    const handleOpen = () => {
        setRows(initialRows)
        setImportUrl('')
        setIsOpen(true)
    }

    const handleRemoveRow = (id: string) => {
        setRows((current) => current.filter((row) => row.id !== id))
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={handleOpen}>
                Open dialog
            </Button>

            <Dialog
                aria-labelledby={dialogTitleId}
                isOpen={isOpen}
                lockScroll
                width={500}
                onClose={() => setIsOpen(false)}
            >
                <h5
                    className="text-lg font-semibold"
                    id={dialogTitleId}
                >
                    Import files
                </h5>

                <Upload draggable showList={false} className="mt-4 w-full">
                    <div className="flex w-full flex-col items-center gap-1 px-4 py-8 text-center">
                        <div className="size-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center">
                            <PiUploadSimple
                                aria-hidden="true"
                                className="text-xl"
                            />
                        </div>
                        <p className="mt-2">
                            Drag files here, or{' '}
                            <span className="font-medium text-primary">
                                browse to upload
                            </span>
                        </p>
                        <p className="text-xs text-muted-foreground">
                            Accepts CSV, JSON, or PDF files up to 10 MB
                        </p>
                    </div>
                </Upload>

                {rows.length > 0 && (
                    <div className="mt-4 flex flex-col gap-2">
                        {rows.map((row) => (
                            <div
                                className="flex items-center gap-4 rounded-card border px-4 py-2"
                                key={row.id}
                            >
                                <FileIcon size={32} type={row.type} />
                                <div className="min-w-0 flex-1">
                                    <p className="truncate font-medium text-foreground">
                                        {row.name}
                                    </p>
                                    <p className="text-xs text-muted-foreground">
                                        {row.size}
                                    </p>
                                </div>
                                <CloseButton
                                    aria-label={`Remove ${row.name}`}
                                    onClick={() => handleRemoveRow(row.id)}
                                />
                            </div>
                        ))}
                    </div>
                )}

                <div className="mt-4 flex items-center gap-4">
                    <Divider className="my-0 flex-1" />
                    <span>or</span>
                    <Divider className="my-0 flex-1" />
                </div>

                <div className="mt-4">
                    <label
                        className="font-medium text-foreground"
                        htmlFor={urlFieldId}
                    >
                        Import from a URL
                    </label>
                    <Input
                        className="mt-2"
                        id={urlFieldId}
                        placeholder="https://example.com/records.csv"
                        suffix={
                            <Button
                                aria-label="Submit URL"
                                size="sm"
                                type="button"
                                variant="subtle"
                                className="!h-6 -mr-2"
                            >
                                Upload
                            </Button>
                        }
                        value={importUrl}
                        onChange={(event) =>
                            setImportUrl(event.target.value)
                        }
                    />
                </div>

                <div className="mt-8 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
                    <div className="flex shrink-0 items-center gap-2">
                        <Button
                            type="button"
                            onClick={() => setIsOpen(false)}
                        >
                            Cancel
                        </Button>
                        <Button
                            type="button"
                            variant="solid"
                            onClick={() => setIsOpen(false)}
                        >
                            Import files
                        </Button>
                    </div>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 05

Preview
npx nateui@latest add DialogTwoFactorSetup
Dark
import { useEffect, useRef, useState } from 'react'
import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import Input from '@/components/ui/Input'
import InputGroup from '@/components/ui/InputGroup'
import Divider from '@/components/composites/Divider'
import IconFrame from '@/components/composites/IconFrame'
import {
    PiArrowRight,
    PiCheck,
    PiCopy,
    PiScan,
} from 'react-icons/pi'

const assetBase = 'https://statics.nateui.com/img'
const dialogTitleId = 'dialog-two-factor-setup-title'
const dialogDescriptionId = 'dialog-two-factor-setup-description'

export default function DialogTwoFactorSetup() {
    const [isOpen, setIsOpen] = useState(true)
    const [isCopied, setIsCopied] = useState(false)
    const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
    const setupKey = 'K3QT VB7N M2XD 9WFA H6ZR P1LS'

    useEffect(() => {
        return () => {
            if (copyTimeoutRef.current !== null) {
                clearTimeout(copyTimeoutRef.current)
            }
        }
    }, [])

    const handleCopy = () => {
        if (copyTimeoutRef.current !== null) {
            clearTimeout(copyTimeoutRef.current)
        }

        void navigator.clipboard?.writeText(setupKey)
        setIsCopied(true)
        copyTimeoutRef.current = setTimeout(() => {
            setIsCopied(false)
            copyTimeoutRef.current = null
        }, 1600)
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={() => setIsOpen(true)}>
                Open dialog
            </Button>

            <Dialog
                aria-describedby={dialogDescriptionId}
                aria-labelledby={dialogTitleId}
                isOpen={isOpen}
                lockScroll
                width={420}
                onClose={() => setIsOpen(false)}
            >
                <div className="mt-4 flex flex-col items-center text-center">
                    <IconFrame
                        aria-hidden="true"
                        className="bg-palette-blue-soft text-palette-blue-soft-foreground border-0"
                        size={48}
                    >
                        <PiScan aria-hidden="true" className="text-2xl" />
                    </IconFrame>
                    <h5
                        className="mt-4 text-lg font-semibold"
                        id={dialogTitleId}
                    >
                        Set up two-factor authentication
                    </h5>
                    <p
                        className="mt-1 text-muted-foreground"
                        id={dialogDescriptionId}
                    >
                        Add an account in your authenticator app, then scan this
                        code.
                    </p>
                </div>

                <div className="mt-4 flex justify-center">
                    <div className="rounded-card border bg-white p-4">
                        <img
                            alt="Authenticator setup QR code"
                            className="block h-30 w-30"
                            height={120}
                            src={`${assetBase}/thumbs/misc/qr-code.svg`}
                            width={120}
                        />
                    </div>
                </div>

                <Button
                    block
                    className="mt-8"
                    icon={<PiArrowRight />}
                    iconAlignment="end"
                    type="button"
                    variant="solid"
                    onClick={() => setIsOpen(false)}
                >
                    Continue
                </Button>

                <div className="mt-4 flex items-center gap-4">
                    <Divider className="my-0 flex-1" />
                    <span className="min-w-0 text-center">
                        Can&apos;t scan? Use the setup key
                    </span>
                    <Divider className="my-0 flex-1" />
                </div>

                <InputGroup className="mt-4">
                    <Input
                        aria-label="Two-factor setup key"
                        className="min-w-0 flex-1 font-mono"
                        readOnly
                        value={setupKey}
                    />
                    <Button
                        aria-label={
                            isCopied ? 'Setup key copied' : 'Copy setup key'
                        }
                        className="shrink-0"
                        icon={isCopied ? <PiCheck /> : <PiCopy />}
                        type="button"
                        onClick={handleCopy}
                    />
                </InputGroup>
            </Dialog>
        </main>
    )
}

Dialog 06

Preview
npx nateui@latest add DialogPlanUpgrade
Dark
import { useState } from 'react'
import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import Link from '@/components/ui/Link'
import Radio from '@/components/ui/Radio'
import IconFrame from '@/components/composites/IconFrame'
import classNames from '@/utils/classNames'
import { PiArrowFatLinesUp  } from 'react-icons/pi'

const dialogTitleId = 'dialog-plan-upgrade-title'
const dialogDescriptionId = 'dialog-plan-upgrade-description'
const planGroupLabelId = 'dialog-plan-upgrade-group-label'

const assetBase = 'https://statics.nateui.com/img'

type PlanTier = {
    id: string
    name: string
    price: number
    description: string
    iconSrc: string
}

const planTiers: PlanTier[] = [
    {
        id: 'basic',
        name: 'Basic',
        price: 19,
        description: 'Core features for a single user.',
        iconSrc: `${assetBase}/thumbs/plans/basic.svg`,
    },
    {
        id: 'pro',
        name: 'Pro',
        price: 49,
        description: 'Higher usage limits and priority support.',
        iconSrc: `${assetBase}/thumbs/plans/standard.svg`,
    },
    {
        id: 'team',
        name: 'Team',
        price: 99,
        description: 'Shared billing for the whole team.',
        iconSrc: `${assetBase}/thumbs/plans/pro.svg`,
    },
]

export default function DialogPlanUpgrade() {
    const [isOpen, setIsOpen] = useState(true)
    const [selectedPlanId, setSelectedPlanId] = useState(planTiers[0].id)

    const handleOpen = () => {
        setSelectedPlanId(planTiers[0].id)
        setIsOpen(true)
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={handleOpen}>
                Open dialog
            </Button>

            <Dialog
                aria-describedby={dialogDescriptionId}
                aria-labelledby={dialogTitleId}
                isOpen={isOpen}
                lockScroll
                width={440}
                onClose={() => setIsOpen(false)}
            >
                <IconFrame variant="muted" size={36}>
                    <PiArrowFatLinesUp
                        aria-hidden="true"
                        className="text-xl text-foreground"
                    />
                </IconFrame>

                <h5
                    className="mt-4 text-lg font-semibold"
                    id={dialogTitleId}
                >
                    Subscription plans
                </h5>
                <p
                    className="text-muted-foreground"
                    id={dialogDescriptionId}
                >
                    Compare the available tiers before you confirm.
                </p>

                <p
                    className="mt-8 mb-2 font-medium text-foreground"
                    id={planGroupLabelId}
                >
                    Available plans
                </p>

                <Radio.Group
                    aria-labelledby={planGroupLabelId}
                    className="w-full gap-2"
                    name="dialog-plan-upgrade-tier"
                    role="radiogroup"
                    value={selectedPlanId}
                    vertical
                    onChange={(value) => setSelectedPlanId(value)}
                >
                    {planTiers.map((plan) => {
                        const isSelected = plan.id === selectedPlanId

                        return (
                            <Radio
                                className={classNames(
                                    'w-full flex-row-reverse items-start rounded-card border p-4',
                                    isSelected
                                        ? 'border-primary bg-primary-soft ring'
                                        : 'hover:bg-accent',
                                )}
                                key={plan.id}
                                value={plan.id}
                            >
                                <div className="flex items-start gap-3">
                                    <IconFrame className="shrink-0" size={40}>
                                        <img
                                            alt=""
                                            className="h-6 w-6 object-contain"
                                            src={plan.iconSrc}
                                        />
                                    </IconFrame>
                                    <div className="min-w-0 flex-1">
                                        <div className="flex flex-wrap items-baseline gap-x-1.5">
                                            <span className="font-medium text-foreground">
                                                {plan.name}
                                            </span>
                                            <span className="font-normal">
                                                ${plan.price}/mo
                                            </span>
                                        </div>
                                        <p className="mt-1 truncate font-normal text-muted-foreground">
                                            {plan.description}
                                        </p>
                                    </div>
                                </div>
                            </Radio>
                        )
                    })}
                </Radio.Group>

                <div className="mt-8 flex flex-col-reverse gap-2 sm:flex-row">
                    <Button
                        className="flex-1"
                        type="button"
                        onClick={() => setIsOpen(false)}
                    >
                        Cancel
                    </Button>
                    <Button
                        className="flex-1"
                        type="button"
                        variant="solid"
                        onClick={() => setIsOpen(false)}
                    >
                        Confirm
                    </Button>
                </div>

                <div className="mt-4 mb-2 text-center">
                    <Link className="underline" href="#plan-comparison">
                        Compare all plans
                    </Link>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 07

Preview
npx nateui@latest add DialogSubscriptionCheckout
Dark
import { useState } from 'react'
import Button from '@/components/ui/Button'
import Checkbox from '@/components/ui/Checkbox'
import Dialog from '@/components/ui/Dialog'
import Form from '@/components/ui/Form'
import Input from '@/components/ui/Input'
import Link from '@/components/ui/Link'
import Select from '@/components/ui/Select'
import Switcher from '@/components/ui/Switcher'
import Tag from '@/components/ui/Tag'
import Tooltip from '@/components/ui/Tooltip'
import Divider from '@/components/composites/Divider'
import IconFrame from '@/components/composites/IconFrame'
import { PiArrowLeft, PiCreditCard, PiInfo, PiReceipt } from 'react-icons/pi'

const assetBase = 'https://statics.nateui.com/img'

const dialogTitleId = 'dialog-subscription-checkout-title'
const emailFieldId = 'dialog-subscription-checkout-email'
const cardholderFieldId = 'dialog-subscription-checkout-cardholder'
const cardDetailsLabelId = 'dialog-subscription-checkout-card-details-label'
const countryLabelId = 'dialog-subscription-checkout-country-label'

type CountryOption = {
    label: string
    value: string
}

const countries: CountryOption[] = [
    { value: 'US', label: 'United States' },
    { value: 'GB', label: 'United Kingdom' },
    { value: 'CA', label: 'Canada' },
    { value: 'AU', label: 'Australia' },
    { value: 'DE', label: 'Germany' },
    { value: 'FR', label: 'France' },
    { value: 'JP', label: 'Japan' },
    { value: 'SG', label: 'Singapore' },
]

const countryFlag = (code: string) => (
    <img
        alt=""
        className="h-4 w-4 shrink-0"
        src={`${assetBase}/countries/${code}.png`}
    />
)

const monthlyAmount = { subtotal: 49, tax: 4.9, total: 53.9 }
const annualAmount = { subtotal: 470, tax: 47, total: 517 }

const formatAmount = (value: number) => `$${value.toFixed(2)}`

const initialFormState = {
    email: 'you@example.com',
    cardholderName: 'Jordan Blake',
    cardNumber: '1234 1234 1234 1234',
    expiry: '01 / 29',
    securityCode: '123',
    address: '100 Market Street',
    saveDetails: true,
}

const joinedGroup = [
    'flex flex-col',
    '[&_.input]:relative [&_.select]:relative',
    '[&_.input:focus]:z-20 [&_.input:focus-within]:z-20',
    '[&_.select.select-menu-open]:z-30',
].join(' ')

export default function DialogSubscriptionCheckout() {
    const [isOpen, setIsOpen] = useState(true)
    const [isAnnual, setIsAnnual] = useState(false)
    const [email, setEmail] = useState(initialFormState.email)
    const [cardholderName, setCardholderName] = useState(
        initialFormState.cardholderName,
    )
    const [cardNumber, setCardNumber] = useState(initialFormState.cardNumber)
    const [expiry, setExpiry] = useState(initialFormState.expiry)
    const [securityCode, setSecurityCode] = useState(
        initialFormState.securityCode,
    )
    const [country, setCountry] = useState<CountryOption>(countries[0])
    const [address, setAddress] = useState(initialFormState.address)
    const [saveDetails, setSaveDetails] = useState(
        initialFormState.saveDetails,
    )

    const amount = isAnnual ? annualAmount : monthlyAmount

    const handleOpen = () => {
        setIsAnnual(false)
        setEmail(initialFormState.email)
        setCardholderName(initialFormState.cardholderName)
        setCardNumber(initialFormState.cardNumber)
        setExpiry(initialFormState.expiry)
        setSecurityCode(initialFormState.securityCode)
        setCountry(countries[0])
        setAddress(initialFormState.address)
        setSaveDetails(initialFormState.saveDetails)
        setIsOpen(true)
    }

    const handleClose = () => setIsOpen(false)

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={handleOpen}>
                Open dialog
            </Button>

            <Dialog
                aria-labelledby={dialogTitleId}
                className="flex max-h-[calc(100dvh-2rem)] w-full flex-col overflow-hidden p-0"
                isOpen={isOpen}
                lockScroll
                width={900}
                onClose={handleClose}
            >
                <div className="flex min-h-0 flex-1 flex-col overflow-y-auto md:flex-row">
                    <div className="p-2">
                        <div className="flex flex-col bg-background p-8 md:w-[380px] rounded-card border">
                            <div className="flex items-center gap-2">
                                <Button
                                    aria-label="Back"
                                    icon={<PiArrowLeft aria-hidden="true" />}
                                    size="sm"
                                    type="button"
                                    variant="ghost"
                                    onClick={handleClose}
                                />
                                <div>
                                    <h5
                                        className="text-lg font-semibold text-foreground"
                                        id={dialogTitleId}
                                    >
                                        Order summary
                                    </h5>
                                </div>
                            </div>

                            <div className="mt-8 flex items-end gap-2">
                                <IconFrame size={36} variant="elavated">
                                    <img
                                        alt=""
                                        className="h-6 w-6 object-contain"
                                        src="https://statics.nateui.com/img/thumbs/plans/pro.svg"
                                    />
                                </IconFrame>
                                <div className="flex items-end gap-2">
                                    <span className="text-2xl font-semibold ">
                                        {formatAmount(amount.subtotal)}
                                    </span>
                                    <span className="flex mb-1 text-xs text-muted-foreground">
                                        <span>per {isAnnual ? 'year' : 'month'}</span>
                                    </span>
                                </div>
                            </div>
                            <p className="mt-4 text-muted-foreground">
                                Includes unlimited projects and priority support.
                            </p>

                            <div className="mt-8 rounded-card border bg-card p-4">
                                <div className="flex items-center justify-between gap-4">
                                    <div className="min-w-0">
                                        <p className="truncate font-medium text-card-foreground">
                                            Pro plan
                                        </p>
                                        <p className="truncate text-muted-foreground">
                                            {isAnnual
                                                ? 'Billed annually'
                                                : 'Billed monthly'}
                                        </p>
                                    </div>
                                    <p className="shrink-0 text-card-foreground tabular-nums">
                                        {formatAmount(amount.subtotal)}
                                    </p>
                                </div>

                                <Divider className="my-4" />

                                <div className="flex items-center gap-2">
                                    <Switcher
                                        aria-label="Bill annually"
                                        checked={isAnnual}
                                        onChange={setIsAnnual}
                                    />
                                    <Tag>Save 20%</Tag>
                                    <span className="min-w-0 flex-1 truncate text-muted-foreground">
                                        Bill annually
                                    </span>
                                    <span className="shrink-0 text-card-foreground tabular-nums">
                                        {formatAmount(annualAmount.subtotal)}
                                    </span>
                                </div>
                            </div>

                            <div className="mt-8 flex flex-col divide-y">
                                <div className="flex items-center justify-between gap-4 py-4">
                                    <span>
                                        Subtotal
                                    </span>
                                    <span className="tabular-nums">
                                        {formatAmount(amount.subtotal)}
                                    </span>
                                </div>
                                <div className="py-4">
                                    <Link
                                        href="#promo-code"
                                    >
                                        Add promo code
                                    </Link>
                                </div>
                                <div className="flex items-center justify-between gap-4 py-4">
                                    <span className="flex items-center gap-1">
                                        Tax
                                        <Tooltip title="Calculated from your billing address">
                                            <PiInfo
                                                aria-label="How tax is calculated"
                                                className="text-sm"
                                                tabIndex={0}
                                            />
                                        </Tooltip>
                                    </span>
                                    <span className="tabular-nums">
                                        {formatAmount(amount.tax)}
                                    </span>
                                </div>
                                <div className="flex items-center justify-between gap-4 py-4">
                                    <span className="font-semibold">Total</span>
                                    <span className="font-semibold tabular-nums">
                                        {formatAmount(amount.total)}
                                    </span>
                                </div>
                            </div>

                            <div className="mt-auto flex items-center gap-4 pt-8">
                                <Link
                                    className="text-muted-foreground hover:text-foreground"
                                    href="#terms"
                                >
                                    Terms of service
                                </Link>
                                <Link
                                    className="text-muted-foreground hover:text-foreground"
                                    href="#privacy"
                                >
                                    Privacy policy
                                </Link>
                            </div>
                        </div>
                    </div>

                    <div className="flex flex-col p-8 pe-12 md:min-w-0 md:flex-1">
                        <Form onSubmit={(event) => event.preventDefault()}>
                            <h5 className="text-lg font-semibold text-foreground">
                                Payment details
                            </h5>

                            <div className="mt-4">
                                <Form.Field
                                    htmlFor={emailFieldId}
                                    label="Email"
                                >
                                    <Input
                                        id={emailFieldId}
                                        type="email"
                                        value={email}
                                        onChange={(event) =>
                                            setEmail(event.target.value)
                                        }
                                    />
                                </Form.Field>

                                <Form.Field
                                    htmlFor={cardholderFieldId}
                                    label="Cardholder name"
                                >
                                    <Input
                                        id={cardholderFieldId}
                                        value={cardholderName}
                                        onChange={(event) =>
                                            setCardholderName(
                                                event.target.value,
                                            )
                                        }
                                    />
                                </Form.Field>

                                <Form.Field
                                    aria-labelledby={cardDetailsLabelId}
                                    label="Card details"
                                    labelId={cardDetailsLabelId}
                                    role="group"
                                >
                                    <div className={joinedGroup}>
                                        <Input
                                            aria-label="Card number"
                                            className="rounded-b-none"
                                            inputMode="numeric"
                                            suffix={
                                                <PiCreditCard
                                                    aria-hidden="true"
                                                    className="text-base text-muted-foreground"
                                                />
                                            }
                                            value={cardNumber}
                                            onChange={(event) =>
                                                setCardNumber(
                                                    event.target.value,
                                                )
                                            }
                                        />
                                        <div className="-mt-px flex">
                                            <Input
                                                aria-label="Expiration date"
                                                className="min-w-0 rounded-t-none rounded-br-none"
                                                inputMode="numeric"
                                                value={expiry}
                                                onChange={(event) =>
                                                    setExpiry(
                                                        event.target.value,
                                                    )
                                                }
                                            />
                                            <Input
                                                aria-label="Security code"
                                                className="-ml-px min-w-0 rounded-t-none rounded-bl-none"
                                                inputMode="numeric"
                                                value={securityCode}
                                                onChange={(event) =>
                                                    setSecurityCode(
                                                        event.target.value,
                                                    )
                                                }
                                            />
                                        </div>
                                    </div>
                                </Form.Field>

                                <Form.Field
                                    aria-labelledby={countryLabelId}
                                    label="Billing address"
                                    labelId={countryLabelId}
                                    role="group"
                                >
                                    <div className={joinedGroup}>
                                        <Select
                                            className="rounded-b-none"
                                            customInputDisplay={(selected) => (
                                                <Select.ValueWithPrefix
                                                    label={selected?.label}
                                                    prefix={countryFlag(
                                                        selected?.value ??
                                                            'US',
                                                    )}
                                                />
                                            )}
                                            customOption={({
                                                option,
                                                selected,
                                                CheckIcon,
                                            }) => (
                                                <Select.OptionWithPrefix
                                                    checkIcon={CheckIcon}
                                                    label={option.label}
                                                    prefix={countryFlag(
                                                        option.value,
                                                    )}
                                                    selected={selected}
                                                />
                                            )}
                                            inputId={countryLabelId}
                                            options={countries}
                                            value={country}
                                            onChange={(option) =>
                                                setCountry(option)
                                            }
                                        />
                                        <Input
                                            aria-label="Street address"
                                            className="-mt-px rounded-t-none"
                                            value={address}
                                            onChange={(event) =>
                                                setAddress(
                                                    event.target.value,
                                                )
                                            }
                                        />
                                    </div>
                                </Form.Field>

                                <div className="rounded-card border p-4">
                                    <Checkbox
                                        checked={saveDetails}
                                        className="w-full items-start [&>span:first-child]:h-5"
                                        contentClass="min-w-0 flex-1"
                                        onChange={setSaveDetails}
                                    >
                                        <span className="flex flex-col gap-1">
                                            <span className="font-medium text-card-foreground">
                                                Save payment details
                                            </span>
                                            <span className="font-normal text-muted-foreground">
                                                Use this card for future
                                                renewals.
                                            </span>
                                        </span>
                                    </Checkbox>
                                </div>
                            </div>

                            <Button
                                block
                                className="mt-8"
                                type="button"
                                variant="solid"
                                onClick={handleClose}
                            >
                                Subscribe for {formatAmount(amount.total)}
                            </Button>
                        </Form>
                    </div>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 08

Preview
npx nateui@latest add DialogConnectAccount
Dark
import { useState } from 'react'
import Avatar from '@/components/ui/Avatar'
import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import Link from '@/components/ui/Link'
import Divider from '@/components/composites/Divider'
import { PiLightning, PiShieldCheck } from 'react-icons/pi'

const assetBase = 'https://statics.nateui.com/img'
const dialogTitleId = 'dialog-connect-account-title'

export default function DialogConnectAccount() {
    const [isOpen, setIsOpen] = useState(true)

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={() => setIsOpen(true)}>
                Open dialog
            </Button>

            <Dialog
                aria-labelledby={dialogTitleId}
                isOpen={isOpen}
                lockScroll
                width={380}
                onClose={() => setIsOpen(false)}
            >
                <div className="mt-12 flex justify-center">
                    <Avatar.Group chained>
                        <Avatar
                            alt="NateUI"
                            className="bg-white p-1"
                            src={`${assetBase}/logos/logo-collapsed.svg`}
                            size={52}
                        />
                        <Avatar
                            alt="Stripe"
                            className="bg-white p-1"
                            src={`${assetBase}/thumbs/brands/stripe.png`}
                            size={52}
                        />
                    </Avatar.Group>
                </div>

                <div
                    className="mt-4 text-center text-lg px-8"
                    id={dialogTitleId}
                >
                    NateUI uses <span className="font-bold">Stripe</span> to
                    connect your bank
                </div>

                <div className="mt-8 rounded-card border p-4">
                    <div className="flex items-start gap-4">
                        <div className="mt-1">
                            <PiLightning
                                aria-hidden="true"
                                className="shrink-0 text-lg text-foreground"
                            />
                        </div>
                        <div className="min-w-0">
                            <p className="font-semibold">Linked in a few taps</p>
                            <p className="mt-1 text-muted-foreground">
                                Pick your institution, sign in once, and the
                                connection stays live.
                            </p>
                        </div>
                    </div>

                    <div className="mt-4 flex items-start gap-4">
                        <div className="mt-1">
                            <PiShieldCheck
                                aria-hidden="true"
                                className="shrink-0 text-lg text-foreground"
                            />
                        </div>
                        <div className="min-w-0">
                            <p className="font-semibold">
                                Your credentials stay private
                            </p>
                            <p className="mt-1 text-muted-foreground">
                                Sign-in happens on your bank&apos;s side. NateUI
                                only ever sees the balances you approve.
                            </p>
                        </div>
                    </div>
                </div>

                <Divider className="-mx-4 mb-0 mt-8 w-auto" />

                <p
                    className="mt-4 text-center text-xs text-muted-foreground"
                >
                    Continuing means you accept Stripe's<br />
                    <Link className="underline outline-none" href="#">
                        Privacy Policy
                    </Link>{' '}
                    and{' '}
                    <Link className="underline  outline-none" href="#">
                        Term
                    </Link>
                    .
                </p>

                <Button
                    block
                    type="button"
                    variant="solid"
                    className="mt-4"
                    onClick={() => setIsOpen(false)}
                >
                    Get started
                </Button>
            </Dialog>
        </main>
    )
}

Dialog 09

Preview
npx nateui@latest add DialogCommandPalette
Dark
import { useState } from 'react'
import Search from '@/components/patterns/Search'
import type { SearchData, SearchResult } from '@/components/patterns/Search'
import {
    PiFiles,
    PiLightning,
    PiUser,
} from 'react-icons/pi'

const assetBase = 'https://statics.nateui.com/img'

const defaultQuickActions: SearchResult[] = [
    {
        title: 'Favourites',
        data: [
            {
                type: 'quickAction',
                title: 'New document',
                icon: 'document',
                path: '#new-document',
                shortcut: 'N',
            },
            {
                type: 'quickAction',
                title: 'Save workspace',
                icon: 'task',
                path: '#save-workspace',
                shortcut: '⌘+S',
            },
        ],
    },
    {
        title: 'Actions',
        data: [
            {
                type: 'quickAction',
                title: 'Add sales funnel',
                icon: 'product',
                path: '#add-sales-funnel',
            },
            {
                type: 'quickAction',
                title: 'Find broken links',
                icon: 'settings',
                path: '#find-broken-links',
            },
            {
                type: 'quickAction',
                title: 'Draft release notes',
                icon: 'note',
                path: '#draft-release-notes',
            },
        ],
    },
    {
        title: 'Files',
        data: [
            {
                type: 'files',
                title: 'Q3 pipeline export',
                fileType: 'xlsx',
                path: '#q3-pipeline-export',
            },
            {
                type: 'files',
                title: 'Onboarding checklist',
                fileType: 'pdf',
                path: '#onboarding-checklist',
            },
        ],
    },
    {
        title: 'People',
        data: [
            {
                type: 'profile',
                img: `${assetBase}/avatars/thumb-1.jpg`,
                id: 'maya-chen',
                title: 'Maya Chen',
                subtitle: 'Operations lead',
                path: '#maya-chen',
            },
            {
                type: 'profile',
                img: `${assetBase}/avatars/thumb-2.jpg`,
                id: 'noah-patel',
                title: 'Noah Patel',
                subtitle: 'Product manager',
                path: '#noah-patel',
            },
        ],
    },
]

const categoryFilters = [
    {
        value: 'profile',
        label: 'People',
        icon: <PiUser className="text-sm" />,
    },
    {
        value: 'files',
        label: 'Files',
        icon: <PiFiles className="text-sm" />,
    },
    {
        value: 'quickAction',
        label: 'Actions',
        icon: <PiLightning className="text-sm" />,
    },
]

const itemMatchesQuery = (item: SearchData, query: string) => {
    const searchableFields = [item.title]

    if (item.type === 'profile') {
        searchableFields.push(item.subtitle)
    }

    if (item.type === 'files') {
        searchableFields.push(item.fileType)
    }

    return searchableFields.some((field) =>
        field.toLowerCase().includes(query),
    )
}

export default function DialogCommandPalette() {
    const [searchResults, setSearchResults] = useState<
        SearchResult[] | undefined
    >(undefined)

    const handleQueryChange = (query: string) => {
        const normalizedQuery = query.trim().toLowerCase()

        if (!normalizedQuery) {
            setSearchResults(undefined)
            return
        }

        setSearchResults(
            defaultQuickActions
                .map((result) => ({
                    ...result,
                    data: result.data.filter((item) =>
                        itemMatchesQuery(item, normalizedQuery),
                    ),
                }))
                .filter((result) => result.data.length > 0),
        )
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Search
                defaultOpen
                trigger="input"
                data={searchResults}
                defaultQuickActions={defaultQuickActions}
                categoryFilters={categoryFilters}
                onQueryChange={handleQueryChange}
                onNavigate={() => {}}
                classNames={{ root: 'w-full max-w-md' }}
            />
        </main>
    )
}

Dialog 10

Preview
npx nateui@latest add DialogBugReport
Dark
import { useState } from 'react'
import Button from '@/components/ui/Button'
import CloseButton from '@/components/ui/CloseButton'
import Dialog from '@/components/ui/Dialog'
import Form from '@/components/ui/Form'
import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import Tooltip from '@/components/ui/Tooltip'
import Upload from '@/components/ui/Upload'
import IconFrame from '@/components/composites/IconFrame'
import classNames from '@/utils/classNames'
import { PiBug, PiCloudArrowUp, PiQuestion } from 'react-icons/pi'

const dialogTitleId = 'dialog-bug-report-title'
const titleFieldId = 'dialog-bug-report-title-field'
const descriptionFieldId = 'dialog-bug-report-description'
const pathFieldId = 'dialog-bug-report-path'
const priorityFieldId = 'dialog-bug-report-priority'

type PriorityOption = {
    label: string
    value: string
    dotClassName: string
}

const priorityOptions: PriorityOption[] = [
    { label: 'Low', value: 'low', dotClassName: 'bg-success' },
    { label: 'Medium', value: 'medium', dotClassName: 'bg-warning' },
    { label: 'High', value: 'high', dotClassName: 'bg-destructive' },
]

const PriorityDot = ({ className }: { className: string }) => (
    <span className={classNames('size-2 rounded-full', className)} />
)

export default function DialogBugReport() {
    const [isOpen, setIsOpen] = useState(true)
    const [issueTitle, setIssueTitle] = useState('')
    const [issueDescription, setIssueDescription] = useState('')
    const [pagePath, setPagePath] = useState('')
    const [priority, setPriority] = useState<PriorityOption>(
        priorityOptions[0],
    )
    const [isDragActive, setIsDragActive] = useState(false)

    const handleOpen = () => {
        setIssueTitle('')
        setIssueDescription('')
        setPagePath('')
        setPriority(priorityOptions[0])
        setIsDragActive(false)
        setIsOpen(true)
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={handleOpen}>
                Open dialog
            </Button>

            <Dialog
                aria-labelledby={dialogTitleId}
                className="flex max-h-[calc(100dvh-2rem)] w-full flex-col overflow-hidden p-0"
                isOpen={isOpen}
                lockScroll
                width={540}
                closable={false}
                onClose={() => setIsOpen(false)}
            >
                <div className="flex item-start justify-between gap-2 p-4 border-b">
                    <div className="flex items-center gap-2 pe-12">
                        <IconFrame variant="layered">
                            <PiBug
                                aria-hidden="true"
                                className="text-xl text-foreground"
                            />
                        </IconFrame>
                        <div>
                            <h5
                                className="text-lg font-semibold"
                                id={dialogTitleId}
                            >
                                Report a bug
                            </h5>
                            <p className="text-muted-foreground">
                                Describe what happened so the team can take a
                                look.
                            </p>
                        </div>
                    </div>
                    <div>
                        <CloseButton onClick={() => setIsOpen(false)} />
                    </div>
                </div>

                <div className="min-h-0 flex-1 overflow-y-auto p-4">
                    <Form onSubmit={(event) => event.preventDefault()}>
                        <Form.Field
                            htmlFor={titleFieldId}
                            label="Title"
                        >
                            <Input
                                id={titleFieldId}
                                placeholder="Enter issue title"
                                value={issueTitle}
                                onChange={(event) =>
                                    setIssueTitle(event.target.value)
                                }
                            />
                        </Form.Field>

                        <Form.Field
                            htmlFor={descriptionFieldId}
                            label="Description"
                        >
                            <Input
                                id={descriptionFieldId}
                                placeholder="Enter issue description"
                                rows={4}
                                textArea
                                value={issueDescription}
                                onChange={(event) =>
                                    setIssueDescription(event.target.value)
                                }
                            />
                        </Form.Field>

                        <div className="grid grid-cols-1 gap-x-4 sm:grid-cols-2">
                            <Form.Field
                                htmlFor={pathFieldId}
                                label="URL or path"
                            >
                                <Input
                                    id={pathFieldId}
                                    placeholder="Enter issue related URL"
                                    suffix={
                                        <Tooltip title="The page or route where this happened">
                                            <PiQuestion
                                                aria-label="What to enter here"
                                                className="text-base"
                                                tabIndex={0}
                                            />
                                        </Tooltip>
                                    }
                                    value={pagePath}
                                    onChange={(event) =>
                                        setPagePath(event.target.value)
                                    }
                                />
                            </Form.Field>

                            <Form.Field
                                htmlFor={priorityFieldId}
                                label="Priority"
                            >
                                <Select<{ dotClassName: string }>
                                    customInputDisplay={(selectedItem) =>
                                        selectedItem ? (
                                            <Select.ValueWithPrefix
                                                label={selectedItem.label}
                                                prefix={
                                                    <PriorityDot
                                                        className={
                                                            selectedItem.dotClassName
                                                        }
                                                    />
                                                }
                                            />
                                        ) : null
                                    }
                                    customOption={({
                                        option,
                                        selected,
                                        CheckIcon,
                                    }) => (
                                        <Select.OptionWithPrefix
                                            checkIcon={CheckIcon}
                                            label={option.label}
                                            prefix={
                                                <PriorityDot
                                                    className={
                                                        option.dotClassName
                                                    }
                                                />
                                            }
                                            selected={selected}
                                        />
                                    )}
                                    inputId={priorityFieldId}
                                    options={priorityOptions}
                                    value={priority}
                                    onChange={(option) => setPriority(option)}
                                />
                            </Form.Field>
                        </div>

                        <Form.Field label="Attachments">
                            <Upload
                                showList={false}
                                className={classNames(
                                    'w-full cursor-pointer rounded-control border-2 border-dashed',
                                    isDragActive && 'border-primary',
                                )}
                                onDragLeave={() => setIsDragActive(false)}
                                onDragOver={(event) => {
                                    event.preventDefault()
                                    setIsDragActive(true)
                                }}
                                onDrop={(event) => {
                                    event.preventDefault()
                                    setIsDragActive(false)
                                }}
                            >
                                <div className="flex w-full flex-col items-center gap-1 px-4 py-8 text-center">
                                    <PiCloudArrowUp
                                        aria-hidden="true"
                                        className="text-2xl"
                                    />
                                    <p className="mt-2">
                                        Drop a file here, or choose one to
                                        upload.
                                    </p>
                                    <p className="text-xs text-muted-foreground">
                                        Accepts PNG, JPG, or PDF files up to 5
                                        MB
                                    </p>
                                    <Button
                                        className="mt-2"
                                        size="sm"
                                        type="button"
                                    >
                                        Browse files
                                    </Button>
                                </div>
                            </Upload>
                        </Form.Field>
                    </Form>
                </div>

                <div className="flex items-center justify-between gap-2 border-t p-4">
                    <Button type="button" onClick={() => setIsOpen(false)}>
                        Cancel
                    </Button>
                    <Button
                        type="button"
                        variant="solid"
                        onClick={() => setIsOpen(false)}
                    >
                        Submit report
                    </Button>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 11

Preview
npx nateui@latest add DialogInviteMembers
Dark
import { useState } from 'react'
import Avatar from '@/components/ui/Avatar'
import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import Dropdown from '@/components/ui/Dropdown'
import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import Tag from '@/components/ui/Tag'
import Divider from '@/components/composites/Divider'
import { PiDotsThreeOutlineVerticalBold } from 'react-icons/pi'

const assetBase = 'https://statics.nateui.com/img'

const dialogTitleId = 'dialog-invite-members-title'
const dialogDescriptionId = 'dialog-invite-members-description'
const roleSelectId = 'dialog-invite-members-role'

type RoleOption = {
    label: string
    value: string
}

const roleOptions: RoleOption[] = [
    { label: 'Member', value: 'member' },
    { label: 'Admin', value: 'admin' },
]

type ActiveMember = {
    id: string
    name: string
    email: string
    role: string
    avatarSrc: string
}

const activeMembers: ActiveMember[] = [
    {
        id: 'morgan',
        name: 'Morgan Ellis',
        email: 'morgan@example.com',
        role: 'Admin',
        avatarSrc: `${assetBase}/avatars/thumb-1.jpg`,
    },
    {
        id: 'priya',
        name: 'Priya Nandan',
        email: 'priya@example.com',
        role: 'Member',
        avatarSrc: `${assetBase}/avatars/thumb-2.jpg`,
    },
    {
        id: 'sam',
        name: 'Sam Whitfield',
        email: 'sam@example.com',
        role: 'Member',
        avatarSrc: `${assetBase}/avatars/thumb-3.jpg`,
    },
    {
        id: 'casey',
        name: 'Casey Okafor',
        email: 'casey@example.com',
        role: 'Member',
        avatarSrc: `${assetBase}/avatars/thumb-4.jpg`,
    },
]

type PendingMember = {
    id: string
    name: string
    email: string
    role: string
    initials: string
}

const pendingMembers: PendingMember[] = [
    {
        id: 'devon',
        name: 'Devon Blake',
        email: 'devon@example.com',
        role: 'Member',
        initials: 'DB',
    },
    {
        id: 'harper',
        name: 'Harper Lin',
        email: 'harper@example.com',
        role: 'Member',
        initials: 'HL',
    },
]

export default function DialogInviteMembers() {
    const [isOpen, setIsOpen] = useState(true)
    const [inviteEmail, setInviteEmail] = useState('')
    const [selectedRole, setSelectedRole] = useState<RoleOption>(
        roleOptions[0],
    )

    const handleOpen = () => {
        setInviteEmail('')
        setSelectedRole(roleOptions[0])
        setIsOpen(true)
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={handleOpen}>
                Open dialog
            </Button>

            <Dialog
                aria-describedby={dialogDescriptionId}
                aria-labelledby={dialogTitleId}
                className="flex max-h-[calc(100dvh-2rem)] w-full flex-col overflow-hidden p-0"
                isOpen={isOpen}
                lockScroll
                width={600}
                onClose={() => setIsOpen(false)}
            >
                <div className="p-4 pe-12">
                    <h5
                        className="text-lg font-semibold"
                        id={dialogTitleId}
                    >
                        Team access
                    </h5>
                    <p
                        className="mt-1 text-muted-foreground"
                        id={dialogDescriptionId}
                    >
                        Invite new teammates by email and see who already has
                        access to this workspace.
                    </p>
                </div>

                <div className="min-h-0 flex-1 overflow-y-auto p-4">
                    <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
                        <Input
                            aria-label="Email address"
                            className="sm:flex-1"
                            placeholder="name@company.com"
                            type="email"
                            value={inviteEmail}
                            onChange={(event) =>
                                setInviteEmail(event.target.value)
                            }
                        />
                        <span className="sr-only" id={roleSelectId}>
                            Role
                        </span>
                        <Select
                            className="sm:w-40"
                            inputId={roleSelectId}
                            options={roleOptions}
                            value={selectedRole}
                            onChange={(option) => setSelectedRole(option)}
                        />
                        <Button type="button" variant="solid">
                            Send invite
                        </Button>
                    </div>

                    <div className="mt-8">
                        <h6 className="text-base font-medium text-foreground">
                            Current members
                        </h6>
                        <Divider className="mt-2 mb-0" />
                    </div>

                    <div className="mt-2 divide-y">
                        {activeMembers.map((member) => (
                            <div
                                className="flex items-center gap-3 py-3"
                                key={member.id}
                            >
                                <Avatar
                                    alt={member.name}
                                    size="sm"
                                    src={member.avatarSrc}
                                />
                                <div className="min-w-0 flex-1">
                                    <p className="truncate font-medium text-foreground">
                                        {member.name}
                                    </p>
                                    <p className="truncate text-muted-foreground">
                                        {member.email}
                                    </p>
                                </div>
                                <Tag size="sm">{member.role}</Tag>
                                <Dropdown
                                    placement="bottom-end"
                                    renderTitle={
                                        <Button
                                            aria-label={`More actions for ${member.name}`}
                                            icon={
                                                <PiDotsThreeOutlineVerticalBold />
                                            }
                                            size="sm"
                                            variant="ghost"
                                        />
                                    }
                                >
                                    <Dropdown.Item>Change role</Dropdown.Item>
                                    <Dropdown.Item>
                                        Remove member
                                    </Dropdown.Item>
                                </Dropdown>
                            </div>
                        ))}
                    </div>

                    <div className="mt-8">
                        <h6 className="text-base font-medium text-foreground">
                            Pending invitations
                        </h6>
                        <Divider className="mt-2 mb-0" />
                    </div>

                    <div className="mt-2 divide-y">
                        {pendingMembers.map((member) => (
                            <div
                                className="flex items-center gap-3 py-3"
                                key={member.id}
                            >
                                <Avatar
                                    className="border-dashed"
                                    size="sm"
                                >
                                    {member.initials}
                                </Avatar>
                                <div className="min-w-0 flex-1">
                                    <p className="truncate font-medium text-foreground">
                                        {member.name}
                                    </p>
                                    <p className="truncate text-muted-foreground">
                                        {member.email}
                                    </p>
                                </div>
                                <Tag size="sm">{member.role}</Tag>
                                <Dropdown
                                    placement="bottom-end"
                                    renderTitle={
                                        <Button
                                            aria-label={`More actions for ${member.name}`}
                                            icon={
                                                <PiDotsThreeOutlineVerticalBold />
                                            }
                                            size="sm"
                                            variant="ghost"
                                        />
                                    }
                                >
                                    <Dropdown.Item>
                                        Resend invite
                                    </Dropdown.Item>
                                    <Dropdown.Item>
                                        Cancel invitation
                                    </Dropdown.Item>
                                </Dropdown>
                            </div>
                        ))}
                    </div>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 12

Preview
npx nateui@latest add DialogTaskDetails
Dark
import {
    Fragment,
    useCallback,
    useEffect,
    useImperativeHandle,
    useMemo,
    useRef,
    useState,
} from 'react'
import type { ComponentProps, ReactNode, Ref } from 'react'
import dayjs from 'dayjs'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Avatar from '@/components/ui/Avatar'
import Button from '@/components/ui/Button'
import Calendar from '@/components/ui/Calendar'
import Card from '@/components/ui/Card'
import Checkbox from '@/components/ui/Checkbox'
import Dialog from '@/components/ui/Dialog'
import Popover from '@/components/ui/Popover'
import Scroll from '@/components/ui/Scroll'
import Tabs from '@/components/ui/Tabs'
import Tag from '@/components/ui/Tag'
import Divider from '@/components/composites/Divider'
import EmptyState from '@/components/composites/EmptyState'
import FileIcon from '@/components/composites/FileIcon'
import RichTextEditor from '@/components/composites/RichTextEditor'
import UsersAvatarGroup from '@/components/composites/UsersAvatarGroup'
import classNames from '@/utils/classNames'
import sleep from '@/utils/sleep'
import {
    PiArrowsDownUp,
    PiBookmarkSimple,
    PiCalendarBlank,
    PiChatCircleSlash,
    PiCheck,
    PiCheckSquare,
    PiClock,
    PiDownloadSimple,
    PiFileText,
    PiPaperPlaneTilt,
    PiTag,
    PiUser,
    PiX,
} from 'react-icons/pi'

const assetBase = 'https://statics.nateui.com/img'
const dialogTitleId = 'dialog-task-details-title'

type Priority = 'urgent' | 'high' | 'normal' | 'low'

type Member = {
    id: string
    name: string
    img: string
}

type Comment = {
    id: string
    author: Member
    createdAt: string
    message: string
}

type Subtask = {
    id: string
    title: string
    assigneeIds: string[]
    priority: Priority
    dueDate: string
    completed: boolean
}

type Attachment = {
    id: string
    name: string
    type: string
    size: string
}

const members: Member[] = [
    {
        id: 'mara',
        name: 'Mara Chen',
        img: `${assetBase}/avatars/thumb-1.jpg`,
    },
    {
        id: 'jonah',
        name: 'Jonah Ellis',
        img: `${assetBase}/avatars/thumb-7.jpg`,
    },
    {
        id: 'nina',
        name: 'Nina Patel',
        img: `${assetBase}/avatars/thumb-12.jpg`,
    },
]

const priorities: Array<{
    value: Priority
    label: string
    dotClass: string
}> = [
    { value: 'urgent', label: 'Urgent', dotClass: 'bg-destructive' },
    { value: 'high', label: 'High', dotClass: 'bg-warning' },
    { value: 'normal', label: 'Normal', dotClass: 'bg-info' },
    { value: 'low', label: 'Low', dotClass: 'bg-muted-foreground' },
]

const tagOptions = [
    { id: 'launch', label: 'Launch', dotClass: 'bg-primary' },
    { id: 'research', label: 'Research', dotClass: 'bg-info' },
    { id: 'design', label: 'Design', dotClass: 'bg-warning' },
    { id: 'blocked', label: 'Blocked', dotClass: 'bg-destructive' },
    { id: 'venue', label: 'Venue', dotClass: 'bg-success' },
    { id: 'follow-up', label: 'Follow-up', dotClass: 'bg-muted-foreground' },
]

const initialComments: Comment[] = [
    {
        id: 'comment-1',
        author: members[1],
        createdAt: '2026-08-12T09:24:00.000Z',
        message:
            'The event outline is ready for review. I added the open questions to the checklist.',
    },
    {
        id: 'comment-2',
        author: members[2],
        createdAt: '2026-08-12T13:40:00.000Z',
        message:
            'I can take the follow-up with the venue once the final guest count is confirmed.',
    },
]

const initialSubtasks: Subtask[] = [
    {
        id: 'subtask-1',
        title: 'Confirm speaker availability',
        assigneeIds: ['jonah'],
        priority: 'high',
        dueDate: '2026-08-18T00:00:00.000Z',
        completed: true,
    },
    {
        id: 'subtask-2',
        title: 'Share the event outline',
        assigneeIds: ['nina'],
        priority: 'normal',
        dueDate: '2026-08-20T00:00:00.000Z',
        completed: false,
    },
    {
        id: 'subtask-3',
        title: 'Publish the attendee reminder',
        assigneeIds: ['mara'],
        priority: 'low',
        dueDate: '2026-08-22T00:00:00.000Z',
        completed: false,
    },
]

const initialAttachments: Attachment[] = [
    { id: 'attachment-1', name: 'event-outline.pdf', type: 'pdf', size: '1.8 MB' },
    { id: 'attachment-2', name: 'speaker-notes.docx', type: 'docx', size: '640 KB' },
    { id: 'attachment-3', name: 'venue-layout.png', type: 'png', size: '2.4 MB' },
]

const getMember = (id: string) =>
    members.find((member) => member.id === id) ?? members[0]

type FieldOptions = ComponentProps<'div'> & { label: string; icon: ReactNode }

const Field = ({ label, icon, children }: FieldOptions) => {
    return (
        <div className="flex items-start gap-2">
            <span className="flex min-h-10 shrink-0 items-center gap-1.5 min-w-[120px] sm:min-w-[150px] font-medium">
                <span className="text-base">{icon}</span>
                <span>{label}:</span>
            </span>
            <div className="flex min-w-0 flex-1 items-center">{children}</div>
        </div>
    )
}

const SelectorWraper = ({
    children,
    editable = true,
    wrap = false,
    className,
    ...rest
}: ComponentProps<'span'> & { editable?: boolean; wrap?: boolean }) => {
    return (
        <span
            className={classNames(
                'py-1 px-2 rounded-lg sm:min-w-[250px] inline-flex items-center max-w-[450px] h-full min-h-[40px] gap-2',
                wrap ? 'flex-wrap' : 'flex-nowrap',
                editable &&
                    'hover:bg-accent focus-within:bg-accent cursor-pointer',
                className,
            )}
            {...rest}
        >
            {children}
        </span>
    )
}

type PrioritySelectorOptions = ComponentProps<'span'> & {
    value?: string
    list: Array<{ value: string; label: string; indicator: string | ReactNode }>
    onValueChange: (value: string) => void
}

const PrioritySelector = ({
    children,
    list,
    value,
    onValueChange,
}: PrioritySelectorOptions) => {
    const [popoverOpen, setPopoverOpen] = useState(false)

    return (
        <Popover
            renderTrigger={children}
            open={popoverOpen}
            placement="bottom-start"
            onOpenChange={setPopoverOpen}
            className="p-1"
            width={220}
        >
            <ul>
                {list.map((item) => (
                    <li
                        className={classNames(
                            'flex items-center justify-between cursor-pointer font-medium px-3 rounded-md w-full whitespace-nowrap gap-x-2 transition-colors duration-150 text-foreground hover:bg-accent h-[36px]',
                            item.value === value && 'font-semibold',
                        )}
                        key={item.value}
                        onClick={() => {
                            onValueChange(item.value)
                            setPopoverOpen(false)
                        }}
                        role="option"
                        aria-selected={item.value === value}
                        tabIndex={-1}
                    >
                        <span className="flex items-center gap-2">
                            {item.indicator}
                            <span>{item.label}</span>
                        </span>
                        {item.value === value && (
                            <PiCheck className="text-primary text-lg" />
                        )}
                    </li>
                ))}
            </ul>
        </Popover>
    )
}

type DuedateSelectorOptions = ComponentProps<'div'> & {
    value?: string
    onValueChange: (value: string) => void
}

const DuedateSelector = ({
    children,
    value,
    onValueChange,
}: DuedateSelectorOptions) => {
    const [datePickerOpen, setDatePickerOpen] = useState(false)

    const handleValueChange = (date: Date) => {
        onValueChange(dayjs(date).toISOString())
        setDatePickerOpen(false)
    }

    return (
        <Popover
            renderTrigger={children}
            open={datePickerOpen}
            placement="bottom-start"
            onOpenChange={setDatePickerOpen}
            style={{ width: 280 }}
        >
            <Calendar
                value={dayjs(value).toDate()}
                onChange={handleValueChange}
            />
        </Popover>
    )
}

type AssigneeSelectorOptions = ComponentProps<'span'> & {
    value?: string[]
    list: Array<{ value: string; label: string; indicator: string | ReactNode }>
    onValueChange: (value: string) => void
}

const AssigneeSelector = ({
    children,
    list,
    value,
    onValueChange,
}: AssigneeSelectorOptions) => {
    const [popoverOpen, setPopoverOpen] = useState(false)

    return (
        <Popover
            renderTrigger={children}
            open={popoverOpen}
            placement="bottom-start"
            onOpenChange={setPopoverOpen}
            className="p-1"
            style={{ width: 230 }}
        >
            <Scroll className="h-[170px]">
                <ul className="ltr:pr-1.5 rtl:pl-1.5">
                    {list.map((item) => (
                        <li
                            className={classNames(
                                'flex items-center justify-between cursor-pointer font-medium px-3 rounded-md w-full whitespace-nowrap gap-x-2 transition-colors duration-150 text-foreground hover:bg-accent h-[36px]',
                                value?.includes(item.value) && 'font-semibold',
                            )}
                            key={item.value}
                            onClick={() => {
                                onValueChange(item.value)
                            }}
                            role="option"
                            aria-selected={value?.includes(item.value)}
                            tabIndex={-1}
                        >
                            <span className="flex items-center gap-2">
                                {item.indicator}
                                <span>{item.label}</span>
                            </span>
                            {value?.includes(item.value) && (
                                <PiCheck className="text-primary text-lg" />
                            )}
                        </li>
                    ))}
                </ul>
            </Scroll>
        </Popover>
    )
}

type TagsSelectorOptions = ComponentProps<'span'> & {
    value?: string[]
    list: Array<{ value: string; label: string }>
    onValueChange: (value: string) => void
}

const TagsSelector = ({
    children,
    list,
    value,
    onValueChange,
}: TagsSelectorOptions) => {
    const [popoverOpen, setPopoverOpen] = useState(false)

    return (
        <Popover
            renderTrigger={children}
            open={popoverOpen}
            placement="bottom-start"
            onOpenChange={setPopoverOpen}
            className="p-1"
            width={220}
        >
            <Scroll className="h-[170px]">
                <ul className="ltr:pr-1.5 rtl:pl-1.5">
                    {list.map((item) => (
                        <li
                            className={classNames(
                                'flex items-center justify-between cursor-pointer font-medium px-3 rounded-md w-full whitespace-nowrap gap-x-2 transition-colors duration-150 text-foreground hover:bg-accent h-[36px]',
                                value?.includes(item.value) && 'font-semibold',
                            )}
                            key={item.value}
                            onClick={() => {
                                onValueChange(item.value)
                            }}
                            role="option"
                            aria-selected={value?.includes(item.value)}
                            tabIndex={-1}
                        >
                            <span className="flex items-center gap-2">
                                <span>{item.label}</span>
                            </span>
                            {value?.includes(item.value) && (
                                <PiCheck className="text-primary text-lg" />
                            )}
                        </li>
                    ))}
                </ul>
            </Scroll>
        </Popover>
    )
}

const SubjectEditor = ({
    className,
    value,
    onValueChange,
}: ComponentProps<'div'> & {
    value: string
    onValueChange: (value: string) => void
}) => {
    const [editing, setEditing] = useState(false)
    const inputRef = useRef<HTMLInputElement>(null)

    const handleEdit = async () => {
        setEditing(true)
        await sleep(10)
        inputRef.current?.focus()
    }

    const handleChange = () => {
        if (inputRef.current) {
            onValueChange(inputRef.current.value)
            setEditing(false)
            inputRef.current.blur()
        }
    }

    const handleClose = () => {
        setEditing(false)
        inputRef.current?.blur()
    }

    return (
        <>
            {editing ? (
                <span
                    className={classNames(
                        'flex items-center justify-between',
                        className,
                    )}
                >
                    <input
                        ref={inputRef}
                        defaultValue={value}
                        className="h-full w-full outline-0 min-h-[21px] leading-normal"
                        onBlur={handleClose}
                        onKeyDown={(e) => {
                            if (e.key === 'Enter') {
                                e.stopPropagation()
                                handleChange()
                            }
                        }}
                        onClick={(e) => {
                            e.stopPropagation()
                            inputRef.current?.focus()
                        }}
                    />
                    <span className="flex gap-2">
                        <button type="button" onMouseDown={handleChange}>
                            <PiCheck className="hover:text-success" />
                        </button>
                        <button
                            type="button"
                            onMouseDown={handleClose}
                            className="hover:text-destructive"
                        >
                            <PiX />
                        </button>
                    </span>
                </span>
            ) : (
                <span
                    onClick={handleEdit}
                    className={classNames('cursor-pointer', className)}
                >
                    {value}
                </span>
            )}
        </>
    )
}

type CommentInputRef = {
    handleFocus: () => void
}

type CommentInputOptions = {
    onSubmit: ({ message }: { message: string }) => void
    onCancel?: () => void
    onChange?: (value: string) => void
    className?: string
    ref?: Ref<CommentInputRef>
    extraTools?: ReactNode
}

const CommentInput = ({
    onSubmit,
    onChange,
    onCancel,
    className,
    extraTools,
    ref,
}: CommentInputOptions) => {
    const editor = useEditor({
        extensions: [
            StarterKit.configure({
                bulletList: {
                    keepMarks: true,
                },
                orderedList: {
                    keepMarks: true,
                },
            }),
        ],
        editorProps: {
            attributes: {
                class: 'focus:outline-hidden min-h-[75px] p-3 prose prose-p:text-sm',
            },
        },
        immediatelyRender: false,
        onUpdate: ({ editor: nextEditor }) => {
            const textContent = nextEditor.getText()
            onChange?.(textContent || '')
        },
    })

    const handleSubmit = () => {
        if (editor) {
            onSubmit({ message: editor.getText() })
            editor.commands.clearContent()
        }
    }

    const handleFocus = useCallback(() => {
        editor?.commands.focus()
    }, [editor])

    useImperativeHandle(ref, () => {
        return {
            handleFocus,
        }
    }, [handleFocus])

    return (
        <div
            className={classNames(
                'border border-border rounded-lg min-h-[120px] shadow flex flex-col',
                className,
            )}
        >
            <div>
                <div>
                    <EditorContent className="h-full" editor={editor} />
                </div>
                <div className="flex items-center justify-between px-4 py-2">
                    <div className="flex justify-center gap-x-1 gap-y-2">
                        {editor && (
                            <>
                                <RichTextEditor.ToolButtonBold editor={editor} />
                                <RichTextEditor.ToolButtonItalic editor={editor} />
                                <RichTextEditor.ToolButtonStrike editor={editor} />
                                <RichTextEditor.ToolButtonBulletList editor={editor} />
                                <RichTextEditor.ToolButtonOrderedList editor={editor} />
                                <RichTextEditor.ToolButtonHorizontalRule editor={editor} />
                                {extraTools}
                            </>
                        )}
                    </div>
                    <div className="flex items-center gap-2">
                        {onCancel && <Button onClick={onCancel}>Cancel</Button>}
                        <Button
                            size="sm"
                            variant="solid"
                            icon={<PiPaperPlaneTilt className="text-xl" />}
                            onClick={handleSubmit}
                        />
                    </div>
                </div>
            </div>
        </div>
    )
}

export default function DialogTaskDetails() {
    const [isOpen, setIsOpen] = useState(true)
    const [subject, setSubject] = useState('Prepare autumn customer event')
    const [priority, setPriority] = useState<Priority>('high')
    const [dueDate, setDueDate] = useState('2026-08-20T00:00:00.000Z')
    const [assigneeIds, setAssigneeIds] = useState<string[]>(['mara'])
    const [selectedTagIds, setSelectedTagIds] = useState<string[]>([
        'launch',
        'design',
    ])
    const [description, setDescription] = useState(
        'Coordinate the autumn customer event from the first speaker check-in through the attendee follow-up.',
    )
    const [isEditingDescription, setIsEditingDescription] = useState(false)
    const [editedDescription, setEditedDescription] = useState('')
    const [comments, setComments] = useState<Comment[]>(initialComments)
    const [subtasks, setSubtasks] = useState<Subtask[]>(initialSubtasks)
    const [attachments] = useState<Attachment[]>(initialAttachments)
    const [isSaving, setIsSaving] = useState(false)
    const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

    useEffect(() => {
        return () => {
            if (saveTimeoutRef.current !== null) {
                clearTimeout(saveTimeoutRef.current)
            }
        }
    }, [])

    const priorityOptions = useMemo(
        () =>
            priorities.map((item) => ({
                label: item.label,
                value: item.value,
                indicator: (
                    <span
                        className={classNames(
                            'h-2.5 w-2.5 rounded-xs',
                            item.dotClass,
                        )}
                    ></span>
                ),
            })),
        [],
    )

    const assigneeOptions = useMemo(
        () =>
            members.map((member) => ({
                label: member.name,
                value: member.id,
                indicator: (
                    <Avatar src={member.img} size={25} />
                ),
            })),
        [],
    )

    const tagsList = useMemo(
        () => tagOptions.map((tag) => ({ value: tag.id, label: tag.label })),
        [],
    )

    const handleCommentSubmit = ({ message }: { message: string }) => {
        setComments((currentComments) => [
            ...currentComments,
            {
                id: `comment-${currentComments.length + 1}`,
                author: members[0],
                message,
                createdAt: '2026-08-15T10:15:00.000Z',
            },
        ])
    }

    const handleSave = () => {
        if (saveTimeoutRef.current !== null) {
            clearTimeout(saveTimeoutRef.current)
        }
        setIsSaving(true)
        saveTimeoutRef.current = setTimeout(() => {
            setIsSaving(false)
            setIsOpen(false)
            saveTimeoutRef.current = null
        }, 500)
    }

    const handleSubTaskCheck = (id: string, checked: boolean) => {
        setSubtasks((currentSubtasks) =>
            currentSubtasks.map((subtask) =>
                subtask.id === id ? { ...subtask, completed: checked } : subtask,
            ),
        )
    }

    const handleMutate = ({ key, value }: { key: string; value: string }) => {
        if (key === 'priority') {
            setPriority(value as Priority)
        }

        if (key === 'dueDate') {
            setDueDate(value)
        }

        if (key === 'assignee') {
            setAssigneeIds((currentIds) =>
                currentIds.includes(value)
                    ? currentIds.filter((id) => id !== value)
                    : [...currentIds, value],
            )
        }

        if (key === 'tags') {
            setSelectedTagIds((currentTags) =>
                currentTags.includes(value)
                    ? currentTags.filter((id) => id !== value)
                    : [...currentTags, value],
            )
        }

        if (key === 'subject') {
            setSubject(value)
        }

        if (key === 'description') {
            setDescription(value)
        }
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={() => setIsOpen(true)}>
                Open dialog
            </Button>

            <Dialog
                aria-labelledby={dialogTitleId}
                className="p-0"
                closable={false}
                isOpen={isOpen}
                lockScroll
                width={650}
                onClose={() => setIsOpen(false)}
            >
                <div className="min-h-[400px] py-4">
                    <div className="space-y-4">
                        <div className="flex justify-between items-center gap-4 px-4">
                            <h4 className="w-full -ml-2" id={dialogTitleId}>
                                <SelectorWraper className="w-full max-w-full">
                                    <SubjectEditor
                                        className="flex-1"
                                        value={subject}
                                        onValueChange={(value) =>
                                            handleMutate({ key: 'subject', value })
                                        }
                                    />
                                </SelectorWraper>
                            </h4>
                            <div>
                                <Button
                                    aria-label="Close task details"
                                    icon={<PiX />}
                                    size="sm"
                                    variant="ghost"
                                    onClick={() => setIsOpen(false)}
                                />
                            </div>
                        </div>

                        <div className="px-4">
                            <Field label="Ticket" icon={<PiBookmarkSimple />}>
                                <SelectorWraper editable={false}>OPS-482</SelectorWraper>
                            </Field>
                            <Field label="Priority" icon={<PiArrowsDownUp />}>
                                <PrioritySelector
                                    value={priority}
                                    list={priorityOptions}
                                    onValueChange={(value) =>
                                        handleMutate({ key: 'priority', value })
                                    }
                                >
                                    <SelectorWraper>
                                        <Tag
                                            className="gap-1 inline-flex items-center bg-transparent py-0.5 px-1.5"
                                            prefix={
                                                <span
                                                    className={classNames(
                                                        'h-2.5 w-2.5 rounded-xs',
                                                        priorities.find(
                                                            (item) => item.value === priority,
                                                        )?.dotClass,
                                                    )}
                                                ></span>
                                            }
                                        >
                                            {priority}
                                        </Tag>
                                    </SelectorWraper>
                                </PrioritySelector>
                            </Field>
                            <Field label="Due date" icon={<PiClock />}>
                                <DuedateSelector
                                    value={dueDate}
                                    onValueChange={(value) =>
                                        handleMutate({ key: 'dueDate', value })
                                    }
                                >
                                    <SelectorWraper>
                                        {dayjs(dueDate).format('DD MMM YYYY')}
                                    </SelectorWraper>
                                </DuedateSelector>
                            </Field>
                            <Field label="Assigned to" icon={<PiUser />}>
                                <AssigneeSelector
                                    list={assigneeOptions}
                                    value={assigneeIds}
                                    onValueChange={(value) =>
                                        handleMutate({ key: 'assignee', value })
                                    }
                                >
                                    <SelectorWraper>
                                        {assigneeIds.length > 1 ? (
                                            <UsersAvatarGroup
                                                users={assigneeIds.map(getMember)}
                                                avatarProps={{ size: 25 }}
                                            />
                                        ) : (
                                            <>
                                                {assigneeIds.map((id) => {
                                                    const member = getMember(id)
                                                    return (
                                                        <div
                                                            key={member.id}
                                                            className="flex items-center gap-1"
                                                        >
                                                            <Avatar
                                                                size={22}
                                                                src={member.img}
                                                                alt={member.name}
                                                            />
                                                            <div>{member.name}</div>
                                                        </div>
                                                    )
                                                })}
                                            </>
                                        )}
                                    </SelectorWraper>
                                </AssigneeSelector>
                            </Field>
                            <Field label="Tags" icon={<PiTag />}>
                                <TagsSelector
                                    list={tagsList}
                                    value={selectedTagIds}
                                    onValueChange={(value) =>
                                        handleMutate({ key: 'tags', value })
                                    }
                                >
                                    <SelectorWraper wrap>
                                        {selectedTagIds.map((tagId) => (
                                            <Tag
                                                className="py-0.5 px-1.5"
                                                key={tagId}
                                            >
                                                {tagOptions.find(
                                                    (tag) => tag.id === tagId,
                                                )?.label ?? tagId}
                                            </Tag>
                                        ))}
                                    </SelectorWraper>
                                </TagsSelector>
                            </Field>
                        </div>

                        <div className="px-4">
                            {isEditingDescription ? (
                                <>
                                    <h6 className="mb-1">Description</h6>
                                    <div onClick={(event) => event.stopPropagation()}>
                                        <RichTextEditor
                                            content={editedDescription}
                                            onChange={({ html }) =>
                                                setEditedDescription(html)
                                            }
                                        />
                                        <div className="flex justify-end gap-2 mt-3">
                                            <Button
                                                size="sm"
                                                onClick={() => {
                                                    setIsEditingDescription(false)
                                                    setEditedDescription('')
                                                }}
                                            >
                                                Cancel
                                            </Button>
                                            <Button
                                                size="sm"
                                                variant="solid"
                                                onClick={() => {
                                                    handleMutate({
                                                        key: 'description',
                                                        value: editedDescription,
                                                    })
                                                    setIsEditingDescription(false)
                                                }}
                                            >
                                                Save
                                            </Button>
                                        </div>
                                    </div>
                                </>
                            ) : (
                                <Card
                                    className={classNames(
                                        'bg-accent',
                                        !isEditingDescription &&
                                            'cursor-pointer hover:ring-1 hover:ring-border transition-all',
                                    )}
                                    onClick={() => {
                                        if (!isEditingDescription) {
                                            setEditedDescription(description)
                                            setIsEditingDescription(true)
                                        }
                                    }}
                                >
                                    <h6 className="mb-1">Description</h6>
                                    <p className={classNames(!description && 'italic')}>
                                        {description || 'Click to add description...'}
                                    </p>
                                </Card>
                            )}
                        </div>

                        <div>
                            <Tabs defaultValue="comments">
                                <Tabs.TabList className="px-4">
                                    <Tabs.TabNav value="comments">Comments</Tabs.TabNav>
                                    <Tabs.TabNav value="subtasks">Subtasks</Tabs.TabNav>
                                    <Tabs.TabNav value="attachments">Attachments</Tabs.TabNav>
                                </Tabs.TabList>
                                <Scroll.FlexSize className="max-h-[600px]">
                                    <div className="py-4 px-6">
                                        <Tabs.TabContent value="comments">
                                            <div>
                                                {comments.map((comment, index) => (
                                                    <Fragment key={comment.id}>
                                                        <div className="flex gap-2 py-4">
                                                            <div>
                                                                <Avatar
                                                                    src={comment.author.img}
                                                                    size={30}
                                                                />
                                                            </div>
                                                            <div className="rounded-sm">
                                                                <div className="flex items-center mb-1">
                                                                    <span className="font-medium">
                                                                        {comment.author.name}
                                                                    </span>
                                                                    <span className="mx-1"> • </span>
                                                                    <span className="text-xs">
                                                                        {dayjs(
                                                                            comment.createdAt,
                                                                        ).format('hh:mm A')}
                                                                    </span>
                                                                </div>
                                                                <div className="mb-0 prose text-sm prose-p:text-sm prose-p:leading-normal [--tw-prose-body:var(--nui-muted-foreground)]">
                                                                    {comment.message}
                                                                </div>
                                                            </div>
                                                        </div>
                                                        {index !== comments.length - 1 && <Divider />}
                                                    </Fragment>
                                                ))}
                                                {comments.length === 0 && (
                                                    <div className="flex-1 flex flex-col items-center justify-center">
                                                        <EmptyState
                                                            variant="dots"
                                                            size={180}
                                                            offset={-20}
                                                            illustration={
                                                                <Avatar shape="round"
                                                                    className="bg-card ring-1 ring-border"
                                                                    icon={<PiChatCircleSlash className="text-xl" />}
                                                                />
                                                            }
                                                        >
                                                            <div className="text-center">
                                                                <h5>No comment yet</h5>
                                                                <p className="max-w-[400px]">
                                                                    Be the first one to comment
                                                                </p>
                                                            </div>
                                                        </EmptyState>
                                                    </div>
                                                )}
                                                <div className="mt-8">
                                                    <CommentInput
                                                        onSubmit={handleCommentSubmit}
                                                    />
                                                </div>
                                            </div>
                                        </Tabs.TabContent>

                                        <Tabs.TabContent value="subtasks">
                                            {subtasks.length === 0 && (
                                                <div className="flex-1 flex flex-col items-center justify-center">
                                                    <EmptyState
                                                        variant="dots"
                                                        size={180}
                                                        offset={-20}
                                                        illustration={
                                                            <Avatar shape="round"
                                                                className="bg-card ring-1 ring-border"
                                                                icon={<PiCheckSquare className="text-xl" />}
                                                            />
                                                        }
                                                    >
                                                        <div className="text-center">
                                                            <h5>No subtasks</h5>
                                                        </div>
                                                    </EmptyState>
                                                </div>
                                            )}
                                            <div>
                                                {subtasks.map((subtask, index) => (
                                                    <Fragment key={subtask.id}>
                                                        <div
                                                            className="py-2 px-2 flex items-center justify-between cursor-pointer"
                                                            tabIndex={0}
                                                            role="button"
                                                            onClick={() =>
                                                                handleSubTaskCheck(
                                                                    subtask.id,
                                                                    !subtask.completed,
                                                                )
                                                            }
                                                        >
                                                            <div className="flex items-center gap-2">
                                                                <Checkbox checked={subtask.completed} />
                                                                <div
                                                                    className={classNames(
                                                                        'font-medium leading-none',
                                                                        subtask.completed && 'line-through',
                                                                    )}
                                                                >
                                                                    {subtask.title}
                                                                </div>
                                                            </div>
                                                            <div className="flex items-center gap-4">
                                                                <div className="flex items-center">
                                                                    <UsersAvatarGroup
                                                                        avatarProps={{ size: 22 }}
                                                                        users={subtask.assigneeIds.map(getMember)}
                                                                    />
                                                                </div>
                                                                <div className="flex items-center gap-1 min-w-[60px] font-medium">
                                                                    <PiCalendarBlank className="text-lg" />
                                                                    <span className="text-xs">
                                                                        {dayjs(subtask.dueDate).format('MMM DD')}
                                                                    </span>
                                                                </div>
                                                            </div>
                                                        </div>
                                                        {index !== subtasks.length - 1 && <Divider />}
                                                    </Fragment>
                                                ))}
                                            </div>
                                        </Tabs.TabContent>

                                        <Tabs.TabContent value="attachments">
                                            {attachments.length === 0 ? (
                                                <div className="flex-1 flex flex-col items-center justify-center mb-4">
                                                    <EmptyState
                                                        variant="dots"
                                                        size={180}
                                                        offset={-20}
                                                        illustration={
                                                            <Avatar shape="round"
                                                                className="bg-card ring-1 ring-border"
                                                                icon={<PiFileText className="text-xl" />}
                                                            />
                                                        }
                                                    >
                                                        <div className="text-center">
                                                            <h5>No attachments</h5>
                                                        </div>
                                                    </EmptyState>
                                                </div>
                                            ) : (
                                                <Card bodyClass="p-0 divide-y divide-border">
                                                    {attachments.map((attachment) => (
                                                        <div
                                                            key={attachment.id}
                                                            className="flex justify-between items-center p-2"
                                                        >
                                                            <div className="flex items-center gap-2">
                                                                <FileIcon
                                                                    type={attachment.type}
                                                                    size={25}
                                                                />
                                                                <div className="font-medium">
                                                                    {attachment.name}
                                                                </div>
                                                            </div>
                                                            <div className="flex items-center gap-1">
                                                                <Button
                                                                    variant="ghost"
                                                                    size="sm"
                                                                    icon={<PiDownloadSimple />}
                                                                />
                                                            </div>
                                                        </div>
                                                    ))}
                                                </Card>
                                            )}
                                        </Tabs.TabContent>
                                    </div>
                                </Scroll.FlexSize>
                            </Tabs>
                        </div>

                        <div className="flex justify-end items-center gap-2 px-4">
                            <Button onClick={() => setIsOpen(false)}>Cancel</Button>
                            <Button variant="solid" loading={isSaving} onClick={handleSave}>
                                Save
                            </Button>
                        </div>
                    </div>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 13

Preview
npx nateui@latest add DialogAccountSettings
Dark
import { useState } from 'react'
import type { KeyboardEvent } from 'react'
import Avatar from '@/components/ui/Avatar'
import Button from '@/components/ui/Button'
import Card from '@/components/ui/Card'
import Dialog from '@/components/ui/Dialog'
import Input from '@/components/ui/Input'
import Menu from '@/components/ui/Menu'
import Select from '@/components/ui/Select'
import Switcher from '@/components/ui/Switcher'
import IconFrame from '@/components/composites/IconFrame'
import PasswordInput from '@/components/composites/PasswordInput'
import {
    PiCheck,
    PiCircleDashed,
    PiFingerprint,
    PiLightning,
    PiPencilSimple,
    PiStack,
    PiUserCircleCheck,
    PiWallet,
} from 'react-icons/pi'

const assetBase = 'https://statics.nateui.com/img'

const dialogTitleId = 'dialog-account-settings-title'

type SettingsSection =
    | 'general'
    | 'profile'
    | 'security'
    | 'notifications'
    | 'subscription'

type EditableField =
    | 'username'
    | 'profileUrl'
    | 'displayName'
    | 'password'

type SelectOption = {
    label: string
    value: string
}

const sectionItems = [
    { eventKey: 'general' as const, label: 'General', icon: PiCircleDashed },
    { eventKey: 'profile' as const, label: 'Profile', icon: PiUserCircleCheck },
    { eventKey: 'security' as const, label: 'Security', icon: PiFingerprint },
    {
        eventKey: 'notifications' as const,
        label: 'Notifications',
        icon: PiLightning,
    },
    {
        eventKey: 'subscription' as const,
        label: 'Subscription',
        icon: PiWallet,
    },
]

const languageOptions: SelectOption[] = [
    { label: 'English', value: 'english' },
    { label: 'French', value: 'french' },
    { label: 'Japanese', value: 'japanese' },
]

const notificationOptions: SelectOption[] = [
    { label: 'Push', value: 'push' },
    { label: 'Email', value: 'email' },
    { label: 'None', value: 'none' },
]

export default function DialogAccountSettings() {
    const [isOpen, setIsOpen] = useState(true)
    const [activeSection, setActiveSection] =
        useState<SettingsSection>('general')
    const [editingField, setEditingField] = useState<EditableField | null>(null)
    const [draftValue, setDraftValue] = useState('')

    const [username, setUsername] = useState('rae')
    const [profileUrl, setProfileUrl] = useState('northstar.design/rae')
    const [displayName, setDisplayName] = useState('Rae Delgado')
    const [password, setPassword] = useState('N8te!Settings')

    const [autoSave, setAutoSave] = useState(true)
    const [keyboardShortcuts, setKeyboardShortcuts] = useState(true)
    const [compactNavigation, setCompactNavigation] = useState(false)
    const [language, setLanguage] = useState(languageOptions[0])

    const [profileVisibility, setProfileVisibility] = useState(false)
    const [mfa, setMfa] = useState(false)
    const [notificationStyle, setNotificationStyle] = useState(
        notificationOptions[0],
    )
    const [exportFinished, setExportFinished] = useState(true)
    const [weeklySummary, setWeeklySummary] = useState(false)
    const [commentReplies, setCommentReplies] = useState(true)

    const beginEdit = (field: EditableField, value: string) => {
        setEditingField(field)
        setDraftValue(value)
    }

    const commitEdit = () => {
        if (!editingField) {
            return
        }

        switch (editingField) {
            case 'username':
                setUsername(draftValue)
                break
            case 'profileUrl':
                setProfileUrl(draftValue)
                break
            case 'displayName':
                setDisplayName(draftValue)
                break
            case 'password':
                setPassword(draftValue)
                break
        }

        setEditingField(null)
    }

    const revertEdit = () => {
        setEditingField(null)
    }

    const renderEditableRow = (
        field: EditableField,
        label: string,
        value: string,
        options: { password?: boolean } = {},
    ) => {
        const isEditing = editingField === field
        const restingValue = options.password ? '••••••••••••' : value

        return (
            <div className="flex min-h-9 items-center justify-between gap-4 px-4 py-4">
                <p className="min-w-0 flex-1 font-medium text-foreground">{label}</p>
                <div className="flex min-h-9 w-1/2 min-w-0 shrink-0 items-center justify-end">
                    {isEditing ? (
                        <div
                            className="flex min-w-0 flex-1 items-center justify-end gap-2"
                            onBlur={(event) => {
                                if (
                                    !event.currentTarget.contains(
                                        event.relatedTarget as Node | null,
                                    )
                                ) {
                                    revertEdit()
                                }
                            }}
                        >
                            {options.password ? (
                                <PasswordInput
                                    aria-label={`${label} value`}
                                    autoFocus
                                    className="min-w-0 flex-1"
                                    size="sm"
                                    value={draftValue}
                                    onChange={(event) =>
                                        setDraftValue(event.target.value)
                                    }
                                    onKeyDown={(event) => {
                                        if (event.key === 'Enter') {
                                            event.preventDefault()
                                            commitEdit()
                                        }
                                        if (event.key === 'Escape') {
                                            event.preventDefault()
                                            revertEdit()
                                        }
                                    }}
                                />
                            ) : (
                                <Input
                                    aria-label={`${label} value`}
                                    autoFocus
                                    className="min-w-0 flex-1"
                                    size="sm"
                                    value={draftValue}
                                    onChange={(event) =>
                                        setDraftValue(event.target.value)
                                    }
                                    onKeyDown={(event) => {
                                        if (event.key === 'Enter') {
                                            event.preventDefault()
                                            commitEdit()
                                        }
                                        if (event.key === 'Escape') {
                                            event.preventDefault()
                                            revertEdit()
                                        }
                                    }}
                                />
                            )}
                            <Button
                                aria-label={`Save ${label.toLowerCase()}`}
                                icon={<PiCheck aria-hidden="true" />}
                                shape="circle"
                                size="sm"
                                type="button"
                                variant="ghost"
                                onClick={commitEdit}
                            />
                        </div>
                    ) : (
                        <button
                            aria-label={`Edit ${label.toLowerCase()}`}
                            className="inline-flex min-w-0 max-w-full items-center gap-2 rounded-control px-2 py-1 text-start font-medium text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
                            type="button"
                            onClick={() => beginEdit(field, value)}
                        >
                            <span className="min-w-0 truncate">{restingValue}</span>
                            <PiPencilSimple
                                aria-hidden="true"
                                className="shrink-0 text-lg"
                            />
                        </button>
                    )}
                </div>
            </div>
        )
    }

    const renderSwitcherRow = (
        label: string,
        checked: boolean,
        onChange: (checked: boolean) => void,
        helper?: string,
    ) => (
        <div className="flex min-h-9 items-center justify-between gap-4 px-4 py-4">
            <div className="min-w-0">
                <p className="font-medium text-foreground">{label}</p>
                {helper && <p className="text-muted-foreground">{helper}</p>}
            </div>
            <Switcher
                aria-label={label}
                checked={checked}
                onChange={onChange}
            />
        </div>
    )

    const renderSelectRow = (
        label: string,
        value: SelectOption,
        options: SelectOption[],
        onChange: (option: SelectOption) => void,
    ) => (
        <div className="flex min-h-9 flex-col justify-center gap-2 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
            <p className="font-medium text-foreground">{label}</p>
            <Select
                aria-label={label}
                className="w-full sm:w-48"
                options={options}
                value={value}
                onChange={(option) => onChange(option as SelectOption)}
            />
        </div>
    )

    const renderGeneralPanel = () => (
        <div className="divide-y">
            {renderEditableRow('username', 'Username', username)}
            <div className="flex min-h-9 items-center justify-between gap-4 px-4 py-4">
                <p className="font-medium text-foreground">Email</p>
                <span className="min-w-0 truncate">
                    rae@northstar.test
                </span>
            </div>
            {renderSwitcherRow('Save drafts automatically', autoSave, setAutoSave)}
            {renderSwitcherRow(
                'Show keyboard shortcuts',
                keyboardShortcuts,
                setKeyboardShortcuts,
            )}
            {renderSwitcherRow(
                'Compact navigation',
                compactNavigation,
                setCompactNavigation,
            )}
            {renderSelectRow('Language', language, languageOptions, setLanguage)}
        </div>
    )

    const renderProfilePanel = () => (
        <div className="divide-y">
            {renderSwitcherRow(
                'Profile visibility',
                profileVisibility,
                setProfileVisibility,
            )}
            <div className="flex min-h-9 items-center justify-between gap-4 px-4 py-4">
                <div className="min-w-0">
                    <p className="font-medium text-foreground">Avatar</p>
                </div>
                <Avatar
                    alt="Profile avatar"
                    size={48}
                    src={`${assetBase}/avatars/thumb-3.jpg`}
                />
            </div>
            {renderEditableRow('profileUrl', 'Public profile URL', profileUrl)}
            {renderEditableRow('displayName', 'Display name', displayName)}
        </div>
    )

    const renderSecurityPanel = () => (
        <div className="divide-y">
            {renderEditableRow('password', 'Password', password, {
                password: true,
            })}
            {renderSwitcherRow(
                'Multi-factor authentication',
                mfa,
                setMfa,
            )}
            <div className="flex min-h-9 items-center justify-between gap-4 px-4 py-4">
                <div className="min-w-0">
                    <p className="font-medium text-foreground">
                        Sign out of all devices
                    </p>
                </div>
                <Button type="button">Sign out all</Button>
            </div>
        </div>
    )

    const renderNotificationsPanel = () => (
        <div className="divide-y">
            {renderSelectRow(
                'Notification style',
                notificationStyle,
                notificationOptions,
                setNotificationStyle,
            )}
            {renderSwitcherRow('Export finished', exportFinished, setExportFinished)}
            {renderSwitcherRow('Weekly activity summary', weeklySummary, setWeeklySummary)}
            {renderSwitcherRow(
                'Replies to your comments',
                commentReplies,
                setCommentReplies,
            )}
        </div>
    )

    const renderSubscriptionPanel = () => (
        <div className="px-4 py-4">
            <p className="text-foreground">
                Next billing date is September 3, 2026
            </p>
            <Card className="mt-4" bodyClass="p-2">
                <div className="bg-muted border rounded-card p-4">
                    <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
                        <div className="flex min-w-0 items-start gap-2">
                            <IconFrame size={40}>
                                <img
                                    alt=""
                                    className="h-6 w-6 object-contain"
                                    src={`${assetBase}/thumbs/plans/pro.svg`}
                                />
                            </IconFrame>
                            <div className="min-w-0">
                                <p className="font-medium text-foreground">
                                    Growth plan
                                </p>
                                <p className="text-muted-foreground">
                                    For teams that outgrew a single seat.
                                </p>
                            </div>
                        </div>
                        <div className="shrink-0 text-end">
                            <p className="font-semibold text-foreground">
                                $20.00
                            </p>
                            <p className="text-muted-foreground">
                                Billed monthly
                            </p>
                        </div>
                    </div>
                </div>
                <Button
                    block
                    className="mt-4"
                    type="button"
                    variant="solid"
                >
                    Upgrade
                </Button>
                <ul className="mt-4 flex flex-col gap-2 p-2" aria-label="Included features">
                    {[
                        'Unlimited projects',
                        'Shared workspaces',
                        'Priority exports',
                        'Version history',
                        'Email support',
                    ].map((feature) => (
                        <li key={feature} className="flex items-center gap-2">
                            <span className="size-6 rounded-lg bg-muted flex items-center justify-center">
                                <PiCheck aria-hidden="true" className="shrink-0" />
                            </span>
                            <span>{feature}</span>
                        </li>
                    ))}
                </ul>
            </Card>
        </div>
    )

    const panelContent = {
        general: renderGeneralPanel,
        profile: renderProfilePanel,
        security: renderSecurityPanel,
        notifications: renderNotificationsPanel,
        subscription: renderSubscriptionPanel,
    }[activeSection]()

    const activeLabel = sectionItems.find(
        (item) => item.eventKey === activeSection,
    )?.label

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={() => setIsOpen(true)}>
                Open dialog
            </Button>

            <Dialog
                aria-labelledby={dialogTitleId}
                className="p-0"
                isOpen={isOpen}
                lockScroll
                width={700}
                onClose={() => setIsOpen(false)}
            >
                <div className="flex h-[540px] flex-col md:flex-row">
                    <nav
                        aria-label="Account settings sections"
                        className="shrink-0 border-b p-4 md:w-52 md:border-b-0 md:border-r"
                    >
                        <Menu variant="subtle">
                            {sectionItems.map((item) => {
                                const isActive = activeSection === item.eventKey
                                const Icon = item.icon

                                return (
                                    <Menu.MenuItem
                                        key={item.eventKey}
                                        aria-current={
                                            isActive ? 'page' : undefined
                                        }
                                        className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
                                        eventKey={item.eventKey}
                                        isActive={isActive}
                                        role="button"
                                        tabIndex={0}
                                        onClick={() =>
                                            setActiveSection(item.eventKey)
                                        }
                                        onKeyDown={(event: KeyboardEvent<HTMLElement>) => {
                                            if (
                                                event.key === 'Enter' ||
                                                event.key === ' '
                                            ) {
                                                event.preventDefault()
                                                setActiveSection(item.eventKey)
                                            }
                                        }}
                                    >
                                        <Icon
                                            aria-hidden="true"
                                            className="text-lg"
                                        />
                                        <span>{item.label}</span>
                                    </Menu.MenuItem>
                                )
                            })}
                        </Menu>
                    </nav>

                    <section className="min-h-0 min-w-0 flex-1 overflow-y-auto">
                        <header className="border-b p-4 pr-12">
                            <h5
                                className="text-lg font-semibold text-foreground"
                                id={dialogTitleId}
                            >
                                {activeLabel}
                            </h5>
                        </header>
                        {panelContent}
                    </section>
                </div>
            </Dialog>
        </main>
    )
}

Dialog 14

Preview
npx nateui@latest add DialogAddIntegrations
Dark
import { useMemo, useState } from 'react'
import {
    PiArrowsClockwise,
    PiCheck,
    PiDatabase,
    PiMagnifyingGlass,
    PiPlus,
} from 'react-icons/pi'
import Avatar from '@/components/ui/Avatar'
import Button from '@/components/ui/Button'
import CloseButton from '@/components/ui/CloseButton'
import Dialog from '@/components/ui/Dialog'
import Input from '@/components/ui/Input'
import Scroll from '@/components/ui/Scroll'
import InputGroup from '@/components/ui/InputGroup'
import EmptyState from '@/components/composites/EmptyState'
import IconFrame from '@/components/composites/IconFrame'

const assetBase = 'https://statics.nateui.com/img'
const dialogTitleId = 'dialog-add-integrations-title'
const dialogDescriptionId = 'dialog-add-integrations-description'

type IntegrationType = 'automation' | 'storageCollaboration'
type FilterKey = 'all' | IntegrationType

type Integration = {
    id: string
    name: string
    desc: string
    logo: string
    type: IntegrationType
    active: boolean
}

const initialIntegrations: Integration[] = [
    {
        id: 'google-drive',
        name: 'Google Drive',
        desc: 'Upload your files to Google Drive',
        logo: `${assetBase}/thumbs/brands/google-drive.png`,
        type: 'storageCollaboration',
        active: true,
    },
    {
        id: 'github',
        name: 'GitHub',
        desc: 'Exchange files with a GitHub repository',
        logo: `${assetBase}/thumbs/brands/github.png`,
        type: 'storageCollaboration',
        active: true,
    },
    {
        id: 'zapier',
        name: 'Zapier',
        desc: 'Integrate with hundreds of services.',
        logo: `${assetBase}/thumbs/brands/zapier.png`,
        type: 'automation',
        active: false,
    },
    {
        id: 'make',
        name: 'Make (Integromat)',
        desc: 'Visually automate your workflows with Make',
        logo: `${assetBase}/thumbs/brands/make.png`,
        type: 'automation',
        active: false,
    },
    {
        id: 'pabbly',
        name: 'Pabbly Connect',
        desc: 'Affordable automation for SaaS and CRM tools',
        logo: `${assetBase}/thumbs/brands/pabbly.png`,
        type: 'automation',
        active: false,
    },
    {
        id: 'slack',
        name: 'Slack',
        desc: 'Post to a Slack channel',
        logo: `${assetBase}/thumbs/brands/slack.png`,
        type: 'storageCollaboration',
        active: false,
    },
    {
        id: 'notion',
        name: 'Notion',
        desc: 'Retrieve notion note to your project',
        logo: `${assetBase}/thumbs/brands/notion.png`,
        type: 'storageCollaboration',
        active: false,
    },
    {
        id: 'dropbox',
        name: 'Dropbox',
        desc: 'Exchange data with Dropbox',
        logo: `${assetBase}/thumbs/brands/dropbox.png`,
        type: 'storageCollaboration',
        active: false,
    },
    {
        id: 'hubspot',
        name: 'HubSpot',
        desc: 'Sync contacts and activity with HubSpot',
        logo: `${assetBase}/thumbs/brands/hubspot.png`,
        type: 'automation',
        active: false,
    },
    {
        id: 'salesforce',
        name: 'Salesforce',
        desc: 'Send records to Salesforce',
        logo: `${assetBase}/thumbs/brands/salesforce.png`,
        type: 'automation',
        active: false,
    },
    {
        id: 'asana',
        name: 'Asana',
        desc: 'Create tasks from new work items',
        logo: `${assetBase}/thumbs/brands/asana.png`,
        type: 'automation',
        active: false,
    },
    {
        id: 'jira',
        name: 'Jira',
        desc: 'Link issues from a Jira project',
        logo: `${assetBase}/thumbs/brands/jira.png`,
        type: 'automation',
        active: false,
    },
]

const filterOptions: Array<{
    key: FilterKey
    label: string
    icon?: typeof PiArrowsClockwise
}> = [
    { key: 'all', label: 'All' },
    { key: 'automation', label: 'Automation', icon: PiArrowsClockwise },
    {
        key: 'storageCollaboration',
        label: 'Storage',
        icon: PiDatabase,
    },
]

export default function DialogAddIntegrations() {
    const [isOpen, setIsOpen] = useState(true)
    const [activeFilter, setActiveFilter] = useState<FilterKey>('all')
    const [query, setQuery] = useState('')
    const [connectedIds, setConnectedIds] = useState<string[]>(() =>
        initialIntegrations
            .filter((integration) => integration.active)
            .map((integration) => integration.id),
    )

    const filteredIntegrations = useMemo(() => {
        const normalizedQuery = query.trim().toLowerCase()

        return initialIntegrations.filter((integration) => {
            const matchesFilter =
                activeFilter === 'all' || integration.type === activeFilter
            const matchesQuery =
                !normalizedQuery ||
                integration.name.toLowerCase().includes(normalizedQuery) ||
                integration.desc.toLowerCase().includes(normalizedQuery)

            return matchesFilter && matchesQuery
        })
    }, [activeFilter, query])

    const toggleConnection = (id: string) => {
        setConnectedIds((current) =>
            current.includes(id)
                ? current.filter((connectedId) => connectedId !== id)
                : [...current, id],
        )
    }

    return (
        <main className="flex min-h-screen items-center justify-center bg-background p-4">
            <Button type="button" onClick={() => setIsOpen(true)}>
                Open dialog
            </Button>

            <Dialog
                aria-describedby={dialogDescriptionId}
                aria-labelledby={dialogTitleId}
                className="p-0"
                closable={false}
                isOpen={isOpen}
                lockScroll
                onClose={() => setIsOpen(false)}
                width={680}
            >
                <header className="flex justify-between border-b px-4 py-4">
                    <div>
                        <h5
                            className="text-lg font-semibold text-foreground"
                            id={dialogTitleId}
                        >
                            Add an integration
                        </h5>
                        <p
                            className="text-muted-foreground"
                            id={dialogDescriptionId}
                        >
                            Connect the tools your workspace uses.
                        </p>
                    </div>
                    <div>
                        <CloseButton onClick={() => setIsOpen(false)} />
                    </div>
                </header>

                <section
                    aria-label="Integration filters"
                    className="flex flex-col gap-4 border-b px-4 py-4 sm:flex-row sm:items-center sm:justify-between"
                >
                    <InputGroup>
                        {filterOptions.map((option) => {
                            const Icon = option.icon
                            const isActive = activeFilter === option.key

                            return (
                                <Button
                                    key={option.key}
                                    icon={
                                        Icon ? (
                                            <Icon aria-hidden="true" />
                                        ) : undefined
                                    }
                                    iconAlignment="start"
                                    active={isActive}
                                    onClick={() => setActiveFilter(option.key)}
                                >
                                    {option.label}
                                </Button>
                            )
                        })}
                    </InputGroup>

                    <div className="w-full sm:max-w-55">
                        <Input
                            aria-label="Search integrations"
                            placeholder="Search integrations"
                            prefix={<PiMagnifyingGlass aria-hidden="true" />}
                            value={query}
                            onChange={(event) => setQuery(event.target.value)}
                        />
                    </div>
                </section>

                <section aria-label="Available integrations" className="">
                    <Scroll.FlexSize className="max-h-[420px]">
                        <div className="divide-y">
                            {filteredIntegrations.length > 0 ? (
                                filteredIntegrations.map((integration) => {
                                    const isConnected = connectedIds.includes(
                                        integration.id,
                                    )

                                    return (
                                        <div
                                            key={integration.id}
                                            className="flex flex-col gap-4 p-4 sm:flex-row sm:items-center"
                                        >
                                            <Avatar
                                                alt={`${integration.name} logo`}
                                                className="shrink-0 border bg-white p-1"
                                                shape="round"
                                                size={40}
                                                src={integration.logo}
                                            />
                                            <div className="min-w-0 flex-1">
                                                <p className="font-semibold text-card-foreground">
                                                    {integration.name}
                                                </p>
                                                <p className="text-muted-foreground">
                                                    {integration.desc}
                                                </p>
                                            </div>
                                            <Button
                                                className="self-start sm:self-auto"
                                                icon={
                                                    isConnected ? (
                                                        <PiCheck aria-hidden="true" />
                                                    ) : (
                                                        <PiPlus aria-hidden="true" />
                                                    )
                                                }
                                                iconAlignment="start"
                                                size="sm"
                                                variant={
                                                    isConnected ? 'subtle' : 'default'
                                                }
                                                onClick={() =>
                                                    toggleConnection(integration.id)
                                                }
                                            >
                                                {isConnected ? 'Connected' : 'Connect'}
                                            </Button>
                                        </div>
                                    )
                                })
                            ) : (
                                <EmptyState
                                    variant="dots"
                                    illustration={
                                        <IconFrame>
                                            <PiMagnifyingGlass
                                                aria-hidden="true"
                                                className="text-xl"
                                            />
                                        </IconFrame>
                                    }
                                    size={160}
                                >
                                    <div className="text-center mb-4">
                                        <h6 className="text-base font-medium text-foreground">
                                            No integrations found
                                        </h6>
                                        <p className="mt-1 text-center text-muted-foreground">
                                            Try a different filter or search term.
                                        </p>
                                    </div>
                                </EmptyState>
                            )}
                        </div>
                    </Scroll.FlexSize>
                </section>

                <footer className="flex flex-col-reverse gap-2 border-t px-4 py-4 sm:flex-row sm:items-center sm:justify-end">
                    <Button
                        className="w-full sm:flex-1"
                        type="button"
                        onClick={() => setIsOpen(false)}
                    >
                        Cancel
                    </Button>
                    <Button
                        className="w-full sm:flex-1"
                        type="button"
                        variant="solid"
                        onClick={() => setIsOpen(false)}
                    >
                        Save Changes
                    </Button>
                </footer>
            </Dialog>
        </main>
    )
}