diff --git a/apps/remix/app/components/general/document-signing/document-signing-date-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-date-field.tsx index 823fe3740..255c07849 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-date-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-date-field.tsx @@ -13,12 +13,14 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones' import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; +import { ZDateFieldMeta } from '@documenso/lib/types/field-meta'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import type { TRemovedSignedFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema, } from '@documenso/trpc/server/field-router/schema'; +import { cn } from '@documenso/ui/lib/utils'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { DocumentSigningFieldContainer } from './document-signing-field-container'; @@ -54,6 +56,9 @@ export const DocumentSigningDateField = ({ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading; + const safeFieldMeta = ZDateFieldMeta.safeParse(field.fieldMeta); + const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null; + const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone); const isDifferentTime = field.inserted && localDateString !== field.customText; @@ -143,9 +148,21 @@ export const DocumentSigningDateField = ({ )} {field.inserted && ( -

- {localDateString} -

+
+

+ {localDateString} +

+
)} ); diff --git a/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx index 4f4b16d16..bf943f178 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx @@ -8,12 +8,14 @@ import { useRevalidator } from 'react-router'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; +import { ZEmailFieldMeta } from '@documenso/lib/types/field-meta'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import type { TRemovedSignedFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema, } from '@documenso/trpc/server/field-router/schema'; +import { cn } from '@documenso/ui/lib/utils'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { DocumentSigningFieldContainer } from './document-signing-field-container'; @@ -48,6 +50,9 @@ export const DocumentSigningEmailField = ({ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading; + const safeFieldMeta = ZEmailFieldMeta.safeParse(field.fieldMeta); + const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null; + const onSign = async (authOptions?: TRecipientActionAuth) => { try { const value = providedEmail ?? ''; @@ -126,9 +131,21 @@ export const DocumentSigningEmailField = ({ )} {field.inserted && ( -

- {field.customText} -

+
+

+ {field.customText} +

+
)} ); diff --git a/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx index 3a97c4dca..5fea0f1aa 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx @@ -10,12 +10,14 @@ import { useRevalidator } from 'react-router'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; +import { ZNameFieldMeta } from '@documenso/lib/types/field-meta'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import type { TRemovedSignedFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema, } from '@documenso/trpc/server/field-router/schema'; +import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog'; import { Input } from '@documenso/ui/primitives/input'; @@ -58,6 +60,9 @@ export const DocumentSigningNameField = ({ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading; + const safeFieldMeta = ZNameFieldMeta.safeParse(field.fieldMeta); + const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null; + const [showFullNameModal, setShowFullNameModal] = useState(false); const [localFullName, setLocalFullName] = useState(''); @@ -172,9 +177,21 @@ export const DocumentSigningNameField = ({ )} {field.inserted && ( -

- {field.customText} -

+
+

+ {field.customText} +

+
)} diff --git a/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx index 78b474e29..8d293599e 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx @@ -54,8 +54,9 @@ export const DocumentSigningNumberField = ({ const [showRadioModal, setShowRadioModal] = useState(false); - const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null; - const isReadOnly = parsedFieldMeta?.readOnly; + const safeFieldMeta = ZNumberFieldMeta.safeParse(field.fieldMeta); + const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null; + const defaultValue = parsedFieldMeta?.value; const [localNumber, setLocalNumber] = useState( parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0', @@ -210,7 +211,7 @@ export const DocumentSigningNumberField = ({ useEffect(() => { if ( (!field.inserted && defaultValue && localNumber) || - (!field.inserted && isReadOnly && defaultValue) + (!field.inserted && parsedFieldMeta?.readOnly && defaultValue) ) { void executeActionAuthProcedure({ onReauthFormSubmit: async (authOptions) => await onSign(authOptions), @@ -262,9 +263,21 @@ export const DocumentSigningNumberField = ({ )} {field.inserted && ( -

- {field.customText} -

+
+

+ {field.customText} +

+
)} diff --git a/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx index 466bcef1d..a1aa569fd 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx @@ -62,7 +62,8 @@ export const DocumentSigningTextField = ({ isPending: isRemoveSignedFieldWithTokenLoading, } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); - const parsedFieldMeta = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null; + const safeFieldMeta = ZTextFieldMeta.safeParse(field.fieldMeta); + const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading; const shouldAutoSignField = @@ -261,11 +262,23 @@ export const DocumentSigningTextField = ({ )} {field.inserted && ( -

- {field.customText.length < 20 - ? field.customText - : field.customText.substring(0, 15) + '...'} -

+
+

+ {field.customText.length < 20 + ? field.customText + : field.customText.substring(0, 15) + '...'} +

+
)} @@ -281,6 +294,10 @@ export const DocumentSigningTextField = ({ className={cn('mt-2 w-full rounded-md', { 'border-2 border-red-300 ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200': userInputHasErrors, + 'text-left': parsedFieldMeta?.textAlign === 'left', + 'text-center': + !parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center', + 'text-right': parsedFieldMeta?.textAlign === 'right', })} value={localText} onChange={handleTextChange} diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts index c81372179..403c42559 100644 --- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts +++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts @@ -2,7 +2,7 @@ import fontkit from '@pdf-lib/fontkit'; import { FieldType } from '@prisma/client'; import type { PDFDocument } from 'pdf-lib'; -import { RotationTypes, degrees, radiansToDegrees } from 'pdf-lib'; +import { RotationTypes, degrees, radiansToDegrees, rgb } from 'pdf-lib'; import { P, match } from 'ts-pattern'; import { @@ -34,6 +34,9 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu ]); const isSignatureField = isSignatureFieldType(field.type); + const isDebugMode = + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true'; pdf.registerFontkit(fontkit); @@ -81,6 +84,35 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu const fieldX = pageWidth * (Number(field.positionX) / 100); const fieldY = pageHeight * (Number(field.positionY) / 100); + // Draw debug box if debug mode is enabled + if (isDebugMode) { + let debugX = fieldX; + let debugY = pageHeight - fieldY - fieldHeight; // Invert Y for PDF coordinates + + if (pageRotationInDegrees !== 0) { + const adjustedPosition = adjustPositionForRotation( + pageWidth, + pageHeight, + debugX, + debugY, + pageRotationInDegrees, + ); + + debugX = adjustedPosition.xPos; + debugY = adjustedPosition.yPos; + } + + page.drawRectangle({ + x: debugX, + y: debugY, + width: fieldWidth, + height: fieldHeight, + borderColor: rgb(1, 0, 0), // Red + borderWidth: 1, + rotate: degrees(pageRotationInDegrees), + }); + } + const font = await pdf.embedFont( isSignatureField ? fontCaveat : fontNoto, isSignatureField ? { features: { calt: false } } : undefined, @@ -276,6 +308,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu const meta = Parser ? Parser.safeParse(field.fieldMeta) : null; const customFontSize = meta?.success && meta.data.fontSize ? meta.data.fontSize : null; + const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'center'; const longestLineInTextForWidth = field.customText .split('\n') .sort((a, b) => b.length - a.length)[0]; @@ -291,7 +324,17 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize); - let textX = fieldX + (fieldWidth - textWidth) / 2; + // Add padding similar to web display (roughly 0.5rem equivalent in PDF units) + const padding = 8; // PDF points, roughly equivalent to 0.5rem + + // Calculate X position based on text alignment with padding + let textX = fieldX + padding; // Left alignment starts after padding + if (textAlign === 'center') { + textX = fieldX + (fieldWidth - textWidth) / 2; // Center alignment ignores padding + } else if (textAlign === 'right') { + textX = fieldX + fieldWidth - textWidth - padding; // Right alignment respects right padding + } + let textY = fieldY + (fieldHeight - textHeight) / 2; // Invert the Y axis since PDFs use a bottom-left coordinate system diff --git a/packages/lib/types/field-meta.ts b/packages/lib/types/field-meta.ts index 2d926f6a1..83cca0ef8 100644 --- a/packages/lib/types/field-meta.ts +++ b/packages/lib/types/field-meta.ts @@ -10,9 +10,14 @@ export const ZBaseFieldMeta = z.object({ export type TBaseFieldMeta = z.infer; +export const ZFieldTextAlignSchema = z.enum(['left', 'center', 'right']); + +export type TFieldTextAlignSchema = z.infer; + export const ZInitialsFieldMeta = ZBaseFieldMeta.extend({ type: z.literal('initials'), fontSize: z.number().min(8).max(96).optional(), + textAlign: ZFieldTextAlignSchema.optional(), }); export type TInitialsFieldMeta = z.infer; @@ -20,6 +25,7 @@ export type TInitialsFieldMeta = z.infer; export const ZNameFieldMeta = ZBaseFieldMeta.extend({ type: z.literal('name'), fontSize: z.number().min(8).max(96).optional(), + textAlign: ZFieldTextAlignSchema.optional(), }); export type TNameFieldMeta = z.infer; @@ -27,6 +33,7 @@ export type TNameFieldMeta = z.infer; export const ZEmailFieldMeta = ZBaseFieldMeta.extend({ type: z.literal('email'), fontSize: z.number().min(8).max(96).optional(), + textAlign: ZFieldTextAlignSchema.optional(), }); export type TEmailFieldMeta = z.infer; @@ -34,6 +41,7 @@ export type TEmailFieldMeta = z.infer; export const ZDateFieldMeta = ZBaseFieldMeta.extend({ type: z.literal('date'), fontSize: z.number().min(8).max(96).optional(), + textAlign: ZFieldTextAlignSchema.optional(), }); export type TDateFieldMeta = z.infer; @@ -43,6 +51,7 @@ export const ZTextFieldMeta = ZBaseFieldMeta.extend({ text: z.string().optional(), characterLimit: z.number().optional(), fontSize: z.number().min(8).max(96).optional(), + textAlign: ZFieldTextAlignSchema.optional(), }); export type TTextFieldMeta = z.infer; @@ -54,6 +63,7 @@ export const ZNumberFieldMeta = ZBaseFieldMeta.extend({ minValue: z.number().optional(), maxValue: z.number().optional(), fontSize: z.number().min(8).max(96).optional(), + textAlign: ZFieldTextAlignSchema.optional(), }); export type TNumberFieldMeta = z.infer; diff --git a/packages/ui/primitives/document-flow/field-item-advanced-settings.tsx b/packages/ui/primitives/document-flow/field-item-advanced-settings.tsx index 6a79da3ab..ea6097f73 100644 --- a/packages/ui/primitives/document-flow/field-item-advanced-settings.tsx +++ b/packages/ui/primitives/document-flow/field-item-advanced-settings.tsx @@ -69,21 +69,25 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => { return { type: 'initials', fontSize: 14, + textAlign: 'left', }; case FieldType.NAME: return { type: 'name', fontSize: 14, + textAlign: 'left', }; case FieldType.EMAIL: return { type: 'email', fontSize: 14, + textAlign: 'left', }; case FieldType.DATE: return { type: 'date', fontSize: 14, + textAlign: 'left', }; case FieldType.TEXT: return { @@ -95,6 +99,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => { fontSize: 14, required: false, readOnly: false, + textAlign: 'left', }; case FieldType.NUMBER: return { @@ -108,6 +113,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => { required: false, readOnly: false, fontSize: 14, + textAlign: 'left', }; case FieldType.RADIO: return { diff --git a/packages/ui/primitives/document-flow/field-items-advanced-settings/date-field.tsx b/packages/ui/primitives/document-flow/field-items-advanced-settings/date-field.tsx index b979d09d7..8f182c5a2 100644 --- a/packages/ui/primitives/document-flow/field-items-advanced-settings/date-field.tsx +++ b/packages/ui/primitives/document-flow/field-items-advanced-settings/date-field.tsx @@ -6,6 +6,13 @@ import { validateFields as validateDateFields } from '@documenso/lib/advanced-fi import { type TDateFieldMeta as DateFieldMeta } from '@documenso/lib/types/field-meta'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; type DateFieldAdvancedSettingsProps = { fieldState: DateFieldMeta; @@ -67,6 +74,27 @@ export const DateFieldAdvancedSettings = ({ max={96} /> + +
+ + + +
); }; diff --git a/packages/ui/primitives/document-flow/field-items-advanced-settings/email-field.tsx b/packages/ui/primitives/document-flow/field-items-advanced-settings/email-field.tsx index 4eb944265..dc92ffdd3 100644 --- a/packages/ui/primitives/document-flow/field-items-advanced-settings/email-field.tsx +++ b/packages/ui/primitives/document-flow/field-items-advanced-settings/email-field.tsx @@ -6,6 +6,13 @@ import { validateFields as validateEmailFields } from '@documenso/lib/advanced-f import { type TEmailFieldMeta as EmailFieldMeta } from '@documenso/lib/types/field-meta'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; type EmailFieldAdvancedSettingsProps = { fieldState: EmailFieldMeta; @@ -49,6 +56,27 @@ export const EmailFieldAdvancedSettings = ({ max={96} /> + +
+ + + +
); }; diff --git a/packages/ui/primitives/document-flow/field-items-advanced-settings/initials-field.tsx b/packages/ui/primitives/document-flow/field-items-advanced-settings/initials-field.tsx index ac9290b36..fba5f22fe 100644 --- a/packages/ui/primitives/document-flow/field-items-advanced-settings/initials-field.tsx +++ b/packages/ui/primitives/document-flow/field-items-advanced-settings/initials-field.tsx @@ -7,6 +7,8 @@ import { type TInitialsFieldMeta as InitialsFieldMeta } from '@documenso/lib/typ import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../select'; + type InitialsFieldAdvancedSettingsProps = { fieldState: InitialsFieldMeta; handleFieldChange: (key: keyof InitialsFieldMeta, value: string | boolean) => void; @@ -49,6 +51,27 @@ export const InitialsFieldAdvancedSettings = ({ max={96} /> + +
+ + + +
); }; diff --git a/packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx b/packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx index f7ffc00f0..7acaa6d7f 100644 --- a/packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx +++ b/packages/ui/primitives/document-flow/field-items-advanced-settings/name-field.tsx @@ -6,6 +6,13 @@ import { validateFields as validateNameFields } from '@documenso/lib/advanced-fi import { type TNameFieldMeta as NameFieldMeta } from '@documenso/lib/types/field-meta'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; type NameFieldAdvancedSettingsProps = { fieldState: NameFieldMeta; @@ -49,6 +56,27 @@ export const NameFieldAdvancedSettings = ({ max={96} /> + +
+ + + +
); }; diff --git a/packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx b/packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx index 9043c16d8..998a73442 100644 --- a/packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx +++ b/packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx @@ -134,6 +134,27 @@ export const NumberFieldAdvancedSettings = ({ /> +
+ + + +
+
+
+ + + +
+