Merge branch 'main' into fix/layout-shift-on-table

This commit is contained in:
Lucas Smith
2024-03-09 00:41:20 +11:00
committed by GitHub
369 changed files with 16462 additions and 2219 deletions

View File

@ -0,0 +1,31 @@
import { forwardRef } from 'react';
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
export const VerifiedIcon: LucideIcon = forwardRef(
({ size = 24, color = 'currentColor', ...props }, ref) => {
return (
<svg
ref={ref}
width={size}
height={size}
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g id="badge, verified, award">
<path
id="Icon"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.5457 2.89094C11.5779 1.70302 13.4223 1.70302 14.4545 2.89094L15.2585 3.81628C15.3917 3.96967 15.5947 4.04354 15.7954 4.0117L17.0061 3.81965C18.5603 3.57309 19.9732 4.75869 20.0003 6.33214L20.0214 7.55778C20.0249 7.76096 20.1329 7.94799 20.3071 8.05261L21.358 8.6837C22.7071 9.49389 23.0274 11.3103 22.0368 12.5331L21.2651 13.4855C21.1372 13.6434 21.0997 13.8561 21.1659 14.0482L21.5652 15.2072C22.0779 16.695 21.1557 18.2923 19.6109 18.5922L18.4075 18.8258C18.208 18.8646 18.0426 19.0034 17.9698 19.1931L17.5308 20.3376C16.9672 21.8069 15.234 22.4378 13.8578 21.6745L12.7858 21.08C12.6081 20.9814 12.3921 20.9814 12.2144 21.08L11.1424 21.6745C9.76623 22.4378 8.033 21.8069 7.4694 20.3376L7.03038 19.1931C6.9576 19.0034 6.79216 18.8646 6.59268 18.8258L5.38932 18.5922C3.84448 18.2923 2.92224 16.695 3.43495 15.2072L3.83431 14.0482C3.90052 13.8561 3.86302 13.6434 3.7351 13.4855L2.96343 12.5331C1.97279 11.3103 2.29307 9.49389 3.64218 8.6837L4.69306 8.05261C4.86728 7.94799 4.97526 7.76096 4.97875 7.55778L4.99985 6.33214C5.02694 4.75869 6.43987 3.57309 7.99413 3.81965L9.20481 4.0117C9.40551 4.04354 9.60845 3.96967 9.74173 3.81628L10.5457 2.89094ZM15.7072 11.2071C16.0977 10.8166 16.0977 10.1834 15.7072 9.79289C15.3167 9.40237 14.6835 9.40237 14.293 9.79289L11.5001 12.5858L10.7072 11.7929C10.3167 11.4024 9.68351 11.4024 9.29298 11.7929C8.90246 12.1834 8.90246 12.8166 9.29298 13.2071L10.4394 14.3536C11.0252 14.9393 11.975 14.9393 12.5608 14.3536L15.7072 11.2071Z"
fill={color}
/>
</g>
</svg>
);
},
);
VerifiedIcon.displayName = 'VerifiedIcon';

View File

@ -0,0 +1,120 @@
import type { Variants } from 'framer-motion';
export const DocumentDropzoneContainerVariants: Variants = {
initial: {
scale: 1,
},
animate: {
scale: 1,
},
hover: {
transition: {
staggerChildren: 0.05,
},
},
};
export const DocumentDropzoneCardLeftVariants: Variants = {
initial: {
x: 40,
y: -10,
rotate: -14,
},
animate: {
x: 40,
y: -10,
rotate: -14,
},
hover: {
x: -25,
y: -25,
rotate: -22,
},
};
export const DocumentDropzoneCardRightVariants: Variants = {
initial: {
x: -40,
y: -10,
rotate: 14,
},
animate: {
x: -40,
y: -10,
rotate: 14,
},
hover: {
x: 25,
y: -25,
rotate: 22,
},
};
export const DocumentDropzoneCardCenterVariants: Variants = {
initial: {
x: 0,
y: 0,
},
animate: {
x: 0,
y: 0,
},
hover: {
x: 0,
y: -25,
},
};
export const DocumentDropzoneDisabledCardLeftVariants: Variants = {
initial: {
x: 50,
y: 0,
rotate: -14,
},
animate: {
x: 50,
y: 0,
rotate: -14,
},
hover: {
x: 30,
y: 0,
rotate: -17,
transition: { type: 'spring', duration: 0.3, stiffness: 500 },
},
};
export const DocumentDropzoneDisabledCardRightVariants: Variants = {
initial: {
x: -50,
y: 0,
rotate: 14,
},
animate: {
x: -50,
y: 0,
rotate: 14,
},
hover: {
x: -30,
y: 0,
rotate: 17,
transition: { type: 'spring', duration: 0.3, stiffness: 500 },
},
};
export const DocumentDropzoneDisabledCardCenterVariants: Variants = {
initial: {
x: -10,
y: 0,
},
animate: {
x: -10,
y: 0,
},
hover: {
x: [-15, -10, -5, -10],
y: 0,
transition: { type: 'spring', duration: 0.3, stiffness: 1000 },
},
};

