Merge pull request #300 from documenso/feat/update-document-flow

feat: update document flow
This commit is contained in:
Lucas Smith
2023-08-25 12:11:43 +10:00
committed by GitHub
15 changed files with 460 additions and 319 deletions

View File

@ -5,7 +5,7 @@ import React, { useState } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Info, Loader } from 'lucide-react'; import { Info } from 'lucide-react';
import { usePlausible } from 'next-plausible'; import { usePlausible } from 'next-plausible';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
@ -85,7 +85,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
}; };
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent> <DialogContent>
@ -97,50 +97,49 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form <form onSubmit={handleSubmit(onFormSubmit)}>
className={cn('flex flex-col gap-y-4', className)} <fieldset disabled={isSubmitting} className={cn('flex flex-col gap-y-4', className)}>
onSubmit={handleSubmit(onFormSubmit)} {params?.get('cancelled') === 'true' && (
> <div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
{params?.get('cancelled') === 'true' && ( <div className="flex">
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4"> <div className="flex-shrink-0">
<div className="flex"> <Info className="h-5 w-5 text-yellow-400" />
<div className="flex-shrink-0"> </div>
<Info className="h-5 w-5 text-yellow-400" /> <div className="ml-3">
</div> <p className="text-sm leading-5 text-yellow-700">
<div className="ml-3"> You have cancelled the payment process. If you didn't mean to do this, please
<p className="text-sm leading-5 text-yellow-700"> try again.
You have cancelled the payment process. If you didn't mean to do this, please </p>
try again. </div>
</p>
</div> </div>
</div> </div>
)}
<div>
<Label className="text-slate-500">Name</Label>
<Input type="text" className="mt-2" {...register('name')} autoFocus />
<FormErrorMessage className="mt-1" error={errors.name} />
</div> </div>
)}
<div> <div>
<Label className="text-slate-500">Name</Label> <Label className="text-slate-500">Email</Label>
<Input type="text" className="mt-2" {...register('name')} autoFocus /> <Input type="email" className="mt-2" {...register('email')} />
<FormErrorMessage className="mt-1" error={errors.name} /> <FormErrorMessage className="mt-1" error={errors.email} />
</div> </div>
<div> <Button type="submit" size="lg" loading={isSubmitting}>
<Label className="text-slate-500">Email</Label> Claim the Community Plan (
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
<Input type="email" className="mt-2" {...register('email')} /> {planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
? 'Monthly'
<FormErrorMessage className="mt-1" error={errors.email} /> : 'Yearly'}
</div> )
</Button>
<Button type="submit" size="lg" disabled={isSubmitting}> </fieldset>
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
Claim the Community Plan ({/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
? 'Monthly'
: 'Yearly'}
)
</Button>
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -13,6 +13,11 @@ import { AddSignersFormPartial } from '@documenso/ui/primitives/document-flow/ad
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types'; import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/add-subject'; import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/add-subject';
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types'; import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
import {
DocumentFlowFormContainer,
DocumentFlowFormContainerHeader,
} from '@documenso/ui/primitives/document-flow/document-flow-root';
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -28,6 +33,8 @@ export type EditDocumentFormProps = {
fields: Field[]; fields: Field[];
}; };
type EditDocumentStep = 'signers' | 'fields' | 'subject';
export const EditDocumentForm = ({ export const EditDocumentForm = ({
className, className,
document, document,
@ -38,29 +45,34 @@ export const EditDocumentForm = ({
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const router = useRouter();
const [step, setStep] = useState<'signers' | 'fields' | 'subject'>('signers'); const [step, setStep] = useState<EditDocumentStep>('signers');
const documentUrl = `data:application/pdf;base64,${document.document}`; const documentUrl = `data:application/pdf;base64,${document.document}`;
const onNextStep = () => { const documentFlow: Record<EditDocumentStep, DocumentFlowStep> = {
if (step === 'signers') { signers: {
setStep('fields'); title: 'Add Signers',
} description: 'Add the people who will sign the document.',
stepIndex: 1,
if (step === 'fields') { onSubmit: () => onAddSignersFormSubmit,
setStep('subject'); },
} fields: {
title: 'Add Fields',
description: 'Add all relevant fields for each recipient.',
stepIndex: 2,
onBackStep: () => setStep('signers'),
onSubmit: () => onAddFieldsFormSubmit,
},
subject: {
title: 'Add Subject',
description: 'Add the subject and message you wish to send to signers.',
stepIndex: 3,
onBackStep: () => setStep('fields'),
onSubmit: () => onAddSubjectFormSubmit,
},
}; };
const onPreviousStep = () => { const currentDocumentFlow = documentFlow[step];
if (step === 'fields') {
setStep('signers');
}
if (step === 'subject') {
setStep('fields');
}
};
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => { const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
try { try {
@ -72,7 +84,7 @@ export const EditDocumentForm = ({
router.refresh(); router.refresh();
onNextStep(); setStep('fields');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -94,7 +106,7 @@ export const EditDocumentForm = ({
router.refresh(); router.refresh();
onNextStep(); setStep('subject');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -119,8 +131,6 @@ export const EditDocumentForm = ({
}); });
router.refresh(); router.refresh();
onNextStep();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -144,38 +154,43 @@ export const EditDocumentForm = ({
</Card> </Card>
<div className="col-span-12 lg:col-span-6 xl:col-span-5"> <div className="col-span-12 lg:col-span-6 xl:col-span-5">
{step === 'signers' && ( <DocumentFlowFormContainer onSubmit={(e) => e.preventDefault()}>
<AddSignersFormPartial <DocumentFlowFormContainerHeader
recipients={recipients} title={currentDocumentFlow.title}
fields={fields} description={currentDocumentFlow.description}
document={document}
onContinue={onNextStep}
onGoBack={onPreviousStep}
onSubmit={onAddSignersFormSubmit}
/> />
)}
{step === 'fields' && ( {step === 'signers' && (
<AddFieldsFormPartial <AddSignersFormPartial
recipients={recipients} documentFlow={documentFlow.signers}
fields={fields} recipients={recipients}
document={document} fields={fields}
onContinue={onNextStep} numberOfSteps={Object.keys(documentFlow).length}
onGoBack={onPreviousStep} onSubmit={onAddSignersFormSubmit}
onSubmit={onAddFieldsFormSubmit} />
/> )}
)}
{step === 'subject' && ( {step === 'fields' && (
<AddSubjectFormPartial <AddFieldsFormPartial
recipients={recipients} documentFlow={documentFlow.fields}
fields={fields} recipients={recipients}
document={document} fields={fields}
onContinue={onNextStep} numberOfSteps={Object.keys(documentFlow).length}
onGoBack={onPreviousStep} onSubmit={onAddFieldsFormSubmit}
onSubmit={onAddSubjectFormSubmit} />
/> )}
)}
{step === 'subject' && (
<AddSubjectFormPartial
documentFlow={documentFlow.subject}
document={document}
recipients={recipients}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddSubjectFormSubmit}
/>
)}
</DocumentFlowFormContainer>
</div> </div>
</div> </div>
); );

View File

@ -13,9 +13,9 @@ export default function Loading() {
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl"> <h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document... Loading Document...
</h1> </h1>
<div className="mt-8 grid min-h-[80vh] w-full grid-cols-12 gap-x-8"> <div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
<div className="dark:bg-background border-border col-span-12 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7"> <div className="dark:bg-background border-border col-span-12 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7">
<div className="flex min-h-[80vh] flex-col items-center justify-center"> <div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center">
<Loader className="text-documenso h-12 w-12 animate-spin" /> <Loader className="text-documenso h-12 w-12 animate-spin" />
<p className="text-muted-foreground mt-4">Loading document...</p> <p className="text-muted-foreground mt-4">Loading document...</p>

View File

@ -50,7 +50,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
return ( return (
<form <form
className={cn( className={cn(
'dark:bg-background border-border bg-widget sticky top-20 flex h-[calc(100vh-6rem)] max-h-screen flex-col rounded-xl border px-4 py-6', 'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6',
)} )}
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >

View File

@ -0,0 +1,93 @@
'use client';
import { useCallback } from 'react';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
export const useDocumentElement = () => {
/**
* Given a mouse event, find the nearest element found by the provided selector.
*/
const getPage = (event: MouseEvent, pageSelector: string) => {
if (!(event.target instanceof HTMLElement)) {
return null;
}
const target = event.target;
const $page =
target.closest<HTMLElement>(pageSelector) ?? target.querySelector<HTMLElement>(pageSelector);
if (!$page) {
return null;
}
return $page;
};
/**
* Provided a page and a field, calculate the position of the field
* as a percentage of the page width and height.
*/
const getFieldPosition = (page: HTMLElement, field: HTMLElement) => {
const {
top: pageTop,
left: pageLeft,
height: pageHeight,
width: pageWidth,
} = getBoundingClientRect(page);
const {
top: fieldTop,
left: fieldLeft,
height: fieldHeight,
width: fieldWidth,
} = getBoundingClientRect(field);
return {
x: ((fieldLeft - pageLeft) / pageWidth) * 100,
y: ((fieldTop - pageTop) / pageHeight) * 100,
width: (fieldWidth / pageWidth) * 100,
height: (fieldHeight / pageHeight) * 100,
};
};
/**
* Given a mouse event, determine if the mouse is within the bounds of the
* nearest element found by the provided selector.
*
* @param mouseWidth The artifical width of the mouse.
* @param mouseHeight The artifical height of the mouse.
*/
const isWithinPageBounds = useCallback(
(event: MouseEvent, pageSelector: string, mouseWidth = 0, mouseHeight = 0) => {
const $page = getPage(event, pageSelector);
if (!$page) {
return false;
}
const { top, left, height, width } = $page.getBoundingClientRect();
const halfMouseWidth = mouseWidth / 2;
const halfMouseHeight = mouseHeight / 2;
if (event.clientY > top + height - halfMouseHeight || event.clientY < top + halfMouseHeight) {
return false;
}
if (event.clientX > left + width - halfMouseWidth || event.clientX < left + halfMouseWidth) {
return false;
}
return true;
},
[],
);
return {
getPage,
getFieldPosition,
isWithinPageBounds,
};
};

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { Slot } from '@radix-ui/react-slot'; import { Slot } from '@radix-ui/react-slot';
import { VariantProps, cva } from 'class-variance-authority'; import { VariantProps, cva } from 'class-variance-authority';
import { Loader } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
@ -30,17 +31,51 @@ const buttonVariants = cva(
}, },
); );
const loaderVariants = cva('mr-2 animate-spin', {
variants: {
size: {
default: 'h-5 w-5',
sm: 'h-4 w-4',
lg: 'h-5 w-5',
},
},
defaultVariants: {
size: 'default',
},
});
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean;
/**
* Will display the loading spinner and disable the button.
*/
loading?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'; if (asChild) {
return (
<Slot className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
}
const showLoader = props.loading === true;
const isDisabled = props.disabled || showLoader;
return ( return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> <button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
disabled={isDisabled}
>
{showLoader && <Loader className={cn('mr-2 animate-spin', loaderVariants({ size }))} />}
{props.children}
</button>
); );
}, },
); );

View File

@ -9,8 +9,9 @@ import { nanoid } from 'nanoid';
import { useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect'; import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { Document, Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client'; import { Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -26,14 +27,13 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
import { TAddFieldsFormSchema } from './add-fields.types'; import { TAddFieldsFormSchema } from './add-fields.types';
import { import {
DocumentFlowFormContainer,
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter, DocumentFlowFormContainerFooter,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { FieldItem } from './field-item'; import { FieldItem } from './field-item';
import { FRIENDLY_FIELD_TYPE } from './types'; import { DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
const fontCaveat = Caveat({ const fontCaveat = Caveat({
weight: ['500'], weight: ['500'],
@ -49,20 +49,24 @@ const MIN_HEIGHT_PX = 60;
const MIN_WIDTH_PX = 200; const MIN_WIDTH_PX = 200;
export type AddFieldsFormProps = { export type AddFieldsFormProps = {
documentFlow: DocumentFlowStep;
hideRecipients?: boolean;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
document: Document; numberOfSteps: number;
onContinue?: () => void;
onGoBack?: () => void;
onSubmit: (_data: TAddFieldsFormSchema) => void; onSubmit: (_data: TAddFieldsFormSchema) => void;
}; };
export const AddFieldsFormPartial = ({ export const AddFieldsFormPartial = ({
documentFlow,
hideRecipients = false,
recipients, recipients,
fields, fields,
onGoBack, numberOfSteps,
onSubmit, onSubmit,
}: AddFieldsFormProps) => { }: AddFieldsFormProps) => {
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
const { const {
control, control,
handleSubmit, handleSubmit,
@ -99,7 +103,7 @@ export const AddFieldsFormPartial = ({
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT; const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
const [visible, setVisible] = useState(false); const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
const [coords, setCoords] = useState({ const [coords, setCoords] = useState({
x: 0, x: 0,
y: 0, y: 0,
@ -110,86 +114,17 @@ export const AddFieldsFormPartial = ({
width: 0, width: 0,
}); });
/**
* Given a mouse event, find the nearest pdf page element.
*/
const getPage = (event: MouseEvent) => {
if (!(event.target instanceof HTMLElement)) {
return null;
}
const target = event.target;
const $page =
target.closest<HTMLElement>(PDF_VIEWER_PAGE_SELECTOR) ??
target.querySelector<HTMLElement>(PDF_VIEWER_PAGE_SELECTOR);
if (!$page) {
return null;
}
return $page;
};
/**
* Provided a page and a field, calculate the position of the field
* as a percentage of the page width and height.
*/
const getFieldPosition = (page: HTMLElement, field: HTMLElement) => {
const {
top: pageTop,
left: pageLeft,
height: pageHeight,
width: pageWidth,
} = getBoundingClientRect(page);
const {
top: fieldTop,
left: fieldLeft,
height: fieldHeight,
width: fieldWidth,
} = getBoundingClientRect(field);
return {
x: ((fieldLeft - pageLeft) / pageWidth) * 100,
y: ((fieldTop - pageTop) / pageHeight) * 100,
width: (fieldWidth / pageWidth) * 100,
height: (fieldHeight / pageHeight) * 100,
};
};
/**
* Given a mouse event, determine if the mouse is within the bounds of the
* nearest pdf page element.
*/
const isWithinPageBounds = useCallback((event: MouseEvent) => {
const $page = getPage(event);
if (!$page) {
return false;
}
const { top, left, height, width } = $page.getBoundingClientRect();
if (event.clientY > top + height || event.clientY < top) {
return false;
}
if (event.clientX > left + width || event.clientX < left) {
return false;
}
return true;
}, []);
const onMouseMove = useCallback( const onMouseMove = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
if (!isWithinPageBounds(event)) { setIsFieldWithinBounds(
setVisible(false); isWithinPageBounds(
return; event,
} PDF_VIEWER_PAGE_SELECTOR,
fieldBounds.current.width,
fieldBounds.current.height,
),
);
setVisible(true);
setCoords({ setCoords({
x: event.clientX - fieldBounds.current.width / 2, x: event.clientX - fieldBounds.current.width / 2,
y: event.clientY - fieldBounds.current.height / 2, y: event.clientY - fieldBounds.current.height / 2,
@ -204,9 +139,18 @@ export const AddFieldsFormPartial = ({
return; return;
} }
const $page = getPage(event); const $page = getPage(event, PDF_VIEWER_PAGE_SELECTOR);
if (!$page || !isWithinPageBounds(event)) { if (
!$page ||
!isWithinPageBounds(
event,
PDF_VIEWER_PAGE_SELECTOR,
fieldBounds.current.width,
fieldBounds.current.height,
)
) {
setSelectedField(null);
return; return;
} }
@ -237,10 +181,10 @@ export const AddFieldsFormPartial = ({
signerEmail: selectedSigner.email, signerEmail: selectedSigner.email,
}); });
setVisible(false); setIsFieldWithinBounds(false);
setSelectedField(null); setSelectedField(null);
}, },
[append, isWithinPageBounds, selectedField, selectedSigner], [append, isWithinPageBounds, selectedField, selectedSigner, getPage],
); );
const onFieldResize = useCallback( const onFieldResize = useCallback(
@ -270,7 +214,7 @@ export const AddFieldsFormPartial = ({
pageHeight, pageHeight,
}); });
}, },
[localFields, update], [getFieldPosition, localFields, update],
); );
const onFieldMove = useCallback( const onFieldMove = useCallback(
@ -293,7 +237,7 @@ export const AddFieldsFormPartial = ({
pageY, pageY,
}); });
}, },
[localFields, update], [getFieldPosition, localFields, update],
); );
useEffect(() => { useEffect(() => {
@ -328,15 +272,18 @@ export const AddFieldsFormPartial = ({
}, [recipients]); }, [recipients]);
return ( return (
<DocumentFlowFormContainer> <>
<DocumentFlowFormContainerContent <DocumentFlowFormContainerContent>
title="Add Fields"
description="Add all relevant fields for each recipient."
>
<div className="flex flex-col"> <div className="flex flex-col">
{selectedField && visible && ( {selectedField && (
<Card <Card
className="border-primary pointer-events-none fixed z-50 cursor-pointer bg-white" className={cn(
'pointer-events-none fixed z-50 cursor-pointer bg-white transition-opacity',
{
'border-primary': isFieldWithinBounds,
'opacity-50': !isFieldWithinBounds,
},
)}
style={{ style={{
top: coords.y, top: coords.y,
left: coords.x, left: coords.x,
@ -357,94 +304,100 @@ export const AddFieldsFormPartial = ({
disabled={selectedSigner?.email !== field.signerEmail || hasSelectedSignerBeenSent} disabled={selectedSigner?.email !== field.signerEmail || hasSelectedSignerBeenSent}
minHeight={fieldBounds.current.height} minHeight={fieldBounds.current.height}
minWidth={fieldBounds.current.width} minWidth={fieldBounds.current.width}
passive={visible && !!selectedField} passive={isFieldWithinBounds && !!selectedField}
onResize={(options) => onFieldResize(options, index)} onResize={(options) => onFieldResize(options, index)}
onMove={(options) => onFieldMove(options, index)} onMove={(options) => onFieldMove(options, index)}
onRemove={() => remove(index)} onRemove={() => remove(index)}
/> />
))} ))}
<Popover> {!hideRecipients && (
<PopoverTrigger asChild> <Popover>
<Button <PopoverTrigger asChild>
type="button" <Button
variant="outline" type="button"
role="combobox" variant="outline"
className="bg-background text-muted-foreground justify-between font-normal" role="combobox"
> className="bg-background text-muted-foreground mb-12 justify-between font-normal"
{selectedSigner?.email && ( >
<span className="flex-1 truncate text-left"> {selectedSigner?.email && (
{selectedSigner?.email} ({selectedSigner?.email}) <span className="flex-1 truncate text-left">
</span> {selectedSigner?.email} ({selectedSigner?.email})
)} </span>
)}
{!selectedSigner?.email && ( {!selectedSigner?.email && (
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span> <span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
)} )}
<ChevronsUpDown className="ml-2 h-4 w-4" /> <ChevronsUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent className="p-0" align="start">
<Command> <Command>
<CommandInput /> <CommandInput />
<CommandEmpty /> <CommandEmpty />
<CommandGroup> <CommandGroup>
{recipients.map((recipient, index) => ( {recipients.map((recipient, index) => (
<CommandItem <CommandItem
key={index} key={index}
className={cn({ className={cn({
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT, 'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
})} })}
onSelect={() => setSelectedSigner(recipient)} onSelect={() => setSelectedSigner(recipient)}
> >
{recipient.sendStatus !== SendStatus.SENT ? ( {recipient.sendStatus !== SendStatus.SENT ? (
<Check <Check
aria-hidden={recipient !== selectedSigner} aria-hidden={recipient !== selectedSigner}
className={cn('mr-2 h-4 w-4 flex-shrink-0', { className={cn('mr-2 h-4 w-4 flex-shrink-0', {
'opacity-0': recipient !== selectedSigner, 'opacity-0': recipient !== selectedSigner,
'opacity-100': recipient === selectedSigner, 'opacity-100': recipient === selectedSigner,
})} })}
/> />
) : ( ) : (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<Info className="mr-2 h-4 w-4" /> <Info className="mr-2 h-4 w-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-xs"> <TooltipContent className="max-w-xs">
This document has already been sent to this recipient. You can no longer This document has already been sent to this recipient. You can no
edit this recipient. longer edit this recipient.
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}
{recipient.name && ( {recipient.name && (
<span className="truncate" title={`${recipient.name} (${recipient.email})`}> <span
{recipient.name} ({recipient.email}) className="truncate"
</span> title={`${recipient.name} (${recipient.email})`}
)} >
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && ( {!recipient.name && (
<span className="truncate" title={recipient.email}> <span className="truncate" title={recipient.email}>
{recipient.email} {recipient.email}
</span> </span>
)} )}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)}
<div className="-mx-2 mt-8 flex-1 overflow-y-scroll px-2"> <div className="-mx-2 flex-1 overflow-y-scroll px-2">
<div className="mt-4 grid grid-cols-2 gap-x-4 gap-y-8"> <div className="grid grid-cols-2 gap-x-4 gap-y-8">
<button <button
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT} disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
onClick={() => setSelectedField(FieldType.SIGNATURE)} onClick={(e) => e.stopPropagation()}
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined} data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
> >
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50"> <Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
@ -467,7 +420,8 @@ export const AddFieldsFormPartial = ({
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT} disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
onClick={() => setSelectedField(FieldType.EMAIL)} onClick={(e) => e.stopPropagation()}
onMouseDown={() => setSelectedField(FieldType.EMAIL)}
data-selected={selectedField === FieldType.EMAIL ? true : undefined} data-selected={selectedField === FieldType.EMAIL ? true : undefined}
> >
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50"> <Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
@ -489,7 +443,8 @@ export const AddFieldsFormPartial = ({
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT} disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
onClick={() => setSelectedField(FieldType.NAME)} onClick={(e) => e.stopPropagation()}
onMouseDown={() => setSelectedField(FieldType.NAME)}
data-selected={selectedField === FieldType.NAME ? true : undefined} data-selected={selectedField === FieldType.NAME ? true : undefined}
> >
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50"> <Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
@ -511,7 +466,8 @@ export const AddFieldsFormPartial = ({
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT} disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
onClick={() => setSelectedField(FieldType.DATE)} onClick={(e) => e.stopPropagation()}
onMouseDown={() => setSelectedField(FieldType.DATE)}
data-selected={selectedField === FieldType.DATE ? true : undefined} data-selected={selectedField === FieldType.DATE ? true : undefined}
> >
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50"> <Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
@ -534,15 +490,19 @@ export const AddFieldsFormPartial = ({
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep title="Add Fields" step={2} maxStep={3} /> <DocumentFlowFormContainerStep
title={documentFlow.title}
step={documentFlow.stepIndex}
maxStep={numberOfSteps}
/>
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
onGoBackClick={documentFlow.onBackStep}
onGoNextClick={() => handleSubmit(onSubmit)()} onGoNextClick={() => handleSubmit(onSubmit)()}
onGoBackClick={onGoBack}
/> />
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>
</DocumentFlowFormContainer> </>
); );
}; };

View File

@ -2,40 +2,41 @@
import React, { useId } from 'react'; import React, { useId } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Plus, Trash } from 'lucide-react'; import { Plus, Trash } from 'lucide-react';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { Document, Field, Recipient, SendStatus } from '@documenso/prisma/client'; import { Field, Recipient, SendStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { TAddSignersFormSchema } from './add-signers.types'; import { TAddSignersFormSchema, ZAddSignersFormSchema } from './add-signers.types';
import { import {
DocumentFlowFormContainer,
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter, DocumentFlowFormContainerFooter,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { DocumentFlowStep } from './types';
export type AddSignersFormProps = { export type AddSignersFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
document: Document; numberOfSteps: number;
onContinue?: () => void;
onGoBack?: () => void;
onSubmit: (_data: TAddSignersFormSchema) => void; onSubmit: (_data: TAddSignersFormSchema) => void;
}; };
export const AddSignersFormPartial = ({ export const AddSignersFormPartial = ({
documentFlow,
numberOfSteps,
recipients, recipients,
fields: _fields, fields: _fields,
onGoBack,
onSubmit, onSubmit,
}: AddSignersFormProps) => { }: AddSignersFormProps) => {
const { toast } = useToast(); const { toast } = useToast();
@ -47,6 +48,7 @@ export const AddSignersFormPartial = ({
handleSubmit, handleSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<TAddSignersFormSchema>({ } = useForm<TAddSignersFormSchema>({
resolver: zodResolver(ZAddSignersFormSchema),
defaultValues: { defaultValues: {
signers: signers:
recipients.length > 0 recipients.length > 0
@ -116,11 +118,8 @@ export const AddSignersFormPartial = ({
}; };
return ( return (
<DocumentFlowFormContainer onSubmit={handleSubmit(onSubmit)}> <>
<DocumentFlowFormContainerContent <DocumentFlowFormContainerContent>
title="Add Signers"
description="Add the people who will sign the document."
>
<div className="flex w-full flex-col gap-y-4"> <div className="flex w-full flex-col gap-y-4">
<AnimatePresence> <AnimatePresence>
{signers.map((signer, index) => ( {signers.map((signer, index) => (
@ -205,15 +204,19 @@ export const AddSignersFormPartial = ({
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep title="Add Signers" step={1} maxStep={3} /> <DocumentFlowFormContainerStep
title={documentFlow.title}
step={documentFlow.stepIndex}
maxStep={numberOfSteps}
/>
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
onGoBackClick={documentFlow.onBackStep}
onGoNextClick={() => handleSubmit(onSubmit)()} onGoNextClick={() => handleSubmit(onSubmit)()}
onGoBackClick={onGoBack}
/> />
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>
</DocumentFlowFormContainer> </>
); );
}; };

View File

@ -3,35 +3,35 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Document, DocumentStatus, Field, Recipient } from '@documenso/prisma/client'; import { Document, DocumentStatus, Field, Recipient } from '@documenso/prisma/client';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { Textarea } from '@documenso/ui/primitives/textarea'; import { Textarea } from '@documenso/ui/primitives/textarea';
import { FormErrorMessage } from '~/components/form/form-error-message';
import { TAddSubjectFormSchema } from './add-subject.types'; import { TAddSubjectFormSchema } from './add-subject.types';
import { import {
DocumentFlowFormContainer,
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter, DocumentFlowFormContainerFooter,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { DocumentFlowStep } from './types';
export type AddSubjectFormProps = { export type AddSubjectFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
document: Document; document: Document;
onContinue?: () => void; numberOfSteps: number;
onGoBack?: () => void;
onSubmit: (_data: TAddSubjectFormSchema) => void; onSubmit: (_data: TAddSubjectFormSchema) => void;
}; };
export const AddSubjectFormPartial = ({ export const AddSubjectFormPartial = ({
documentFlow,
recipients: _recipients, recipients: _recipients,
fields: _fields, fields: _fields,
document, document,
onGoBack, numberOfSteps,
onSubmit, onSubmit,
}: AddSubjectFormProps) => { }: AddSubjectFormProps) => {
const { const {
@ -48,11 +48,8 @@ export const AddSubjectFormPartial = ({
}); });
return ( return (
<DocumentFlowFormContainer> <>
<DocumentFlowFormContainerContent <DocumentFlowFormContainerContent>
title="Add Subject"
description="Add the subject and message you wish to send to signers."
>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<div> <div>
@ -122,16 +119,20 @@ export const AddSubjectFormPartial = ({
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep title="Add Subject" step={3} maxStep={3} /> <DocumentFlowFormContainerStep
title={documentFlow.title}
step={documentFlow.stepIndex}
maxStep={numberOfSteps}
/>
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'} goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
onGoBackClick={documentFlow.onBackStep}
onGoNextClick={() => handleSubmit(onSubmit)()} onGoNextClick={() => handleSubmit(onSubmit)()}
onGoBackClick={onGoBack}
/> />
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>
</DocumentFlowFormContainer> </>
); );
}; };

View File

@ -2,7 +2,7 @@
import React, { HTMLAttributes } from 'react'; import React, { HTMLAttributes } from 'react';
import { Loader } from 'lucide-react'; import { motion } from 'framer-motion';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -21,7 +21,7 @@ export const DocumentFlowFormContainer = ({
<form <form
id={id} id={id}
className={cn( className={cn(
'dark:bg-background border-border bg-widget sticky top-20 flex h-[calc(100vh-6rem)] max-h-screen flex-col rounded-xl border px-4 py-6', 'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6',
className, className,
)} )}
{...props} {...props}
@ -31,27 +31,37 @@ export const DocumentFlowFormContainer = ({
); );
}; };
export type DocumentFlowFormContainerContentProps = HTMLAttributes<HTMLDivElement> & { export type DocumentFlowFormContainerHeaderProps = {
title: string; title: string;
description: string; description: string;
children?: React.ReactNode;
}; };
export const DocumentFlowFormContainerContent = ({ export const DocumentFlowFormContainerHeader = ({
children,
title, title,
description, description,
className, }: DocumentFlowFormContainerHeaderProps) => {
...props
}: DocumentFlowFormContainerContentProps) => {
return ( return (
<div className={cn('flex flex-1 flex-col', className)} {...props}> <>
<h3 className="text-foreground text-2xl font-semibold">{title}</h3> <h3 className="text-foreground text-2xl font-semibold">{title}</h3>
<p className="text-muted-foreground mt-2 text-sm">{description}</p> <p className="text-muted-foreground mt-2 text-sm">{description}</p>
<hr className="border-border mb-8 mt-4" /> <hr className="border-border mb-8 mt-4" />
</>
);
};
export type DocumentFlowFormContainerContentProps = HTMLAttributes<HTMLDivElement> & {
children?: React.ReactNode;
};
export const DocumentFlowFormContainerContent = ({
children,
className,
...props
}: DocumentFlowFormContainerContentProps) => {
return (
<div className={cn('flex flex-1 flex-col', className)} {...props}>
<div className="-mx-2 flex flex-1 flex-col overflow-y-auto px-2">{children}</div> <div className="-mx-2 flex flex-1 flex-col overflow-y-auto px-2">{children}</div>
</div> </div>
); );
@ -94,7 +104,9 @@ export const DocumentFlowFormContainerStep = ({
</p> </p>
<div className="bg-muted relative mt-4 h-[2px] rounded-md"> <div className="bg-muted relative mt-4 h-[2px] rounded-md">
<div <motion.div
layout="size"
layoutId="document-flow-container-step"
className="bg-documenso absolute inset-y-0 left-0" className="bg-documenso absolute inset-y-0 left-0"
style={{ style={{
width: `${(100 / maxStep) * step}%`, width: `${(100 / maxStep) * step}%`,
@ -133,20 +145,19 @@ export const DocumentFlowFormContainerActions = ({
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10" className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
size="lg" size="lg"
variant="secondary" variant="secondary"
disabled={disabled || loading || !canGoBack} disabled={disabled || loading || !canGoBack || !onGoBackClick}
onClick={onGoBackClick} onClick={onGoBackClick}
> >
{goBackLabel} {goBackLabel}
</Button> </Button>
<Button <Button
type="button" type="submit"
className="bg-documenso flex-1" className="bg-documenso flex-1"
size="lg" size="lg"
disabled={disabled || loading || !canGoNext} disabled={disabled || loading || !canGoNext}
onClick={onGoNextClick} onClick={onGoNextClick}
> >
{loading && <Loader className="mr-2 h-5 w-5 animate-spin" />}
{goNextLabel} {goNextLabel}
</Button> </Button>
</div> </div>

View File

@ -91,9 +91,10 @@ export const FieldItem = ({
return createPortal( return createPortal(
<Rnd <Rnd
key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth} key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth}
className={cn('absolute z-20', { className={cn('z-20', {
'pointer-events-none': passive, 'pointer-events-none': passive,
'pointer-events-none z-10 opacity-75': disabled, 'pointer-events-none opacity-75': disabled,
'z-10': !active || disabled,
})} })}
// minHeight={minHeight} // minHeight={minHeight}
// minWidth={minWidth} // minWidth={minWidth}
@ -117,7 +118,7 @@ export const FieldItem = ({
> >
{!disabled && ( {!disabled && (
<button <button
className="text-muted-foreground/50 hover:text-muted-foreground/80 absolute -right-2 -top-2 z-[9999] flex h-8 w-8 items-center justify-center rounded-full border bg-white shadow-[0_0_0_2px_theme(colors.gray.100/70%)]" className="text-muted-foreground/50 hover:text-muted-foreground/80 absolute -right-2 -top-2 z-20 flex h-8 w-8 items-center justify-center rounded-full border bg-white shadow-[0_0_0_2px_theme(colors.gray.100/70%)]"
onClick={() => onRemove?.()} onClick={() => onRemove?.()}
> >
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />

View File

@ -47,3 +47,12 @@ export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
[FieldType.EMAIL]: 'Email', [FieldType.EMAIL]: 'Email',
[FieldType.NAME]: 'Name', [FieldType.NAME]: 'Name',
}; };
export interface DocumentFlowStep {
title: string;
description: string;
stepIndex: number;
onSubmit?: () => void;
onBackStep?: () => void;
onNextStep?: () => void;
}

View File

@ -7,7 +7,7 @@ import { Loader } from 'lucide-react';
export const LazyPDFViewer = dynamic(async () => import('./pdf-viewer'), { export const LazyPDFViewer = dynamic(async () => import('./pdf-viewer'), {
ssr: false, ssr: false,
loading: () => ( loading: () => (
<div className="dark:bg-background flex min-h-[80vh] flex-col items-center justify-center bg-white/50"> <div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<Loader className="text-documenso h-12 w-12 animate-spin" /> <Loader className="text-documenso h-12 w-12 animate-spin" />
<p className="text-muted-foreground mt-4">Loading document...</p> <p className="text-muted-foreground mt-4">Loading document...</p>

View File

@ -104,11 +104,13 @@ export const PDFViewer = ({ className, document, onPageClick, ...props }: PDFVie
<div ref={$el} className={cn('overflow-hidden', className)} {...props}> <div ref={$el} className={cn('overflow-hidden', className)} {...props}>
<PDFDocument <PDFDocument
file={document} file={document}
className="w-full overflow-hidden rounded" className={cn('w-full overflow-hidden rounded', {
'h-[80vh] max-h-[60rem]': numPages === 0,
})}
onLoadSuccess={(d) => onDocumentLoaded(d)} onLoadSuccess={(d) => onDocumentLoaded(d)}
externalLinkTarget="_blank" externalLinkTarget="_blank"
loading={ loading={
<div className="dark:bg-background flex min-h-[80vh] flex-col items-center justify-center bg-white/50"> <div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<Loader className="text-documenso h-12 w-12 animate-spin" /> <Loader className="text-documenso h-12 w-12 animate-spin" />
<p className="text-muted-foreground mt-4">Loading document...</p> <p className="text-muted-foreground mt-4">Loading document...</p>

View File

@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const baseConfig = require('@documenso/tailwind-config');
module.exports = {
...baseConfig,
content: [
...baseConfig.content,
'./primitives/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./lib/**/*.{ts,tsx}',
],
};