mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 07:43:16 +10:00
## Description Adds support for customising the theme and CSS for the embedding components which is restricted to platform customers and above. Additionally adds proper support for the platform plan which will let us update our stripe products. <img width="1040" alt="image" src="https://github.com/user-attachments/assets/f694cd1e-ac93-4dc0-9f78-92fa813f6404"> <img width="1015" alt="image" src="https://github.com/user-attachments/assets/4209972a-b2bd-40c9-9049-0367382a4de5"> <img width="1065" alt="image" src="https://github.com/user-attachments/assets/fdbaaaa5-a028-4b1d-a58a-ea6224e21abe"> ## Related Issue N/A ## Changes Made - Added support for using CSS Vars and CSS within the embedding route - Added a guard for platform and enterprise plans to activate the custom css - Added support for the platform plan ## Testing Performed Yes
154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
|
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
|
import type { Field } from '@documenso/prisma/client';
|
|
|
|
import { cn } from '../../lib/utils';
|
|
import { Card, CardContent } from '../../primitives/card';
|
|
|
|
export type FieldRootContainerProps = {
|
|
field: Field;
|
|
children: React.ReactNode;
|
|
};
|
|
|
|
export type FieldContainerPortalProps = {
|
|
field: Field;
|
|
className?: string;
|
|
children: React.ReactNode;
|
|
cardClassName?: string;
|
|
};
|
|
|
|
const getCardClassNames = (
|
|
field: Field,
|
|
parsedField: TFieldMetaSchema | null,
|
|
isValidating: boolean,
|
|
checkBoxOrRadio: boolean,
|
|
cardClassName?: string,
|
|
) => {
|
|
const baseClasses = 'field-card-container relative z-20 h-full w-full transition-all';
|
|
|
|
const insertedClasses =
|
|
'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
|
|
const nonRequiredClasses =
|
|
'border-yellow-300 shadow-none ring-2 ring-yellow-100 ring-offset-2 ring-offset-yellow-100 dark:border-2';
|
|
const validatingClasses = 'border-orange-300 ring-1 ring-orange-300';
|
|
const requiredClasses =
|
|
'border-red-500 shadow-none ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 hover:text-red-500';
|
|
const requiredCheckboxRadioClasses = 'border-dashed border-red-500';
|
|
|
|
if (checkBoxOrRadio) {
|
|
return cn(
|
|
{
|
|
[insertedClasses]: field.inserted,
|
|
'ring-offset-yellow-200 border-dashed border-yellow-300 ring-2 ring-yellow-200 ring-offset-2 dark:shadow-none':
|
|
!field.inserted && !parsedField?.required,
|
|
'shadow-none': !field.inserted,
|
|
[validatingClasses]: !field.inserted && isValidating,
|
|
[requiredCheckboxRadioClasses]: !field.inserted && parsedField?.required,
|
|
},
|
|
cardClassName,
|
|
);
|
|
}
|
|
|
|
return cn(
|
|
baseClasses,
|
|
{
|
|
[insertedClasses]: field.inserted,
|
|
[nonRequiredClasses]: !field.inserted && !parsedField?.required,
|
|
'shadow-none': !field.inserted && checkBoxOrRadio,
|
|
[validatingClasses]: !field.inserted && isValidating,
|
|
[requiredClasses]: !field.inserted && parsedField?.required && !checkBoxOrRadio,
|
|
},
|
|
cardClassName,
|
|
);
|
|
};
|
|
|
|
export function FieldContainerPortal({
|
|
field,
|
|
children,
|
|
className = '',
|
|
}: FieldContainerPortalProps) {
|
|
const coords = useFieldPageCoords(field);
|
|
|
|
const isCheckboxOrRadioField = field.type === 'CHECKBOX' || field.type === 'RADIO';
|
|
const isFieldSigned = field.inserted;
|
|
|
|
const style = {
|
|
top: `${coords.y}px`,
|
|
left: `${coords.x}px`,
|
|
// height: `${coords.height}px`,
|
|
// width: `${coords.width}px`,
|
|
...(!isCheckboxOrRadioField && {
|
|
height: `${coords.height}px`,
|
|
width: `${coords.width}px`,
|
|
}),
|
|
};
|
|
|
|
return createPortal(
|
|
<div className={cn('absolute', className)} style={style}>
|
|
{children}
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|
|
|
|
export function FieldRootContainer({ field, children, cardClassName }: FieldContainerPortalProps) {
|
|
const [isValidating, setIsValidating] = useState(false);
|
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!ref.current) {
|
|
return;
|
|
}
|
|
|
|
const observer = new MutationObserver((_mutations) => {
|
|
if (ref.current) {
|
|
setIsValidating(ref.current.getAttribute('data-validate') === 'true');
|
|
}
|
|
});
|
|
|
|
observer.observe(ref.current, {
|
|
attributes: true,
|
|
});
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
const parsedField = useMemo(
|
|
() => (field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null),
|
|
[field.fieldMeta],
|
|
);
|
|
const isCheckboxOrRadio = useMemo(
|
|
() => parsedField?.type === 'checkbox' || parsedField?.type === 'radio',
|
|
[parsedField],
|
|
);
|
|
|
|
const cardClassNames = useMemo(
|
|
() => getCardClassNames(field, parsedField, isValidating, isCheckboxOrRadio, cardClassName),
|
|
[field, parsedField, isValidating, isCheckboxOrRadio, cardClassName],
|
|
);
|
|
|
|
return (
|
|
<FieldContainerPortal field={field}>
|
|
<Card
|
|
id={`field-${field.id}`}
|
|
ref={ref}
|
|
data-inserted={field.inserted ? 'true' : 'false'}
|
|
className={cardClassNames}
|
|
>
|
|
<CardContent className="text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2">
|
|
{children}
|
|
</CardContent>
|
|
</Card>
|
|
</FieldContainerPortal>
|
|
);
|
|
}
|