View File

@ -64,6 +64,7 @@
"luxon": "^3.4.2",
"next": "14.0.3",
"pdfjs-dist": "3.6.172",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.7.1",
"react-hook-form": "^7.45.4",
"react-pdf": "7.3.3",

View File

@ -6,20 +6,30 @@ import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';
const badgeVariants = cva(
'inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center rounded-md text-xs font-medium ring-1 ring-inset w-fit',
{
variants: {
variant: {
default: 'bg-primary hover:bg-primary/80 border-transparent text-primary-foreground',
secondary:
'bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground',
neutral:
'bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20',
destructive:
'bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground',
outline: 'text-foreground',
'bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20',
warning:
'bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:ring-yellow-400/20',
default:
'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20',
secondary:
'bg-blue-50 text-blue-700 ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30',
},
size: {
small: 'px-1.5 py-0.5 text-xs',
default: 'px-2 py-1.5 text-xs',
large: 'px-3 py-2 text-sm',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
@ -28,8 +38,8 @@ export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
function Badge({ className, variant, size, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant, size }), className)} {...props} />;
}
export { Badge, badgeVariants };

View File

@ -13,7 +13,8 @@ const buttonVariants = cva(
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',

View File

@ -0,0 +1,82 @@
import type { HTMLAttributes } from 'react';
import React, { useState } from 'react';
import { HexColorInput, HexColorPicker } from 'react-colorful';
import { cn } from '../lib/utils';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
export type ColorPickerProps = {
disabled?: boolean;
value: string;
defaultValue?: string;
onChange: (color: string) => void;
} & HTMLAttributes<HTMLDivElement>;
export const ColorPicker = ({
className,
disabled = false,
value,
defaultValue = '#000000',
onChange,
...props
}: ColorPickerProps) => {
const [color, setColor] = useState(value || defaultValue);
const [inputColor, setInputColor] = useState(value || defaultValue);
const onColorChange = (newColor: string) => {
setColor(newColor);
setInputColor(newColor);
onChange(newColor);
};
const onInputChange = (newColor: string) => {
setInputColor(newColor);
};
const onInputBlur = () => {
setColor(inputColor);
onChange(inputColor);
};
return (
<Popover>
<PopoverTrigger>
<button
type="button"
disabled={disabled}
className="bg-background h-12 w-12 rounded-md border p-1 disabled:pointer-events-none disabled:opacity-50"
>
<div className="h-full w-full rounded-sm" style={{ backgroundColor: color }} />
</button>
</PopoverTrigger>
<PopoverContent className="w-auto">
<HexColorPicker
className={cn(
className,
'w-full aria-disabled:pointer-events-none aria-disabled:opacity-50',
)}
color={color}
onChange={onColorChange}
aria-disabled={disabled}
{...props}
/>
<HexColorInput
className="mt-4 h-10 rounded-md border bg-transparent px-3 py-2 text-sm disabled:pointer-events-none disabled:opacity-50"
color={inputColor}
onChange={onInputChange}
onBlur={onInputBlur}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
onInputBlur();
}
}}
disabled={disabled}
/>
</PopoverContent>
</Popover>
);
};

View File

