Converge modal and drawer frames
This commit is contained in:
parent
9b424f0c9f
commit
bd3b99b3ee
7 changed files with 215 additions and 172 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { Fragment, KeyboardEvent } from 'react';
|
||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
||||
import { Button, dialogPanelStyles } from '@/components/ui';
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { Button, ModalFrame, ModalTitle } from '@/components/ui';
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -31,66 +30,25 @@ export function ConfirmDialog({
|
|||
};
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
role={undefined}
|
||||
className="relative z-50"
|
||||
onClose={onClose}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-standard duration-slow"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-standard duration-base"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
<ModalFrame open={isOpen} onClose={onClose} onKeyDown={handleKeyDown}>
|
||||
<ModalTitle>{title}</ModalTitle>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-soft break-words">{message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<Button variant="outline" size="sm" onClick={onClose}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button
|
||||
variant={isDangerous ? 'danger' : 'primary'}
|
||||
size="sm"
|
||||
className="text-wrap"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<div className="fixed inset-0 overlay-dim backdrop-blur-sm" />
|
||||
</TransitionChild>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-start justify-center p-4 pt-6 text-center sm:items-center sm:pt-4">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-standard duration-slow"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-standard duration-base"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel role='dialog' className={dialogPanelStyles({ size: 'md' })}>
|
||||
<DialogTitle
|
||||
as="h3"
|
||||
className="text-lg font-semibold leading-6 text-foreground"
|
||||
>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-soft break-words">{message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<Button variant="outline" size="sm" onClick={onClose}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button
|
||||
variant={isDangerous ? 'danger' : 'primary'}
|
||||
size="sm"
|
||||
className="text-wrap"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFrame>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import {
|
|||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
Button,
|
||||
Input,
|
||||
} from '@headlessui/react';
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
|
|
@ -55,8 +53,10 @@ import {
|
|||
SidebarNav,
|
||||
SidebarNavItem,
|
||||
SegmentedControl,
|
||||
Button,
|
||||
IconButton,
|
||||
Input,
|
||||
inputClass,
|
||||
buttonClass,
|
||||
listboxButtonClass,
|
||||
listboxOptionClass,
|
||||
listboxOptionsClass,
|
||||
|
|
@ -142,8 +142,10 @@ export function SettingsTrigger({
|
|||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onOpen}
|
||||
className={`inline-flex items-center py-1 px-2 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash hover:text-accent transition-transform transition-colors duration-base ease-standard ${className}`}
|
||||
className={className}
|
||||
aria-label="Settings"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
|
@ -549,6 +551,8 @@ export function SettingsModal({
|
|||
Settings
|
||||
</DialogTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsChangelogOpen(true)}
|
||||
className="text-sm font-medium leading-6 text-soft hover:text-accent transition-colors"
|
||||
>
|
||||
|
|
@ -557,6 +561,8 @@ export function SettingsModal({
|
|||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => showPrivacyModal()}
|
||||
className="text-sm font-medium text-soft hover:text-accent transition-colors"
|
||||
>
|
||||
|
|
@ -842,7 +848,8 @@ export function SettingsModal({
|
|||
<div className="pt-4 flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
className={buttonClass({ variant: 'secondary', size: 'md' })}
|
||||
variant="secondary"
|
||||
size="md"
|
||||
onClick={async () => {
|
||||
const defaults = resolveProviderDefaults({
|
||||
providerRef: runtimeConfig.defaultTtsProvider,
|
||||
|
|
@ -862,7 +869,8 @@ export function SettingsModal({
|
|||
<Button
|
||||
data-testid="settings-save-button"
|
||||
type="button"
|
||||
className={buttonClass({ variant: 'primary', size: 'md' })}
|
||||
variant="primary"
|
||||
size="md"
|
||||
disabled={!canSubmit}
|
||||
onClick={async () => {
|
||||
const defaults = resolveProviderDefaults({
|
||||
|
|
@ -1128,7 +1136,8 @@ export function SettingsModal({
|
|||
<Button
|
||||
onClick={handleImportLibrary}
|
||||
disabled={isBusy}
|
||||
className={buttonClass({ variant: 'outline', size: 'md' })}
|
||||
variant="outline"
|
||||
size="md"
|
||||
>
|
||||
{isImportingLibrary ? `Importing... ${Math.round(progress)}%` : 'Import from library'}
|
||||
</Button>
|
||||
|
|
@ -1140,14 +1149,16 @@ export function SettingsModal({
|
|||
<Button
|
||||
onClick={handleRefresh}
|
||||
disabled={isBusy}
|
||||
className={buttonClass({ variant: 'outline', size: 'md' })}
|
||||
variant="outline"
|
||||
size="md"
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleClearCache}
|
||||
disabled={isBusy}
|
||||
className={buttonClass({ variant: 'outline', size: 'md' })}
|
||||
variant="outline"
|
||||
size="md"
|
||||
>
|
||||
Clear cache
|
||||
</Button>
|
||||
|
|
@ -1226,7 +1237,8 @@ export function SettingsModal({
|
|||
<>
|
||||
<Button
|
||||
onClick={handleSignOut}
|
||||
className={buttonClass({ variant: 'outline', size: 'md' })}
|
||||
variant="outline"
|
||||
size="md"
|
||||
>
|
||||
Disconnect account
|
||||
</Button>
|
||||
|
|
@ -1236,7 +1248,8 @@ export function SettingsModal({
|
|||
<label className="block text-sm font-medium text-danger mb-2">Danger Zone</label>
|
||||
<Button
|
||||
onClick={() => setShowDeleteAccountConfirm(true)}
|
||||
className={buttonClass({ variant: 'danger', size: 'md' })}
|
||||
variant="danger"
|
||||
size="md"
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
|
|
@ -1259,19 +1272,19 @@ export function SettingsModal({
|
|||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link href="/signin">
|
||||
<Button className={buttonClass({ variant: 'outline', size: 'md' })}>
|
||||
<Button variant="outline" size="md">
|
||||
Connect
|
||||
</Button>
|
||||
</Link>
|
||||
{runtimeConfig.enableUserSignups && (
|
||||
<Link href="/signup">
|
||||
<Button className={buttonClass({ variant: 'primary', size: 'md' })}>
|
||||
<Button variant="primary" size="md">
|
||||
Create account
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link href="/?redirect=false">
|
||||
<Button className={buttonClass({ variant: 'outline', size: 'md' })}>
|
||||
<Button variant="outline" size="md">
|
||||
Back to landing page
|
||||
</Button>
|
||||
</Link>
|
||||
|
|
@ -1418,16 +1431,15 @@ function SettingsChangelogPanel({
|
|||
return (
|
||||
<div className="h-[490px] flex flex-col bg-surface">
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b border-line-soft bg-background">
|
||||
<Button
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
className="inline-flex items-center justify-center rounded-md text-soft hover:text-accent hover:bg-surface transition duration-base ease-standard transform"
|
||||
aria-label="Back to settings"
|
||||
title="Back"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
</Button>
|
||||
</IconButton>
|
||||
<div className="min-w-0">
|
||||
<h4 className="text-sm font-semibold text-foreground">Changelog</h4>
|
||||
<p className="text-xs text-soft truncate">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react';
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import toast from 'react-hot-toast';
|
||||
import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons';
|
||||
|
|
@ -13,7 +13,7 @@ import {
|
|||
listboxButtonClass,
|
||||
listboxOptionClass,
|
||||
listboxOptionsClass,
|
||||
buttonClass,
|
||||
Button,
|
||||
} from '@/components/ui';
|
||||
import { type TtsProviderId } from '@/lib/shared/tts-provider-catalog';
|
||||
import { useSharedProviders, type SharedProviderEntry } from '@/hooks/useSharedProviders';
|
||||
|
|
@ -581,14 +581,16 @@ export function AdminFeaturesPanel() {
|
|||
<Button
|
||||
onClick={discardAll}
|
||||
disabled={dirty.size === 0 || saving}
|
||||
className={buttonClass({ variant: 'secondary', size: 'sm' })}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
onClick={saveAll}
|
||||
disabled={dirty.size === 0 || saving}
|
||||
className={buttonClass({ variant: 'primary', size: 'sm' })}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
{saving ? 'Saving…' : dirty.size > 0 ? `Save (${dirty.size})` : 'Save'}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Input, Listbox, ListboxButton, ListboxOption, ListboxOptions, Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
||||
import { Fragment } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import toast from 'react-hot-toast';
|
||||
|
|
@ -17,7 +17,8 @@ import {
|
|||
listboxButtonClass,
|
||||
listboxOptionClass,
|
||||
listboxOptionsClass,
|
||||
buttonClass,
|
||||
Button,
|
||||
Input,
|
||||
} from '@/components/ui';
|
||||
|
||||
type ProviderType = TtsProviderId;
|
||||
|
|
@ -364,7 +365,9 @@ export function AdminProvidersPanel() {
|
|||
{!editingId ? (
|
||||
<Button
|
||||
onClick={startCreate}
|
||||
className={buttonClass({ variant: 'primary', size: 'icon', className: 'h-7 w-7' })}
|
||||
variant="primary"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
aria-label="Add provider"
|
||||
title="Add provider"
|
||||
>
|
||||
|
|
@ -589,13 +592,14 @@ export function AdminProvidersPanel() {
|
|||
/>
|
||||
|
||||
<div className="pt-1 flex justify-end gap-2">
|
||||
<Button onClick={cancelEdit} className={buttonClass({ variant: 'secondary', size: 'sm' })}>
|
||||
<Button onClick={cancelEdit} variant="secondary" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={submit}
|
||||
disabled={submitDisabled}
|
||||
className={buttonClass({ variant: 'primary', size: 'sm' })}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
{saveMutation.isPending ? 'Saving…' : isEditingExisting ? 'Save changes' : 'Create'}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Fragment, KeyboardEvent } from 'react';
|
||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
||||
import { dialogPanelStyles, Input } from '@/components/ui';
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { Input, ModalFrame, ModalTitle } from '@/components/ui';
|
||||
|
||||
interface CreateFolderDialogProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -18,52 +17,20 @@ export function CreateFolderDialog({
|
|||
onClose,
|
||||
}: CreateFolderDialogProps) {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-standard duration-slow"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-standard duration-base"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 overlay-dim backdrop-blur-sm" />
|
||||
</TransitionChild>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-start justify-center p-4 pt-6 text-center sm:items-center sm:pt-4">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-standard duration-slow"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-standard duration-base"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel className={dialogPanelStyles({ size: 'md' })}>
|
||||
<DialogTitle as="h3" className="text-lg font-semibold text-foreground">
|
||||
Create New Folder
|
||||
</DialogTitle>
|
||||
<div className="mt-4">
|
||||
<Input
|
||||
type="text"
|
||||
value={folderName}
|
||||
onChange={(e) => onFolderNameChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Enter folder name"
|
||||
controlSize="lg"
|
||||
autoFocus
|
||||
/>
|
||||
<p className="mt-2 text-xs text-soft">Press Enter to create or Escape to cancel</p>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
<ModalFrame open={isOpen} onClose={onClose}>
|
||||
<ModalTitle>Create New Folder</ModalTitle>
|
||||
<div className="mt-4">
|
||||
<Input
|
||||
type="text"
|
||||
value={folderName}
|
||||
onChange={(e) => onFolderNameChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Enter folder name"
|
||||
controlSize="lg"
|
||||
autoFocus
|
||||
/>
|
||||
<p className="mt-2 text-xs text-soft">Press Enter to create or Escape to cancel</p>
|
||||
</div>
|
||||
</ModalFrame>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
'use client';
|
||||
|
||||
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react';
|
||||
import { Fragment, useEffect, useState, type ReactNode } from 'react';
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { cn } from '@/components/ui/cn';
|
||||
import { DrawerFrame } from '@/components/ui';
|
||||
|
||||
const NARROW_QUERY = '(max-width: 767px)';
|
||||
|
||||
|
|
@ -65,35 +65,10 @@ function LibrarySidebarDrawer({
|
|||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Transition show={open} as={Fragment}>
|
||||
<Dialog onClose={onClose ?? (() => undefined)} className="relative z-40 md:hidden">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="transition-opacity duration-fast"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-fast"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
</TransitionChild>
|
||||
<div className="fixed inset-y-0 left-0 flex max-w-full">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="transition-transform duration-base ease-standard"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-fast ease-standard"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<DialogPanel className="h-full w-[80vw] max-w-[280px] bg-surface shadow-elev-3">
|
||||
{children}
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
<div className="md:hidden">
|
||||
<DrawerFrame open={open} onClose={onClose ?? (() => undefined)} side="left" className="w-[80vw] max-w-[280px]">
|
||||
{children}
|
||||
</DrawerFrame>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import type { ReactNode } from 'react';
|
||||
'use client';
|
||||
|
||||
import { Fragment, type KeyboardEventHandler, type ReactNode } from 'react';
|
||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
||||
import { variants } from './variants';
|
||||
import { cn } from './cn';
|
||||
|
||||
export type DialogSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
|
|
@ -29,3 +33,124 @@ export function DialogShell({
|
|||
}) {
|
||||
return <div className={dialogPanelStyles({ size, className })}>{children}</div>;
|
||||
}
|
||||
|
||||
export function ModalFrame({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
size = 'md',
|
||||
className,
|
||||
panelClassName,
|
||||
panelTestId,
|
||||
onKeyDown,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
size?: DialogSize;
|
||||
className?: string;
|
||||
panelClassName?: string;
|
||||
panelTestId?: string;
|
||||
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
return (
|
||||
<Transition appear show={open} as={Fragment}>
|
||||
<Dialog as="div" role={undefined} className={cn('relative z-50', className)} onClose={onClose} onKeyDown={onKeyDown}>
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-standard duration-slow"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-standard duration-base"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 overlay-dim backdrop-blur-sm" />
|
||||
</TransitionChild>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-start justify-center p-4 pt-6 text-center sm:items-center sm:pt-4">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-standard duration-slow"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-standard duration-base"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel data-testid={panelTestId} className={dialogPanelStyles({ size, className: panelClassName })}>
|
||||
{children}
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalTitle({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<DialogTitle as="h3" className={cn('text-lg font-semibold leading-6 text-foreground', className)}>
|
||||
{children}
|
||||
</DialogTitle>
|
||||
);
|
||||
}
|
||||
|
||||
export function DrawerFrame({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
side = 'left',
|
||||
className,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
side?: 'left' | 'right';
|
||||
className?: string;
|
||||
}) {
|
||||
const sideClass = side === 'left' ? 'left-0' : 'right-0';
|
||||
const enterFrom = side === 'left' ? '-translate-x-full' : 'translate-x-full';
|
||||
const leaveTo = enterFrom;
|
||||
|
||||
return (
|
||||
<Transition show={open} as={Fragment}>
|
||||
<Dialog onClose={onClose} className="relative z-40">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="transition-opacity duration-fast"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-fast"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
</TransitionChild>
|
||||
<div className={cn('fixed inset-y-0 flex max-w-full', sideClass)}>
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="transition-transform duration-base ease-standard"
|
||||
enterFrom={enterFrom}
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-fast ease-standard"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo={leaveTo}
|
||||
>
|
||||
<DialogPanel className={cn('h-full bg-surface shadow-elev-3', className)}>
|
||||
{children}
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue