Migrate secondary modals to shared frame

This commit is contained in:
Richard R 2026-06-01 10:55:18 -06:00
parent bd3b99b3ee
commit 5e0303d065
5 changed files with 228 additions and 406 deletions

View file

@ -1,15 +1,8 @@
'use client'; 'use client';
import { Fragment, useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { updateAppConfig } from '@/lib/client/dexie'; import { updateAppConfig } from '@/lib/client/dexie';
import { Button, dialogPanelStyles } from '@/components/ui'; import { Button, ModalFrame, ModalTitle } from '@/components/ui';
interface PrivacyModalProps { interface PrivacyModalProps {
isOpen: boolean; isOpen: boolean;
@ -77,38 +70,8 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
}; };
return ( return (
<Transition appear show={isOpen} as={Fragment}> <ModalFrame open={isOpen} onClose={onDismiss ?? (() => {})} panelTestId="privacy-modal" className="z-[80]">
<Dialog as="div" className="relative z-[80]" onClose={onDismiss ?? (() => { })}> <ModalTitle>Privacy & Data Usage</ModalTitle>
<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="privacy-modal" className={dialogPanelStyles({ size: 'md' })}>
<DialogTitle
as="h3"
className="text-lg font-semibold leading-6 text-foreground"
>
Privacy & Data Usage
</DialogTitle>
<PrivacyModalBody origin={origin} /> <PrivacyModalBody origin={origin} />
@ -146,12 +109,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
</Button> </Button>
</div> </div>
</div> </div>
</DialogPanel> </ModalFrame>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
); );
} }
@ -179,46 +137,15 @@ export function showPrivacyModal(): void {
}; };
return ( return (
<Transition <ModalFrame
appear open={show}
show={show} onClose={handleClose}
as={Fragment}
afterLeave={() => { afterLeave={() => {
root.unmount(); root.unmount();
container.remove(); container.remove();
}} }}
> >
<Dialog as="div" className="relative z-50" onClose={handleClose}> <ModalTitle>Privacy & Data Usage</ModalTitle>
<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 leading-6 text-foreground"
>
Privacy & Data Usage
</DialogTitle>
<PrivacyModalBody origin={origin} /> <PrivacyModalBody origin={origin} />
@ -231,12 +158,7 @@ export function showPrivacyModal(): void {
Close Close
</Button> </Button>
</div> </div>
</DialogPanel> </ModalFrame>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
); );
}; };

View file