@ -1,90 +1,27 @@
'use client';
import type { Variants } from 'framer-motion';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { Plus } from 'lucide-react';
import { AlertTriangle, Plus } from 'lucide-react';
import { useDropzone } from 'react-dropzone';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import {
DocumentDropzoneCardCenterVariants,
DocumentDropzoneCardLeftVariants,
DocumentDropzoneCardRightVariants,
DocumentDropzoneContainerVariants,
DocumentDropzoneDisabledCardCenterVariants,
DocumentDropzoneDisabledCardLeftVariants,
DocumentDropzoneDisabledCardRightVariants,
} from '../lib/document-dropzone-constants';
import { cn } from '../lib/utils';
import { Button } from './button';
import { Card, CardContent } from './card';
const DocumentDropzoneContainerVariants: Variants = {
initial: {
scale: 1,
},
animate: {
scale: 1,
},
hover: {
transition: {
staggerChildren: 0.05,
},
},
};
const DocumentDropzoneCardLeftVariants: Variants = {
initial: {
x: 40,
y: -10,
rotate: -14,
},
animate: {
x: 40,
y: -10,
rotate: -14,
},
hover: {
x: -25,
y: -25,
rotate: -22,
},
};
const DocumentDropzoneCardRightVariants: Variants = {
initial: {
x: -40,
y: -10,
rotate: 14,
},
animate: {
x: -40,
y: -10,
rotate: 14,
},
hover: {
x: 25,
y: -25,
rotate: 22,
},
};
const DocumentDropzoneCardCenterVariants: Variants = {
initial: {
x: 0,
y: 0,
},
animate: {
x: 0,
y: 0,
},
hover: {
x: 0,
y: -25,
},
};
const DocumentDescription = {
document: {
headline: 'Add a document',
},
template: {
headline: 'Upload Template Document',
},
};
export type DocumentDropzoneProps = {
className?: string;
disabled?: boolean;
@ -123,68 +60,108 @@ export const DocumentDropzone = ({
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
});
const heading = {
document: disabled ? 'You have reached your document limit.' : 'Add a document',
template: 'Upload Template Document',
};
return (
<motion.div
className={cn('flex aria-disabled:cursor-not-allowed', className)}
variants={DocumentDropzoneContainerVariants}
initial="initial"
animate="animate"
whileHover="hover"
aria-disabled={disabled}
>
<Card
role="button"
className={cn(
'focus-visible:ring-ring ring-offset-background flex flex-1 cursor-pointer flex-col items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 aria-disabled:pointer-events-none aria-disabled:opacity-60',
'focus-visible:ring-ring ring-offset-background group flex flex-1 cursor-pointer flex-col items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
className,
)}
gradient={true}
gradient={!disabled}
degrees={120}
aria-disabled={disabled}
{...getRootProps()}
{...props}
>
<CardContent className="text-muted-foreground/40 flex flex-col items-center justify-center p-6">
{/* <FilePlus strokeWidth="1px" className="h-16 w-16"/> */}
<div className="flex">
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={!disabled ? DocumentDropzoneCardLeftVariants : undefined}
>
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
</motion.div>
{disabled ? (
// Disabled State
<div className="flex">
<motion.div
className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneDisabledCardLeftVariants}
>
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={!disabled ? DocumentDropzoneCardCenterVariants : undefined}
>
<Plus
strokeWidth="2px"
className="text-muted-foreground/20 group-hover:text-documenso h-12 w-12"
/>
</motion.div>
<motion.div
className="group-hover:bg-destructive/5 border-muted-foreground/20 group-hover:border-destructive/50 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneDisabledCardCenterVariants}
>
<AlertTriangle
strokeWidth="2px"
className="text-muted-foreground/20 group-hover:text-destructive h-12 w-12"
/>
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={!disabled ? DocumentDropzoneCardRightVariants : undefined}
>
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
</motion.div>
</div>
<motion.div
className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneDisabledCardRightVariants}
>
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
</motion.div>
</div>
) : (
// Non Disabled State
<div className="flex">
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardLeftVariants}
>
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardCenterVariants}
>
<Plus
strokeWidth="2px"
className="text-muted-foreground/20 group-hover:text-documenso h-12 w-12"
/>
</motion.div>
<motion.div
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
variants={DocumentDropzoneCardRightVariants}
>
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
</motion.div>
</div>
)}
<input {...getInputProps()} />
<p className="group-hover:text-foreground text-muted-foreground mt-8 font-medium">
{DocumentDescription[type].headline}
</p>
<p className="text-foreground mt-8 font-medium">{heading[type]}</p>
<p className="text-muted-foreground/80 mt-1 text-sm">
<p className="text-muted-foreground/80 mt-1 text-center text-sm">
{disabled ? disabledMessage : 'Drag & drop your PDF here.'}
</p>
{disabled && IS_BILLING_ENABLED() && (
<Button className="hover:bg-warning/80 bg-warning mt-4 w-32" asChild>
<Link href="/settings/billing">Upgrade</Link>
</Button>
)}
</CardContent>
</Card>
</motion.div>

