Merge branch 'main' into experiment/self-sign

This commit is contained in:
Ephraim Duncan
2024-11-21 17:50:07 +00:00
committed by GitHub
43 changed files with 1051 additions and 443 deletions

View File

@ -0,0 +1,157 @@
'use client';
import { useLayoutEffect, useRef } from 'react';
import { cn } from '../lib/utils';
export type Dimensions = {
height: number;
width: number;
};
export type AutoSizedTextProps = {
children: React.ReactNode;
className?: string;
maxHeight?: number;
useRem?: boolean;
};
const ITERATION_LIMIT = 20;
const MAXIMUM_DIFFERENCE = 1; // px
function getElementDimensions(element: HTMLElement): Dimensions {
const bbox = element.getBoundingClientRect();
return {
width: bbox.width,
height: bbox.height,
};
}
function getBaseFontSize(): number {
try {
const fontSize = getComputedStyle(document.documentElement).fontSize;
const parsed = parseFloat(fontSize);
// Check if we got a valid number
if (!Number.isFinite(parsed)) {
return 16;
}
return parsed;
} catch (error) {
// Fallback to browser default if anything goes wrong
return 16;
}
}
function pxToRem(px: number): number {
return px / getBaseFontSize();
}
export function AutoSizedText({
children,
className,
maxHeight,
useRem = false,
}: AutoSizedTextProps) {
const childRef = useRef<HTMLDivElement>(null);
const fontSize = useRef<number>(0);
const fontSizeLowerBound = useRef<number>(0);
const fontSizeUpperBound = useRef<number>(0);
const adjustFontSize = (childDimensions: Dimensions, parentDimensions: Dimensions) => {
const childElement = childRef.current;
if (!childElement) {
return;
}
let newFontSize: number;
const targetHeight =
maxHeight && maxHeight < parentDimensions.height ? maxHeight : parentDimensions.height;
const isElementTooBig =
childDimensions.width > parentDimensions.width || childDimensions.height > targetHeight;
if (isElementTooBig) {
// Scale down if element is bigger than target
newFontSize = (fontSizeLowerBound.current + fontSize.current) / 2;
fontSizeUpperBound.current = fontSize.current;
} else if (
childDimensions.width < parentDimensions.width ||
childDimensions.height < parentDimensions.height
) {
// Scale up if element is smaller than target
newFontSize = (fontSizeUpperBound.current + fontSize.current) / 2;
fontSizeLowerBound.current = fontSize.current;
}
fontSize.current = newFontSize;
// Convert to rem if useRem is true
const displayFontSize = useRem ? `${pxToRem(newFontSize)}rem` : `${newFontSize}px`;
childElement.style.fontSize = displayFontSize;
};
useLayoutEffect(() => {
const childElement = childRef.current;
const parentElement = childRef.current?.parentElement;
if (!childElement || !parentElement) {
return;
}
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry) {
return;
}
const parentDimensions = entry.contentRect;
// Reset iteration parameters
fontSizeLowerBound.current = 0;
fontSizeUpperBound.current = parentDimensions.height;
let iterationCount = 0;
while (iterationCount <= ITERATION_LIMIT) {
const childDimensions = getElementDimensions(childElement);
const targetHeight =
maxHeight && maxHeight < parentDimensions.height ? maxHeight : parentDimensions.height;
const widthDifference = parentDimensions.width - childDimensions.width;
const heightDifference = targetHeight - childDimensions.height;
const childFitsIntoParent = heightDifference >= 0 && widthDifference >= 0;
const isWithinTolerance =
Math.abs(widthDifference) <= MAXIMUM_DIFFERENCE ||
Math.abs(heightDifference) <= MAXIMUM_DIFFERENCE;
if (childFitsIntoParent && isWithinTolerance) {
break;
}
adjustFontSize(childDimensions, parentDimensions);
iterationCount += 1;
}
});
observer.observe(parentElement);
return () => {
observer.disconnect();
};
}, [maxHeight, useRem]);
return (
<div ref={childRef} className={cn('inline-block leading-none', className)}>
{children}
</div>
);
}

View File

