Compare commits
27 Commits
ElTimuro-p
...
feat/accep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
536adf6e0a | ||
|
|
be3ab09738 | ||
|
|
1c4a5449bb | ||
|
|
144bd4782b | ||
|
|
857e35c10a | ||
|
|
e9d6c24137 | ||
|
|
dd5f39205a | ||
|
|
f6ce7be61f | ||
|
|
9979d32a56 | ||
|
|
daa541d570 | ||
|
|
1d91a9e813 | ||
|
|
560352492d | ||
|
|
075e15d428 | ||
|
|
27e5ef0a51 | ||
|
|
84b0c2756b | ||
|
|
58b3a127ea | ||
|
|
7e71e06e04 | ||
|
|
0b8e84b6b7 | ||
|
|
e17e4566cd | ||
|
|
d73ef57794 | ||
|
|
b09071ebc7 | ||
|
|
66bb56047a | ||
|
|
3054d84ba7 | ||
|
|
a71078cbd5 | ||
|
|
4fd6a0d5b6 | ||
|
|
fface15a22 | ||
|
|
6ad3edb6c8 |
@@ -13,9 +13,9 @@
|
||||
·
|
||||
<a href="https://github.com/documenso/documenso/issues">Issues</a>
|
||||
·
|
||||
<a href="https://github.com/documenso/documenso/milestones">Roadmap</a>
|
||||
<a href="https://documen.so/live">Upcoming Releases</a>
|
||||
·
|
||||
<a href="https://documen.so/launches">Upcoming Launches</a>
|
||||
<a href="https://documen.so/roadmap">Roadmap</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Announcing Pre-Seed and Open Metrics
|
||||
description: We are exicited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso.
|
||||
description: We are excited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
|
||||
@@ -30,7 +30,7 @@ We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania)
|
||||
|
||||
## Documenso Merch Shop
|
||||
|
||||
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso.
|
||||
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contributing to Documenso.
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes, KeyboardEvent } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { HTMLAttributes, KeyboardEvent } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@@ -355,6 +355,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 flex cursor-auto items-center justify-between px-4 pb-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Input
|
||||
id="signatureText"
|
||||
@@ -392,10 +393,11 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
</DialogHeader>
|
||||
|
||||
<DialogDescription>
|
||||
By signing you signal your support of Documenso's mission in a <br></br>
|
||||
<strong>non-legally binding, but heartfelt way</strong>. <br></br>
|
||||
<br></br>You also unlock the option to purchase the early supporter plan including
|
||||
everything we build this year for fixed price.
|
||||
By signing you signal your support of Documenso's mission in a <br />
|
||||
<strong>non-legally binding, but heartfelt way</strong>. <br />
|
||||
<br />
|
||||
You also unlock the option to purchase the early supporter plan including everything we
|
||||
build this year for fixed price.
|
||||
</DialogDescription>
|
||||
|
||||
<SignaturePad
|
||||
|
||||
@@ -17,6 +17,10 @@ const FONT_CAVEAT_BYTES = fs.readFileSync(
|
||||
path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
|
||||
);
|
||||
|
||||
const FONT_DANCING_SCRIPT_BYTES = fs.readFileSync(
|
||||
path.join(__dirname, '../../packages/assets/fonts/dancing-script.ttf'),
|
||||
);
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
|
||||
@@ -40,6 +44,7 @@ const config = {
|
||||
APP_VERSION: version,
|
||||
NEXT_PUBLIC_PROJECT: 'web',
|
||||
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
|
||||
FONT_DANCING_SCRIPT_URI: `data:font/ttf;base64,${FONT_DANCING_SCRIPT_BYTES.toString('base64')}`,
|
||||
},
|
||||
modularizeImports: {
|
||||
'lucide-react': {
|
||||
|
||||
@@ -4,10 +4,18 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { useElementScaleSize } from '@documenso/lib/client-only/hooks/use-element-scale-size';
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import {
|
||||
DEFAULT_HANDWRITING_FONT_SIZE,
|
||||
MIN_HANDWRITING_FONT_SIZE,
|
||||
} from '@documenso/lib/constants/pdf';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { Document, Field, Recipient } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -28,7 +36,27 @@ export type SigningFormProps = {
|
||||
fields: Field[];
|
||||
};
|
||||
|
||||
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
||||
const ZSigningpadSchema = z.union([
|
||||
z.object({
|
||||
signatureDataUrl: z.string().min(1),
|
||||
signatureText: z.null().or(z.string().max(0)),
|
||||
}),
|
||||
z.object({
|
||||
signatureDataUrl: z.null().or(z.string().max(0)),
|
||||
signatureText: z.string().trim().min(1),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type TSigningpadSchema = z.infer<typeof ZSigningpadSchema>;
|
||||
|
||||
export const SigningForm = ({ document: _document, recipient, fields }: SigningFormProps) => {
|
||||
const fontVariable = '--font-signature';
|
||||
const minFontSize = MIN_HANDWRITING_FONT_SIZE;
|
||||
const maxFontSize = DEFAULT_HANDWRITING_FONT_SIZE;
|
||||
const fontVariableValue = getComputedStyle(document.documentElement).getPropertyValue(
|
||||
fontVariable,
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const analytics = useAnalytics();
|
||||
const { data: session } = useSession();
|
||||
@@ -41,14 +69,34 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
} = useForm();
|
||||
} = useForm<TSigningpadSchema>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
signatureDataUrl: signature || null,
|
||||
signatureText: '',
|
||||
},
|
||||
resolver: zodResolver(ZSigningpadSchema),
|
||||
});
|
||||
|
||||
const { height, width } = useFieldPageCoords(fields.find((field) => field.type === 'SIGNATURE')!);
|
||||
|
||||
const signatureDataUrl = watch('signatureDataUrl');
|
||||
const signatureText = watch('signatureText');
|
||||
|
||||
const uninsertedFields = useMemo(() => {
|
||||
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
|
||||
}, [fields]);
|
||||
|
||||
const fieldsValidated = () => {
|
||||
setValidateUninsertedFields(true);
|
||||
validateFieldsInserted(fields);
|
||||
};
|
||||
|
||||
const onFormSubmit = async () => {
|
||||
setValidateUninsertedFields(true);
|
||||
|
||||
@@ -60,18 +108,30 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
||||
|
||||
await completeDocumentWithToken({
|
||||
token: recipient.token,
|
||||
documentId: document.id,
|
||||
documentId: _document.id,
|
||||
});
|
||||
|
||||
analytics.capture('App: Recipient has completed signing', {
|
||||
signerId: recipient.id,
|
||||
documentId: document.id,
|
||||
documentId: _document.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
router.push(`/sign/${recipient.token}/complete`);
|
||||
};
|
||||
|
||||
const scalingFactor = useElementScaleSize(
|
||||
{
|
||||
height,
|
||||
width,
|
||||
},
|
||||
signatureText || '',
|
||||
maxFontSize,
|
||||
fontVariableValue,
|
||||
);
|
||||
|
||||
const fontSize = maxFontSize * scalingFactor;
|
||||
|
||||
return (
|
||||
<form
|
||||
className={cn(
|
||||
@@ -123,15 +183,79 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
||||
<div>
|
||||
<Label htmlFor="Signature">Signature</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
/>
|
||||
<Card id="signature" className="mt-4" degrees={-120} gradient>
|
||||
<CardContent role="button" className="relative cursor-pointer pt-6">
|
||||
<div className="flex h-44 max-w-[18rem] items-center justify-center pb-6">
|
||||
{!signatureText && (
|
||||
<SignaturePad
|
||||
className="h-44"
|
||||
defaultValue={signature ?? undefined}
|
||||
clearSignatureClassName="absolute -bottom-6 -right-2 z-10 cursor-pointer"
|
||||
undoSignatureClassName="absolute -top-32 -left-4 z-10 cursor-pointer"
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{signatureText && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: `clamp(${minFontSize}px, ${fontSize}px, ${maxFontSize}px)`,
|
||||
fontFamily: `var(${fontVariable})`,
|
||||
}}
|
||||
className={cn(
|
||||
'text-foreground font-signature max-w-[18rem] text-4xl font-semibold',
|
||||
)}
|
||||
>
|
||||
{signatureText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 flex cursor-auto items-end justify-between px-4 pb-1 pt-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Input
|
||||
id="signatureText"
|
||||
className="text-foreground placeholder:text-muted-foreground max-w-[15rem] border-0 border-none bg-transparent p-0 text-sm focus-visible:ring-transparent"
|
||||
placeholder="Draw or type your name here"
|
||||
disabled={isSubmitting || signature?.startsWith('data:')}
|
||||
{...register('signatureText', {
|
||||
onChange: (e) => {
|
||||
if (e.target.value !== '') {
|
||||
setValue('signatureDataUrl', null);
|
||||
}
|
||||
|
||||
setValue('signatureText', e.target.value);
|
||||
},
|
||||
|
||||
onBlur: (e) => {
|
||||
if (e.target.value === '') {
|
||||
return setValue('signatureText', '');
|
||||
}
|
||||
|
||||
setSignature(e.target.value.trimStart());
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{signatureText && (
|
||||
<div className="absolute bottom-3 right-4 z-10 cursor-pointer">
|
||||
<button
|
||||
type="button"
|
||||
className="focus-visible:ring-ring ring-offset-background text-muted-foreground rounded-full p-0 text-xs focus-visible:outline-none focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
setValue('signatureText', '');
|
||||
setValue('signatureDataUrl', null);
|
||||
}}
|
||||
>
|
||||
Clear Signature
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -152,8 +276,9 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
||||
<SignDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||
document={document}
|
||||
document={_document}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ export type SignDialogProps = {
|
||||
isSubmitting: boolean;
|
||||
document: Document;
|
||||
fields: Field[];
|
||||
fieldsValidated: () => void | Promise<void>;
|
||||
onSignatureComplete: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
@@ -22,6 +23,7 @@ export const SignDialog = ({
|
||||
isSubmitting,
|
||||
document,
|
||||
fields,
|
||||
fieldsValidated,
|
||||
onSignatureComplete,
|
||||
}: SignDialogProps) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
@@ -29,16 +31,16 @@ export const SignDialog = ({
|
||||
const isComplete = fields.every((field) => field.inserted);
|
||||
|
||||
return (
|
||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||
<Dialog open={showDialog && isComplete} onOpenChange={setShowDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="w-full"
|
||||
type="button"
|
||||
size="lg"
|
||||
disabled={!isComplete}
|
||||
onClick={fieldsValidated}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Complete
|
||||
{isComplete ? 'Complete' : 'Next field'}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
|
||||
@@ -86,7 +86,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
value,
|
||||
isBase64: true,
|
||||
isBase64: typeof value === 'string' && value.startsWith('data:image/png;base64,'),
|
||||
});
|
||||
|
||||
if (source === 'local' && !providedSignature) {
|
||||
|
||||
@@ -95,8 +95,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
const toggleOpen = (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
const toggleOpen = () => {
|
||||
setIsOpen((isOpen) => !isOpen);
|
||||
onOpenChange?.(!isOpen);
|
||||
|
||||
@@ -136,7 +135,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]);
|
||||
const goToTemplates = useCallback(() => push(TEMPLATES_PAGES[0].path), [push]);
|
||||
|
||||
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen);
|
||||
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen, { preventDefault: true });
|
||||
useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings);
|
||||
useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments);
|
||||
useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates);
|
||||
|
||||
BIN
packages/assets/fonts/dancing-script.ttf
Normal file
BIN
packages/assets/fonts/dancing-script.ttf
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
import { APP_BASE_URL } from './app';
|
||||
|
||||
export const DEFAULT_STANDARD_FONT_SIZE = 15;
|
||||
export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
|
||||
export const DEFAULT_HANDWRITING_FONT_SIZE = 30;
|
||||
|
||||
export const MIN_STANDARD_FONT_SIZE = 8;
|
||||
export const MIN_HANDWRITING_FONT_SIZE = 20;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-fiel
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
|
||||
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||
const fontCaveat = await fetch(process.env.FONT_CAVEAT_URI).then(async (res) =>
|
||||
const fontDancingScript = await fetch(process.env.FONT_DANCING_SCRIPT_URI).then(async (res) =>
|
||||
res.arrayBuffer(),
|
||||
);
|
||||
|
||||
@@ -40,14 +40,28 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
const fieldX = pageWidth * (Number(field.positionX) / 100);
|
||||
const fieldY = pageHeight * (Number(field.positionY) / 100);
|
||||
|
||||
const font = await pdf.embedFont(isSignatureField ? fontCaveat : StandardFonts.Helvetica);
|
||||
const font = await pdf.embedFont(isSignatureField ? fontDancingScript : StandardFonts.Helvetica, {
|
||||
subset: true,
|
||||
features: {
|
||||
liga: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
|
||||
await pdf.embedFont(fontCaveat);
|
||||
await pdf.embedFont(fontDancingScript, {
|
||||
subset: true,
|
||||
features: {
|
||||
liga: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const CUSTOM_TEXT = field.customText || field.Signature?.typedSignature || '';
|
||||
|
||||
const isInsertingImage =
|
||||
isSignatureField && typeof field.Signature?.signatureImageAsBase64 === 'string';
|
||||
isSignatureField &&
|
||||
typeof field.Signature?.signatureImageAsBase64 === 'string' &&
|
||||
field.Signature?.signatureImageAsBase64.startsWith('data:image/png;base64,');
|
||||
|
||||
if (isSignatureField && isInsertingImage) {
|
||||
const image = await pdf.embedPng(field.Signature?.signatureImageAsBase64 ?? '');
|
||||
@@ -73,13 +87,13 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
height: imageHeight,
|
||||
});
|
||||
} else {
|
||||
let textWidth = font.widthOfTextAtSize(field.customText, fontSize);
|
||||
let textWidth = font.widthOfTextAtSize(CUSTOM_TEXT, fontSize);
|
||||
const textHeight = font.heightAtSize(fontSize);
|
||||
|
||||
const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1);
|
||||
|
||||
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
|
||||
textWidth = font.widthOfTextAtSize(field.customText, fontSize);
|
||||
textWidth = font.widthOfTextAtSize(CUSTOM_TEXT, fontSize);
|
||||
|
||||
const textX = fieldX + (fieldWidth - textWidth) / 2;
|
||||
let textY = fieldY + (fieldHeight - textHeight) / 2;
|
||||
@@ -87,7 +101,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||
textY = pageHeight - textY - textHeight;
|
||||
|
||||
page.drawText(field.customText, {
|
||||
page.drawText(CUSTOM_TEXT, {
|
||||
x: textX,
|
||||
y: textY,
|
||||
size: fontSize,
|
||||
|
||||
@@ -238,7 +238,6 @@ export const documentRouter = router({
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We are unable to duplicate this document. Please try again later.',
|
||||
|
||||
1
packages/tsconfig/process-env.d.ts
vendored
1
packages/tsconfig/process-env.d.ts
vendored
@@ -64,6 +64,7 @@ declare namespace NodeJS {
|
||||
|
||||
DEPLOYMENT_TARGET?: 'webapp' | 'marketing';
|
||||
FONT_CAVEAT_URI: string;
|
||||
FONT_DANCING_SCRIPT_URI: string;
|
||||
|
||||
POSTGRES_URL?: string;
|
||||
DATABASE_URL?: string;
|
||||
|
||||
@@ -121,6 +121,7 @@ export const FieldItem = ({
|
||||
<button
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground/80 bg-background absolute -right-2 -top-2 z-20 flex h-8 w-8 items-center justify-center rounded-full border"
|
||||
onClick={() => onRemove?.()}
|
||||
onTouchEnd={() => onRemove?.()}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -16,12 +16,16 @@ const DPI = 2;
|
||||
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
|
||||
onChange?: (_signatureDataUrl: string | null) => void;
|
||||
containerClassName?: string;
|
||||
clearSignatureClassName?: string;
|
||||
undoSignatureClassName?: string;
|
||||
};
|
||||
|
||||
export const SignaturePad = ({
|
||||
className,
|
||||
containerClassName,
|
||||
defaultValue,
|
||||
clearSignatureClassName,
|
||||
undoSignatureClassName,
|
||||
onChange,
|
||||
...props
|
||||
}: SignaturePadProps) => {
|
||||
@@ -227,7 +231,7 @@ export const SignaturePad = ({
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||
<div className={cn('absolute bottom-4 right-4', clearSignatureClassName)}>
|
||||
<button
|
||||
type="button"
|
||||
className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground rounded-full p-0 text-xs focus-visible:outline-none focus-visible:ring-2"
|
||||
@@ -238,7 +242,7 @@ export const SignaturePad = ({
|
||||
</div>
|
||||
|
||||
{lines.length > 0 && (
|
||||
<div className="absolute bottom-4 left-4 flex gap-2">
|
||||
<div className={cn('absolute bottom-4 left-4 flex gap-2', undoSignatureClassName)}>
|
||||
<button
|
||||
type="button"
|
||||
title="undo"
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ThemeSwitcher = () => {
|
||||
>
|
||||
{isMounted && theme === THEMES_TYPE.LIGHT && (
|
||||
<motion.div
|
||||
className="bg-background absolute inset-0 rounded-full mix-blend-exclusion"
|
||||
className="bg-background absolute inset-0 rounded-full mix-blend-color-burn"
|
||||
layoutId="selected-theme"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"VERCEL_URL",
|
||||
"DEPLOYMENT_TARGET",
|
||||
"FONT_CAVEAT_URI",
|
||||
"FONT_DANCING_SCRIPT_URI",
|
||||
"POSTGRES_URL",
|
||||
"DATABASE_URL",
|
||||
"POSTGRES_PRISMA_URL",
|
||||
|
||||
Reference in New Issue
Block a user