View File

@ -323,9 +323,9 @@ export const AddFieldsFormPartial = ({
{selectedField && (
<Card
className={cn(
'bg-background pointer-events-none fixed z-50 cursor-pointer transition-opacity',
'bg-field-card/80 pointer-events-none fixed z-50 cursor-pointer border-2 backdrop-blur-[1px]',
{
'border-primary': isFieldWithinBounds,
'border-field-card-border': isFieldWithinBounds,
'opacity-50': !isFieldWithinBounds,
},
)}
@ -336,7 +336,7 @@ export const AddFieldsFormPartial = ({
width: fieldBounds.current.width,
}}
>
<CardContent className="text-foreground flex h-full w-full items-center justify-center p-2">
<CardContent className="text-field-card-foreground flex h-full w-full items-center justify-center p-2">
{FRIENDLY_FIELD_TYPE[selectedField]}
</CardContent>
</Card>
@ -552,6 +552,28 @@ export const AddFieldsFormPartial = ({
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"
onClick={() => setSelectedField(FieldType.TEXT)}
onMouseDown={() => setSelectedField(FieldType.TEXT)}
data-selected={selectedField === FieldType.TEXT ? true : undefined}
>
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
)}
>
{'Text'}
</p>
<p className="text-muted-foreground mt-2 text-xs">Custom Text</p>
</CardContent>
</Card>
</button>
</fieldset>
</div>
</div>

View File

@ -44,6 +44,7 @@ export type AddSignatureFormProps = {
onSubmit: (_data: TAddSignatureFormSchema) => Promise<void> | void;
requireName?: boolean;
requireCustomText?: boolean;
requireSignature?: boolean;
};
@ -54,6 +55,7 @@ export const AddSignatureFormPartial = ({
onSubmit,
requireName = false,
requireCustomText = false,
requireSignature = true,
}: AddSignatureFormProps) => {
const { currentStep, totalSteps } = useStep();
@ -70,6 +72,14 @@ export const AddSignatureFormPartial = ({
});
}
if (requireCustomText && val.customText.length === 0) {
ctx.addIssue({
path: ['customText'],
code: 'custom',
message: 'Text is required',
});
}
if (requireSignature && val.signature.length === 0) {
ctx.addIssue({
path: ['signature'],
@ -85,6 +95,7 @@ export const AddSignatureFormPartial = ({
name: '',
email: '',
signature: '',
customText: '',
},
});
@ -131,6 +142,11 @@ export const AddSignatureFormPartial = ({
return !form.formState.errors.email;
}
if (fieldType === FieldType.TEXT) {
await form.trigger('customText');
return !form.formState.errors.customText;
}
return true;
};
@ -154,6 +170,11 @@ export const AddSignatureFormPartial = ({
customText: form.getValues('name'),
inserted: true,
}))
.with(FieldType.TEXT, () => ({
...field,
customText: form.getValues('customText'),
inserted: true,
}))
.with(FieldType.SIGNATURE, () => {
const value = form.getValues('signature');
@ -302,6 +323,29 @@ export const AddSignatureFormPartial = ({
)}
/>
)}
{requireCustomText && (
<FormField
control={form.control}
name="customText"
render={({ field }) => (
<FormItem>
<FormLabel required={requireCustomText}>Custom Text</FormLabel>
<FormControl>
<Input
className="bg-background"
{...field}
onChange={(value) => {
onFormValueChange(FieldType.TEXT);
field.onChange(value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</DocumentFlowFormContainerContent>
@ -330,7 +374,7 @@ export const AddSignatureFormPartial = ({
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{localFields.map((field) =>
match(field.type)
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
.with(FieldType.DATE, FieldType.TEXT, FieldType.EMAIL, FieldType.NAME, () => {
return (
<SinglePlayerModeCustomTextField
onClick={insertField(field)}

View File

@ -6,6 +6,7 @@ export const ZAddSignatureFormSchema = z.object({
.min(1, { message: 'Email is required' })
.email({ message: 'Invalid email address' }),
name: z.string(),
customText: z.string(),
signature: z.string(),
});

View File

@ -4,7 +4,7 @@ import React, { useId } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { BadgeCheck, Copy, Eye, PencilLine, Plus, Trash } from 'lucide-react';
import { Plus, Trash } from 'lucide-react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
@ -17,6 +17,7 @@ import { Button } from '../button';
import { FormErrorMessage } from '../form/form-error-message';
import { Input } from '../input';
import { Label } from '../label';
import { ROLE_ICONS } from '../recipient-role-icons';
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
import { useStep } from '../stepper';
import { useToast } from '../use-toast';
@ -32,13 +33,6 @@ import {
import { ShowFieldItem } from './show-field-item';
import type { DocumentFlowStep } from './types';
const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
SIGNER: <PencilLine className="h-4 w-4" />,
APPROVER: <BadgeCheck className="h-4 w-4" />,
CC: <Copy className="h-4 w-4" />,
VIEWER: <Eye className="h-4 w-4" />,
};
export type AddSignersFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];

View File

@ -226,7 +226,7 @@ export const AddSubjectFormPartial = ({
</>
)}
<div className="flex flex-col">
<div className="mt-2 flex flex-col">
<div className="flex flex-col gap-y-4">
<div>
<Label htmlFor="redirectUrl" className="flex items-center">

View File

@ -128,24 +128,22 @@ export const FieldItem = ({
)}
<Card
className={cn('bg-background h-full w-full', {
'border-primary': !disabled,
'border-primary/80': active,
className={cn('bg-field-card/80 h-full w-full backdrop-blur-[1px]', {
'border-field-card-border': !disabled,
'border-field-card-border/80': active,
})}
>
<CardContent
className={cn(
'text-foreground flex h-full w-full flex-col items-center justify-center p-2',
'text-field-card-foreground flex h-full w-full flex-col items-center justify-center p-2',
{
'text-muted-foreground/50': disabled,
'text-field-card-foreground/50': disabled,
},
)}
>
{FRIENDLY_FIELD_TYPE[field.type]}
<p className="text-muted-foreground/50 w-full truncate text-center text-xs">
{field.signerEmail}
</p>
<p className="w-full truncate text-center text-xs">{field.signerEmail}</p>
</CardContent>
</Card>
</Rnd>,

View File

@ -172,6 +172,7 @@ export function SinglePlayerModeCustomTextField({
.with(FieldType.DATE, () => 'Date')
.with(FieldType.NAME, () => 'Name')
.with(FieldType.EMAIL, () => 'Email')
.with(FieldType.TEXT, () => 'Text')
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, () => 'Signature')
.otherwise(() => '')}
</button>

View File

@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'bg-background border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
{
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],

View File

@ -233,18 +233,20 @@ export const PDFViewer = ({
{Array(numPages)
.fill(null)
.map((_, i) => (
<div
key={i}
className="border-border my-8 overflow-hidden rounded border will-change-transform first:mt-0 last:mb-0"
>
<PDFPage
pageNumber={i + 1}
width={width}
renderAnnotationLayer={false}
renderTextLayer={false}
loading={() => ''}
onClick={(e) => onDocumentPageClick(e, i + 1)}
/>
<div key={i} className="last:-mb-2">
<div className="border-border overflow-hidden rounded border will-change-transform">
<PDFPage
pageNumber={i + 1}
width={width}
renderAnnotationLayer={false}
renderTextLayer={false}
loading={() => ''}
onClick={(e) => onDocumentPageClick(e, i + 1)}
/>
</div>
<p className="text-muted-foreground/80 my-2 text-center text-[11px]">
Page {i + 1} of {numPages}
</p>
</div>
))}
</PDFDocument>

View File

@ -0,0 +1,10 @@
import { BadgeCheck, Copy, Eye, PencilLine } from 'lucide-react';
import type { RecipientRole } from '.prisma/client';
export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
SIGNER: <PencilLine className="h-4 w-4" />,
APPROVER: <BadgeCheck className="h-4 w-4" />,
CC: <Copy className="h-4 w-4" />,
VIEWER: <Eye className="h-4 w-4" />,
};

View File

@ -143,14 +143,17 @@ const sheetVariants = cva(
export interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
VariantProps<typeof sheetVariants> {
showOverlay?: boolean;
sheetClass?: string;
}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
DialogContentProps
>(({ position, size, className, children, ...props }, ref) => (
>(({ position, size, className, sheetClass, showOverlay = true, children, ...props }, ref) => (
<SheetPortal position={position}>
<SheetOverlay />
{showOverlay && <SheetOverlay className={sheetClass} />}
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ position, size }), className)}

View File

@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
@ -10,9 +10,10 @@ import { useFieldArray, useForm } from 'react-hook-form';
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 { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { nanoid } from '@documenso/lib/universal/id';
import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -291,6 +292,28 @@ export const AddTemplateFieldsFormPartial = ({
setSelectedSigner(recipients[0]);
}, [recipients]);
const recipientsByRole = useMemo(() => {
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
CC: [],
VIEWER: [],
SIGNER: [],
APPROVER: [],
};
recipients.forEach((recipient) => {
recipientsByRole[recipient.role].push(recipient);
});
return recipientsByRole;
}, [recipients]);
const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
);
}, [recipientsByRole]);
return (
<>
<DocumentFlowFormContainerContent>
@ -363,55 +386,49 @@ export const AddTemplateFieldsFormPartial = ({
</span>
</CommandEmpty>
<CommandGroup>
{recipients.map((recipient, index) => (
<CommandItem
key={index}
className={cn({
// 'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
})}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
{/* {recipient.sendStatus !== SendStatus.SENT ? (
<Check
aria-hidden={recipient !== selectedSigner}
className={cn('mr-2 h-4 w-4 flex-shrink-0', {
'opacity-0': recipient !== selectedSigner,
'opacity-100': recipient === selectedSigner,
})}
/>
) : (
<Tooltip>
<TooltipTrigger>
<Info className="mr-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
This document has already been sent to this recipient. You can no
longer edit this recipient.
</TooltipContent>
</Tooltip>
)} */}
{recipientsByRoleToDisplay.map(([role, recipients], roleIndex) => (
<CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
</div>
{recipient.name && (
{recipients.length === 0 && (
<div
key={`${role}-empty`}
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
>
No recipients with this role
</div>
)}
{recipients.map((recipient) => (
<CommandItem
key={recipient.id}
className={cn('px-2 last:mb-1 [&:not(:first-child)]:mt-1')}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
<span
className="truncate"
title={`${recipient.name} (${recipient.email})`}
className={cn('text-foreground/70 truncate', {
'text-foreground/80': recipient === selectedSigner,
})}
>
{recipient.name} ({recipient.email})
</span>
)}
{recipient.name && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && (
<span className="truncate" title={recipient.email}>
{recipient.email}
{!recipient.name && (
<span title={recipient.email}>{recipient.email}</span>
)}
</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandItem>
))}
</CommandGroup>
))}
</Command>
</PopoverContent>
</Popover>
@ -511,6 +528,28 @@ export const AddTemplateFieldsFormPartial = ({
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"
onClick={() => setSelectedField(FieldType.TEXT)}
onMouseDown={() => setSelectedField(FieldType.TEXT)}
data-selected={selectedField === FieldType.TEXT ? true : undefined}
>
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
)}
>
{'Text'}
</p>
<p className="text-muted-foreground mt-2 text-xs">Custom Text</p>
</CardContent>
</Card>
</button>
</div>
</div>
</div>

View File

@ -5,10 +5,10 @@ import React, { useId, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { Plus, Trash } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { nanoid } from '@documenso/lib/universal/id';
import type { Field, Recipient } from '@documenso/prisma/client';
import { type Field, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input';
@ -21,6 +21,8 @@ import {
DocumentFlowFormContainerStep,
} from '../document-flow/document-flow-root';
import type { DocumentFlowStep } from '../document-flow/types';
import { ROLE_ICONS } from '../recipient-role-icons';
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
import { useStep } from '../stepper';
import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
@ -59,12 +61,14 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
formId: String(recipient.id),
name: recipient.name,
email: recipient.email,
role: recipient.role,
}))
: [
{
formId: initialId,
name: `Recipient 1`,
email: `recipient.1@documenso.com`,
role: RecipientRole.SIGNER,
},
],
},
@ -86,6 +90,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
formId: nanoid(12),
name: `Recipient ${placeholderRecipientCount}`,
email: `recipient.${placeholderRecipientCount}@documenso.com`,
role: RecipientRole.SIGNER,
});
setPlaceholderRecipientCount((count) => count + 1);
@ -95,12 +100,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
removeSigner(index);
};
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
onAddPlaceholderRecipient();
}
};
return (
<>
<DocumentFlowFormContainerContent>
@ -113,10 +112,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
className="flex flex-wrap items-end gap-x-4"
>
<div className="flex-1">
<Label htmlFor={`signer-${signer.id}-email`}>
Email
<span className="text-destructive ml-1 inline-block font-medium">*</span>
</Label>
<Label htmlFor={`signer-${signer.id}-email`}>Email</Label>
<Input
id={`signer-${signer.id}-email`}
@ -139,6 +135,48 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
/>
</div>
<div className="w-[60px]">
<Controller
control={control}
name={`signers.${index}.role`}
render={({ field: { value, onChange } }) => (
<Select value={value} onValueChange={(x) => onChange(x)}>
<SelectTrigger className="bg-background">{ROLE_ICONS[value]}</SelectTrigger>
<SelectContent className="" align="end">
<SelectItem value={RecipientRole.SIGNER}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
Signer
</div>
</SelectItem>
<SelectItem value={RecipientRole.CC}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
Receives copy
</div>
</SelectItem>
<SelectItem value={RecipientRole.APPROVER}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
Approver
</div>
</SelectItem>
<SelectItem value={RecipientRole.VIEWER}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
Viewer
</div>
</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div>
<button
type="button"

View File

@ -1,5 +1,7 @@
import { z } from 'zod';
import { RecipientRole } from '.prisma/client';
export const ZAddTemplatePlacholderRecipientsFormSchema = z
.object({
signers: z.array(
@ -8,6 +10,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
nativeId: z.number().optional(),
email: z.string().min(1).email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
}),
),
})

View File

@ -3,7 +3,8 @@
import * as React from 'react';
import * as TogglePrimitive from '@radix-ui/react-toggle';
import { VariantProps, cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { cn } from '../lib/utils';

View File

@ -18,6 +18,10 @@
--card-border-tint: 112 205 159;
--card-foreground: 222.2 47.4% 11.2%;
--field-card: 95 74% 90%;
--field-card-border: 95.08 71.08% 67.45%;
--field-card-foreground: 222.2 47.4% 11.2%;
--widget: 0 0% 97%;
--border: 214.3 31.8% 91.4%;
@ -38,6 +42,8 @@
--ring: 95.08 71.08% 67.45%;
--radius: 0.5rem;
--warning: 54 96% 45%;
}
.dark {
@ -55,6 +61,8 @@
--card-border-tint: 112 205 159;
--card-foreground: 0 0% 95%;
--widget: 0 0% 14.9%;
--border: 0 0% 27.9%;
--input: 0 0% 27.9%;
@ -73,6 +81,8 @@
--ring: 95.08 71.08% 67.45%;
--radius: 0.5rem;
--warning: 54 96% 45%;
}
}