@ -113,7 +113,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('truncate text-lg font-semibold leading-none tracking-tight', className)}
className={cn('truncate text-lg font-semibold tracking-tight', className)}
{...props}
/>
));

View File

@ -77,8 +77,11 @@ const fontCaveat = Caveat({
variable: '--font-caveat',
});
const MIN_HEIGHT_PX = 20;
const MIN_WIDTH_PX = 80;
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
const DEFAULT_HEIGHT_PX = MIN_HEIGHT_PX * 2.5;
const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
export type FieldFormType = {
nativeId?: number;
@ -482,8 +485,8 @@ export const AddFieldsFormPartial = ({
}
fieldBounds.current = {
height: Math.max(MIN_HEIGHT_PX),
width: Math.max(MIN_WIDTH_PX),
height: Math.max(DEFAULT_HEIGHT_PX),
width: Math.max(DEFAULT_WIDTH_PX),
};
});
@ -600,7 +603,7 @@ export const AddFieldsFormPartial = ({
{selectedField && (
<div
className={cn(
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]',
selectedSignerStyles.default.base,
{
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
@ -615,7 +618,9 @@ export const AddFieldsFormPartial = ({
width: fieldBounds.current.width,
}}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
<span className="text-[clamp(0.425rem,25cqw,0.825rem)]">
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
</span>
</div>
)}
@ -636,8 +641,10 @@ export const AddFieldsFormPartial = ({
selectedSigner?.email !== field.signerEmail ||
!canRecipientBeModified(selectedSigner, fields)
}
minHeight={fieldBounds.current.height}
minWidth={fieldBounds.current.width}
minHeight={MIN_HEIGHT_PX}
minWidth={MIN_WIDTH_PX}
defaultHeight={DEFAULT_HEIGHT_PX}
defaultWidth={DEFAULT_WIDTH_PX}
passive={isFieldWithinBounds && !!selectedField}
onFocus={() => setLastActiveField(field)}
onBlur={() => setLastActiveField(null)}

View File

@ -45,7 +45,7 @@ export const FieldIcon = ({
return (
<div
className={cn(
'text-field-card-foreground flex items-center justify-center gap-x-1 text-[clamp(0.875rem,1.8cqw,1.2rem)]',
'text-field-card-foreground flex items-center justify-center gap-x-1 text-[clamp(0.575rem,25cqw,1.2rem)]',
fontCaveatClassName,
)}
>
@ -71,8 +71,9 @@ export const FieldIcon = ({
}
return (
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-[clamp(0.625rem,1cqw,0.825rem)]">
<Icon className="h-4 w-4" /> {label}
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-[clamp(0.425rem,25cqw,0.825rem)]">
<Icon className="h-[clamp(0.625rem,20cqw,0.925rem)] w-[clamp(0.625rem,20cqw,0.925rem)]" />{' '}
{label}
</div>
);
}

View File

@ -35,6 +35,8 @@ export type FieldItemProps = {
disabled?: boolean;
minHeight?: number;
minWidth?: number;
defaultHeight?: number;
defaultWidth?: number;
onResize?: (_node: HTMLElement) => void;
onMove?: (_node: HTMLElement) => void;
onRemove?: () => void;
@ -53,6 +55,8 @@ export const FieldItem = ({
disabled,
minHeight,
minWidth,
defaultHeight,
defaultWidth,
onResize,
onMove,
onRemove,
@ -68,8 +72,8 @@ export const FieldItem = ({
const [coords, setCoords] = useState({
pageX: 0,
pageY: 0,
pageHeight: 0,
pageWidth: 0,
pageHeight: defaultHeight || 0,
pageWidth: defaultWidth || 0,
});
const [settingsActive, setSettingsActive] = useState(false);
const $el = useRef(null);

View File

@ -44,18 +44,18 @@ export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
width: `${coords.width}px`,
}}
>
<Card className={cn('bg-background h-full w-full')}>
<Card className={cn('bg-background h-full w-full [container-type:size]')}>
<CardContent
className={cn(
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-2 text-xl',
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-0 text-[clamp(0.575rem,1.8cqw,1.2rem)] leading-none',
field.type === FieldType.SIGNATURE && fontCaveat.className,
)}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
<p className="text-muted-foreground/50 w-full truncate text-center text-xs">
{/* <p className="text-muted-foreground/50 w-full truncate text-center text-xs">
{signerEmail}
</p>
</p> */}
</CardContent>
</Card>
</div>,

View File

@ -45,7 +45,7 @@ export const ZDocumentFlowFormSchema = z.object({
export type TDocumentFlowFormSchema = z.infer<typeof ZDocumentFlowFormSchema>;
export const FRIENDLY_FIELD_TYPE: Record<FieldType, MessageDescriptor | string> = {
export const FRIENDLY_FIELD_TYPE: Record<FieldType, MessageDescriptor> = {
[FieldType.SIGNATURE]: msg`Signature`,
[FieldType.FREE_SIGNATURE]: msg`Free Signature`,
[FieldType.INITIALS]: msg`Initials`,
@ -54,9 +54,9 @@ export const FRIENDLY_FIELD_TYPE: Record<FieldType, MessageDescriptor | string>
[FieldType.EMAIL]: msg`Email`,
[FieldType.NAME]: msg`Name`,
[FieldType.NUMBER]: msg`Number`,
[FieldType.RADIO]: `Radio`,
[FieldType.CHECKBOX]: `Checkbox`,
[FieldType.DROPDOWN]: `Select`,
[FieldType.RADIO]: msg`Radio`,
[FieldType.CHECKBOX]: msg`Checkbox`,
[FieldType.DROPDOWN]: msg`Select`,
};
export interface DocumentFlowStep {

View File

@ -67,8 +67,11 @@ const fontCaveat = Caveat({
variable: '--font-caveat',
});
const MIN_HEIGHT_PX = 20;
const MIN_WIDTH_PX = 80;
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
const DEFAULT_HEIGHT_PX = MIN_HEIGHT_PX * 2.5;
const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
export type AddTemplateFieldsFormProps = {
documentFlow: DocumentFlowStep;
@ -354,8 +357,8 @@ export const AddTemplateFieldsFormPartial = ({
}
fieldBounds.current = {
height: Math.max(MIN_HEIGHT_PX),
width: Math.max(MIN_WIDTH_PX),
height: Math.max(DEFAULT_HEIGHT_PX),
width: Math.max(DEFAULT_WIDTH_PX),
};
});
@ -425,7 +428,7 @@ export const AddTemplateFieldsFormPartial = ({
{selectedField && (
<div
className={cn(
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]',
selectedSignerStyles.default.base,
{
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
@ -439,7 +442,9 @@ export const AddTemplateFieldsFormPartial = ({
width: fieldBounds.current.width,
}}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
<span className="text-[clamp(0.425rem,25cqw,0.825rem)]">
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
</span>
</div>
)}
@ -452,8 +457,10 @@ export const AddTemplateFieldsFormPartial = ({
recipientIndex={recipientIndex === -1 ? 0 : recipientIndex}
field={field}
disabled={selectedSigner?.email !== field.signerEmail}
minHeight={fieldBounds.current.height}
minWidth={fieldBounds.current.width}
minHeight={MIN_HEIGHT_PX}
minWidth={MIN_WIDTH_PX}
defaultHeight={DEFAULT_HEIGHT_PX}
defaultWidth={DEFAULT_WIDTH_PX}
passive={isFieldWithinBounds && !!selectedField}
onResize={(options) => onFieldResize(options, index)}
onMove={(options) => onFieldMove(options, index)}

View File

@ -1,6 +1,6 @@
'use client';
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import type { DropResult, SensorAPI } from '@hello-pangea/dnd';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
@ -94,7 +94,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
];
}
return recipients.map((recipient, index) => ({
let mappedRecipients = recipients.map((recipient, index) => ({
nativeId: recipient.id,
formId: String(recipient.id),
name: recipient.name,
@ -103,6 +103,14 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
actionAuth: ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? undefined,
signingOrder: recipient.signingOrder ?? index + 1,
}));
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
mappedRecipients = mappedRecipients.sort(
(a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0),
);
}
return mappedRecipients;
};
const form = useForm<TAddTemplatePlacholderRecipientsFormSchema>({