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