@ -1,16 +1,9 @@
'use client'; 'use client';
import { Fragment, useState } from 'react'; import { useState } from 'react';
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Button, dialogPanelStyles } from '@/components/ui'; import { Button, ModalFrame, ModalTitle } from '@/components/ui';
export type ClaimableCounts = { export type ClaimableCounts = {
documents: number; documents: number;
@ -74,38 +67,8 @@ export default function ClaimDataModal({
}; };
return ( return (
<Transition appear show={isOpen} as={Fragment}> <ModalFrame open={isOpen} onClose={onDismiss} panelTestId="claim-modal" className="z-[80]">
<Dialog as="div" className="relative z-[80]" onClose={onDismiss}> <ModalTitle className="mb-4">Existing Data Found</ModalTitle>
<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="claim-modal" className={dialogPanelStyles({ size: 'md' })}>
<DialogTitle
as="h3"
className="text-lg font-semibold leading-6 text-foreground mb-4"
>
Existing Data Found
</DialogTitle>
<p className="text-sm text-soft mb-2"> <p className="text-sm text-soft mb-2">
We found existing anonymous data from before auth was enabled. We found existing anonymous data from before auth was enabled.
@ -146,11 +109,6 @@ export default function ClaimDataModal({
{isClaiming ? 'Claiming...' : 'Claim Data'} {isClaiming ? 'Claiming...' : 'Claim Data'}
</Button> </Button>
</div> </div>
</DialogPanel> </ModalFrame>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
); );
} }

View file

@ -1,13 +1,12 @@
'use client'; 'use client';
import { Fragment, useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import { getAllEpubDocuments, getAllHtmlDocuments, getAllPdfDocuments, updateAppConfig } from '@/lib/client/dexie'; import { getAllEpubDocuments, getAllHtmlDocuments, getAllPdfDocuments, updateAppConfig } from '@/lib/client/dexie';
import { listDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents'; import { listDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents';
import { useDocuments } from '@/contexts/DocumentContext'; import { useDocuments } from '@/contexts/DocumentContext';
import type { BaseDocument } from '@/types/documents'; import type { BaseDocument } from '@/types/documents';
import { cacheStoredDocumentFromBytes } from '@/lib/client/cache/documents'; import { cacheStoredDocumentFromBytes } from '@/lib/client/cache/documents';
import { Button, dialogPanelStyles } from '@/components/ui'; import { Button, ModalFrame, ModalTitle } from '@/components/ui';
type DexieMigrationModalProps = { type DexieMigrationModalProps = {
isOpen: boolean; isOpen: boolean;
@ -137,35 +136,15 @@ export function DexieMigrationModal({
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<Transition appear show={isOpen} as={Fragment}> <ModalFrame
<Dialog as="div" className="relative z-[80]" onClose={() => (closeDisabled ? null : onComplete())}> open={isOpen}
<TransitionChild onClose={() => {
as={Fragment} if (!closeDisabled) onComplete();
enter="ease-standard duration-slow" }}
enterFrom="opacity-0" panelTestId="migration-modal"
enterTo="opacity-100" className="z-[80]"
leave="ease-standard duration-base"
leaveFrom="opacity-100"
leaveTo="opacity-0"
> >
<div className="fixed inset-0 overlay-dim backdrop-blur-sm" /> <ModalTitle className="mb-4">{title}</ModalTitle>
</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="migration-modal" className={dialogPanelStyles({ size: 'md' })}>
<DialogTitle as="h3" className="text-lg font-semibold leading-6 text-foreground mb-4">
{title}
</DialogTitle>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm text-soft mb-2"> <p className="text-sm text-soft mb-2">
Found {localCount} document{localCount === 1 ? '' : 's'} stored locally from an older version. Found {localCount} document{localCount === 1 ? '' : 's'} stored locally from an older version.
@ -204,11 +183,6 @@ export function DexieMigrationModal({
{isUploading ? 'Uploading…' : 'Upload'} {isUploading ? 'Uploading…' : 'Upload'}
</Button> </Button>
</div> </div>
</DialogPanel> </ModalFrame>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
); );
} }

View file

@ -1,9 +1,8 @@
'use client'; 'use client';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'; import { useEffect, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';
import { BaseDocument } from '@/types/documents'; import { BaseDocument } from '@/types/documents';
import { Button, dialogPanelStyles } from '@/components/ui'; import { Button, ModalFrame, ModalTitle } from '@/components/ui';
interface DocumentSelectionModalProps { interface DocumentSelectionModalProps {
isOpen: boolean; isOpen: boolean;
@ -114,36 +113,8 @@ export function DocumentSelectionModal({
}; };
return ( return (
<Transition appear show={isOpen} as={Fragment}> <ModalFrame open={isOpen} onClose={onClose} size="lg" panelClassName="flex h-[80vh] flex-col" className="z-[60]">
<Dialog as="div" className="relative z-[60]" onClose={onClose}> <ModalTitle className="mb-4 flex flex-shrink-0 items-center justify-between">
<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: 'lg', className: 'flex h-[80vh] flex-col' })}>
<DialogTitle
as="h3"
className="text-lg font-semibold leading-6 text-foreground mb-4 flex-shrink-0 flex justify-between items-center"
>
{title} {title}
{files.length > 0 && ( {files.length > 0 && (
<div className="flex items-center text-sm font-normal"> <div className="flex items-center text-sm font-normal">
@ -161,7 +132,7 @@ export function DocumentSelectionModal({
</label> </label>
</div> </div>
)} )}
</DialogTitle> </ModalTitle>
<div className="flex-1 overflow-auto border border-line rounded-lg bg-background p-2 min-h-0"> <div className="flex-1 overflow-auto border border-line rounded-lg bg-background p-2 min-h-0">
{isLoading ? ( {isLoading ? (
@ -178,9 +149,9 @@ export function DocumentSelectionModal({
<div <div
key={file.id} key={file.id}
onClick={(e) => handleRowClick(e, file.id)} onClick={(e) => handleRowClick(e, file.id)}
className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer text-sm select-none className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer text-sm select-none ${
${isSelected ? 'bg-accent-wash' : 'hover:bg-accent-wash'} isSelected ? 'bg-accent-wash' : 'hover:bg-accent-wash'
`} }`}
> >
<div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}> <div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}>
<input <input
@ -218,12 +189,7 @@ export function DocumentSelectionModal({
{isProcessing ? 'Processing...' : `${confirmLabel} ${selectedCount > 0 ? `(${selectedCount})` : ''}`} {isProcessing ? 'Processing...' : `${confirmLabel} ${selectedCount > 0 ? `(${selectedCount})` : ''}`}
</Button> </Button>
</div> </div>
</DialogPanel> </ModalFrame>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
); );
} }

View file

@ -43,6 +43,7 @@ export function ModalFrame({
panelClassName, panelClassName,
panelTestId, panelTestId,
onKeyDown, onKeyDown,
afterLeave,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@ -52,9 +53,10 @@ export function ModalFrame({
panelClassName?: string; panelClassName?: string;
panelTestId?: string; panelTestId?: string;
onKeyDown?: KeyboardEventHandler<HTMLDivElement>; onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
afterLeave?: () => void;
}) { }) {
return ( return (
<Transition appear show={open} as={Fragment}> <Transition appear show={open} as={Fragment} afterLeave={afterLeave}>
<Dialog as="div" role={undefined} className={cn('relative z-50', className)} onClose={onClose} onKeyDown={onKeyDown}> <Dialog as="div" role={undefined} className={cn('relative z-50', className)} onClose={onClose} onKeyDown={onKeyDown}>
<TransitionChild <TransitionChild
as={Fragment} as={Fragment}