mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Compare commits
1 Commits
88371b665a
...
exp/keyboa
| Author | SHA1 | Date | |
|---|---|---|---|
| 357017314d |
@ -36,6 +36,7 @@ export type DocumentSigningSignatureFieldProps = {
|
|||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
uploadSignatureEnabled?: boolean;
|
uploadSignatureEnabled?: boolean;
|
||||||
drawSignatureEnabled?: boolean;
|
drawSignatureEnabled?: boolean;
|
||||||
|
keyboardSignatureEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningSignatureField = ({
|
export const DocumentSigningSignatureField = ({
|
||||||
@ -45,6 +46,7 @@ export const DocumentSigningSignatureField = ({
|
|||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
uploadSignatureEnabled,
|
uploadSignatureEnabled,
|
||||||
drawSignatureEnabled,
|
drawSignatureEnabled,
|
||||||
|
keyboardSignatureEnabled,
|
||||||
}: DocumentSigningSignatureFieldProps) => {
|
}: DocumentSigningSignatureFieldProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -283,6 +285,7 @@ export const DocumentSigningSignatureField = ({
|
|||||||
typedSignatureEnabled={typedSignatureEnabled}
|
typedSignatureEnabled={typedSignatureEnabled}
|
||||||
uploadSignatureEnabled={uploadSignatureEnabled}
|
uploadSignatureEnabled={uploadSignatureEnabled}
|
||||||
drawSignatureEnabled={drawSignatureEnabled}
|
drawSignatureEnabled={drawSignatureEnabled}
|
||||||
|
keyboardSignatureEnabled={keyboardSignatureEnabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentSigningDisclosure />
|
<DocumentSigningDisclosure />
|
||||||
|
|||||||
@ -69,4 +69,11 @@ export const DOCUMENT_SIGNATURE_TYPES = {
|
|||||||
}),
|
}),
|
||||||
value: DocumentSignatureType.UPLOAD,
|
value: DocumentSignatureType.UPLOAD,
|
||||||
},
|
},
|
||||||
|
[DocumentSignatureType.KEYBOARD]: {
|
||||||
|
label: msg({
|
||||||
|
message: `Keyboard`,
|
||||||
|
context: `Keyboard signatute type`,
|
||||||
|
}),
|
||||||
|
value: DocumentSignatureType.KEYBOARD,
|
||||||
|
},
|
||||||
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;
|
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export enum DocumentSignatureType {
|
|||||||
DRAW = 'draw',
|
DRAW = 'draw',
|
||||||
TYPE = 'type',
|
TYPE = 'type',
|
||||||
UPLOAD = 'upload',
|
UPLOAD = 'upload',
|
||||||
|
KEYBOARD = 'keyboard',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatTeamUrl = (teamUrl: string, baseUrl?: string) => {
|
export const formatTeamUrl = (teamUrl: string, baseUrl?: string) => {
|
||||||
@ -83,10 +84,16 @@ export const extractTeamSignatureSettings = (
|
|||||||
typedSignatureEnabled: boolean | null;
|
typedSignatureEnabled: boolean | null;
|
||||||
drawSignatureEnabled: boolean | null;
|
drawSignatureEnabled: boolean | null;
|
||||||
uploadSignatureEnabled: boolean | null;
|
uploadSignatureEnabled: boolean | null;
|
||||||
|
keyboardSignatureEnabled?: boolean | null;
|
||||||
} | null,
|
} | null,
|
||||||
) => {
|
) => {
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return [DocumentSignatureType.TYPE, DocumentSignatureType.UPLOAD, DocumentSignatureType.DRAW];
|
return [
|
||||||
|
DocumentSignatureType.TYPE,
|
||||||
|
DocumentSignatureType.UPLOAD,
|
||||||
|
DocumentSignatureType.DRAW,
|
||||||
|
DocumentSignatureType.KEYBOARD,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const signatureTypes: DocumentSignatureType[] = [];
|
const signatureTypes: DocumentSignatureType[] = [];
|
||||||
@ -103,6 +110,10 @@ export const extractTeamSignatureSettings = (
|
|||||||
signatureTypes.push(DocumentSignatureType.UPLOAD);
|
signatureTypes.push(DocumentSignatureType.UPLOAD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.keyboardSignatureEnabled !== false) {
|
||||||
|
signatureTypes.push(DocumentSignatureType.KEYBOARD);
|
||||||
|
}
|
||||||
|
|
||||||
return signatureTypes;
|
return signatureTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -175,6 +186,7 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
|
|||||||
typedSignatureEnabled: null,
|
typedSignatureEnabled: null,
|
||||||
uploadSignatureEnabled: null,
|
uploadSignatureEnabled: null,
|
||||||
drawSignatureEnabled: null,
|
drawSignatureEnabled: null,
|
||||||
|
keyboardSignatureEnabled: null,
|
||||||
|
|
||||||
brandingEnabled: null,
|
brandingEnabled: null,
|
||||||
brandingLogo: null,
|
brandingLogo: null,
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "keyboardSignatureEnabled" BOOLEAN;
|
||||||
@ -776,6 +776,7 @@ model TeamGlobalSettings {
|
|||||||
typedSignatureEnabled Boolean?
|
typedSignatureEnabled Boolean?
|
||||||
uploadSignatureEnabled Boolean?
|
uploadSignatureEnabled Boolean?
|
||||||
drawSignatureEnabled Boolean?
|
drawSignatureEnabled Boolean?
|
||||||
|
keyboardSignatureEnabled Boolean?
|
||||||
|
|
||||||
emailId String?
|
emailId String?
|
||||||
email OrganisationEmail? @relation(fields: [emailId], references: [id])
|
email OrganisationEmail? @relation(fields: [emailId], references: [id])
|
||||||
|
|||||||
207
packages/ui/primitives/signature-pad/keyboard-utils.ts
Normal file
207
packages/ui/primitives/signature-pad/keyboard-utils.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
export enum KeyboardLayout {
|
||||||
|
QWERTY = 'QWERTY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CurveType =
|
||||||
|
| 'linear'
|
||||||
|
| 'simple-curve'
|
||||||
|
| 'quadratic-bezier'
|
||||||
|
| 'cubic-bezier'
|
||||||
|
| 'catmull-rom';
|
||||||
|
|
||||||
|
export enum StrokeStyle {
|
||||||
|
SOLID = 'solid',
|
||||||
|
GRADIENT = 'gradient',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StrokeConfig {
|
||||||
|
style: StrokeStyle;
|
||||||
|
color: string;
|
||||||
|
gradientStart: string;
|
||||||
|
gradientEnd: string;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getKeyboardLayout = (
|
||||||
|
layout: KeyboardLayout,
|
||||||
|
includeNumbers: boolean = false,
|
||||||
|
): Record<string, Point> => {
|
||||||
|
const qwertyLayout: Record<string, Point> = {
|
||||||
|
Q: { x: 0, y: includeNumbers ? 1 : 0 },
|
||||||
|
W: { x: 1, y: includeNumbers ? 1 : 0 },
|
||||||
|
E: { x: 2, y: includeNumbers ? 1 : 0 },
|
||||||
|
R: { x: 3, y: includeNumbers ? 1 : 0 },
|
||||||
|
T: { x: 4, y: includeNumbers ? 1 : 0 },
|
||||||
|
Y: { x: 5, y: includeNumbers ? 1 : 0 },
|
||||||
|
U: { x: 6, y: includeNumbers ? 1 : 0 },
|
||||||
|
I: { x: 7, y: includeNumbers ? 1 : 0 },
|
||||||
|
O: { x: 8, y: includeNumbers ? 1 : 0 },
|
||||||
|
P: { x: 9, y: includeNumbers ? 1 : 0 },
|
||||||
|
|
||||||
|
A: { x: 0.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
S: { x: 1.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
D: { x: 2.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
F: { x: 3.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
G: { x: 4.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
H: { x: 5.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
J: { x: 6.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
K: { x: 7.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
L: { x: 8.5, y: includeNumbers ? 2 : 1 },
|
||||||
|
|
||||||
|
Z: { x: 1, y: includeNumbers ? 3 : 2 },
|
||||||
|
X: { x: 2, y: includeNumbers ? 3 : 2 },
|
||||||
|
C: { x: 3, y: includeNumbers ? 3 : 2 },
|
||||||
|
V: { x: 4, y: includeNumbers ? 3 : 2 },
|
||||||
|
B: { x: 5, y: includeNumbers ? 3 : 2 },
|
||||||
|
N: { x: 6, y: includeNumbers ? 3 : 2 },
|
||||||
|
M: { x: 7, y: includeNumbers ? 3 : 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeNumbers) {
|
||||||
|
const numberRow = {
|
||||||
|
'1': { x: 0, y: 0 },
|
||||||
|
'2': { x: 1, y: 0 },
|
||||||
|
'3': { x: 2, y: 0 },
|
||||||
|
'4': { x: 3, y: 0 },
|
||||||
|
'5': { x: 4, y: 0 },
|
||||||
|
'6': { x: 5, y: 0 },
|
||||||
|
'7': { x: 6, y: 0 },
|
||||||
|
'8': { x: 7, y: 0 },
|
||||||
|
'9': { x: 8, y: 0 },
|
||||||
|
'0': { x: 9, y: 0 },
|
||||||
|
};
|
||||||
|
return { ...numberRow, ...qwertyLayout };
|
||||||
|
}
|
||||||
|
|
||||||
|
return qwertyLayout;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatePath = (points: Point[], curveType: CurveType): string => {
|
||||||
|
if (points.length === 0) return '';
|
||||||
|
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
switch (curveType) {
|
||||||
|
case 'linear':
|
||||||
|
return generateLinearPath(points);
|
||||||
|
case 'simple-curve':
|
||||||
|
return generateSimpleCurvePath(points);
|
||||||
|
case 'quadratic-bezier':
|
||||||
|
return generateQuadraticBezierPath(points);
|
||||||
|
case 'cubic-bezier':
|
||||||
|
return generateCubicBezierPath(points);
|
||||||
|
case 'catmull-rom':
|
||||||
|
return generateCatmullRomPath(points);
|
||||||
|
default:
|
||||||
|
return generateLinearPath(points);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateLinearPath = (points: Point[]): string => {
|
||||||
|
if (points.length === 0) return '';
|
||||||
|
|
||||||
|
let path = `M ${points[0].x} ${points[0].y}`;
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
path += ` L ${points[i].x} ${points[i].y}`;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateSimpleCurvePath = (points: Point[]): string => {
|
||||||
|
if (points.length === 0) return '';
|
||||||
|
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
let path = `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
const prev = points[i - 1];
|
||||||
|
const curr = points[i];
|
||||||
|
|
||||||
|
const midX = (prev.x + curr.x) / 2;
|
||||||
|
const midY = (prev.y + curr.y) / 2;
|
||||||
|
|
||||||
|
if (i === 1) {
|
||||||
|
path += ` Q ${prev.x} ${prev.y} ${midX} ${midY}`;
|
||||||
|
} else {
|
||||||
|
path += ` T ${midX} ${midY}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPoint = points[points.length - 1];
|
||||||
|
path += ` T ${lastPoint.x} ${lastPoint.y}`;
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateQuadraticBezierPath = (points: Point[]): string => {
|
||||||
|
if (points.length === 0) return '';
|
||||||
|
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
let path = `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
const prev = points[i - 1];
|
||||||
|
const curr = points[i];
|
||||||
|
|
||||||
|
const controlX = prev.x + (curr.x - prev.x) * 0.5;
|
||||||
|
const controlY = prev.y - Math.abs(curr.x - prev.x) * 0.3;
|
||||||
|
|
||||||
|
path += ` Q ${controlX} ${controlY} ${curr.x} ${curr.y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateCubicBezierPath = (points: Point[]): string => {
|
||||||
|
if (points.length === 0) return '';
|
||||||
|
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
let path = `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
const prev = points[i - 1];
|
||||||
|
const curr = points[i];
|
||||||
|
|
||||||
|
const dx = curr.x - prev.x;
|
||||||
|
const dy = curr.y - prev.y;
|
||||||
|
|
||||||
|
const cp1x = prev.x + dx * 0.25;
|
||||||
|
const cp1y = prev.y + dy * 0.25 - Math.abs(dx) * 0.2;
|
||||||
|
|
||||||
|
const cp2x = prev.x + dx * 0.75;
|
||||||
|
const cp2y = prev.y + dy * 0.75 - Math.abs(dx) * 0.2;
|
||||||
|
|
||||||
|
path += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${curr.x} ${curr.y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateCatmullRomPath = (points: Point[]): string => {
|
||||||
|
if (points.length === 0) return '';
|
||||||
|
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||||
|
if (points.length === 2) return generateLinearPath(points);
|
||||||
|
|
||||||
|
let path = `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
const p0 = i === 1 ? points[0] : points[i - 2];
|
||||||
|
const p1 = points[i - 1];
|
||||||
|
const p2 = points[i];
|
||||||
|
const p3 = i === points.length - 1 ? points[i] : points[i + 1];
|
||||||
|
|
||||||
|
const cp1x = p1.x + (p2.x - p0.x) / 6;
|
||||||
|
const cp1y = p1.y + (p2.y - p0.y) / 6;
|
||||||
|
|
||||||
|
const cp2x = p2.x - (p3.x - p1.x) / 6;
|
||||||
|
const cp2y = p2.y - (p3.y - p1.y) / 6;
|
||||||
|
|
||||||
|
path += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${p2.x} ${p2.y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
146
packages/ui/primitives/signature-pad/signature-pad-keyboard.tsx
Normal file
146
packages/ui/primitives/signature-pad/signature-pad-keyboard.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
import { KeyboardLayout, StrokeStyle, generatePath, getKeyboardLayout } from './keyboard-utils';
|
||||||
|
|
||||||
|
export type SignaturePadKeyboardProps = {
|
||||||
|
className?: string;
|
||||||
|
onChange: (_value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SignaturePadKeyboard = ({ className, onChange }: SignaturePadKeyboardProps) => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [currentKeyboardLayout] = useState<KeyboardLayout>(KeyboardLayout.QWERTY);
|
||||||
|
|
||||||
|
const curveType = 'linear';
|
||||||
|
const includeNumbers = false;
|
||||||
|
const strokeConfig = {
|
||||||
|
style: StrokeStyle.SOLID,
|
||||||
|
color: '#000000',
|
||||||
|
gradientStart: '#ff6b6b',
|
||||||
|
gradientEnd: '#4ecdc4',
|
||||||
|
width: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const isInputFocused = document.activeElement === inputRef.current;
|
||||||
|
const isAnyInputFocused = document.activeElement?.tagName === 'INPUT';
|
||||||
|
|
||||||
|
if (!isInputFocused && !isAnyInputFocused) {
|
||||||
|
const regex = includeNumbers ? /^[a-zA-Z0-9]$/ : /^[a-zA-Z]$/;
|
||||||
|
if (regex.test(e.key) || e.key === 'Backspace') {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [includeNumbers]);
|
||||||
|
|
||||||
|
// Generate signature path
|
||||||
|
const signaturePath = useMemo(() => {
|
||||||
|
if (!name) return '';
|
||||||
|
|
||||||
|
const points = [];
|
||||||
|
const currentLayout = getKeyboardLayout(currentKeyboardLayout, includeNumbers);
|
||||||
|
|
||||||
|
for (const char of name.toUpperCase()) {
|
||||||
|
if (char in currentLayout) {
|
||||||
|
const { x, y } = currentLayout[char];
|
||||||
|
const yOffset = includeNumbers ? 100 : 40;
|
||||||
|
points.push({ x: x * 60 + 28, y: y * 60 + yOffset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points.length === 0) return '';
|
||||||
|
return generatePath(points, curveType);
|
||||||
|
}, [name, currentKeyboardLayout, curveType, includeNumbers]);
|
||||||
|
|
||||||
|
// Update parent component when signature changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (signaturePath && name) {
|
||||||
|
// Convert SVG to data URL for consistency with other signature types
|
||||||
|
const svgData = generateSVGDataURL(signaturePath);
|
||||||
|
onChange(svgData);
|
||||||
|
} else {
|
||||||
|
onChange('');
|
||||||
|
}
|
||||||
|
}, [signaturePath, name, onChange]);
|
||||||
|
|
||||||
|
const generateSVGDataURL = (path: string): string => {
|
||||||
|
const height = includeNumbers ? 260 : 200;
|
||||||
|
const gradients =
|
||||||
|
strokeConfig.style === StrokeStyle.GRADIENT
|
||||||
|
? `<linearGradient id="pathGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" style="stop-color:${strokeConfig.gradientStart};stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:${strokeConfig.gradientEnd};stop-opacity:1" />
|
||||||
|
</linearGradient>`
|
||||||
|
: '';
|
||||||
|
const strokeColor =
|
||||||
|
strokeConfig.style === StrokeStyle.SOLID ? strokeConfig.color : 'url(#pathGradient)';
|
||||||
|
|
||||||
|
const svgContent = `<svg width="650" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>${gradients}</defs>
|
||||||
|
<path d="${path}" stroke="${strokeColor}" stroke-width="${strokeConfig.width}" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
return `data:image/svg+xml;base64,${btoa(svgContent)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex h-full w-full flex-col items-center justify-center', className)}>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="sr-only"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative w-full max-w-lg">
|
||||||
|
<svg
|
||||||
|
className="pointer-events-none w-full"
|
||||||
|
viewBox="0 0 650 200"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
style={{ height: '150px' }}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
{strokeConfig.style === StrokeStyle.GRADIENT && (
|
||||||
|
<linearGradient id="pathGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor={strokeConfig.gradientStart} stopOpacity={1} />
|
||||||
|
<stop offset="100%" stopColor={strokeConfig.gradientEnd} stopOpacity={1} />
|
||||||
|
</linearGradient>
|
||||||
|
)}
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{signaturePath && (
|
||||||
|
<path
|
||||||
|
d={signaturePath}
|
||||||
|
stroke={
|
||||||
|
strokeConfig.style === StrokeStyle.SOLID ? strokeConfig.color : 'url(#pathGradient)'
|
||||||
|
}
|
||||||
|
strokeWidth={strokeConfig.width}
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 w-full max-w-lg">
|
||||||
|
<div className="text-muted-foreground/70 font-mono text-xs">{name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { KeyboardIcon, UploadCloudIcon } from 'lucide-react';
|
import { Keyboard, KeyboardIcon, UploadCloudIcon } from 'lucide-react';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||||
@ -10,6 +10,7 @@ import { isBase64Image } from '@documenso/lib/constants/signatures';
|
|||||||
import { SignatureIcon } from '../../icons/signature';
|
import { SignatureIcon } from '../../icons/signature';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { SignaturePadDraw } from './signature-pad-draw';
|
import { SignaturePadDraw } from './signature-pad-draw';
|
||||||
|
import { SignaturePadKeyboard } from './signature-pad-keyboard';
|
||||||
import { SignaturePadType } from './signature-pad-type';
|
import { SignaturePadType } from './signature-pad-type';
|
||||||
import { SignaturePadUpload } from './signature-pad-upload';
|
import { SignaturePadUpload } from './signature-pad-upload';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './signature-tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './signature-tabs';
|
||||||
@ -28,6 +29,7 @@ export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChang
|
|||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
uploadSignatureEnabled?: boolean;
|
uploadSignatureEnabled?: boolean;
|
||||||
drawSignatureEnabled?: boolean;
|
drawSignatureEnabled?: boolean;
|
||||||
|
keyboardSignatureEnabled?: boolean;
|
||||||
|
|
||||||
onValidityChange?: (isValid: boolean) => void;
|
onValidityChange?: (isValid: boolean) => void;
|
||||||
};
|
};
|
||||||
@ -39,10 +41,12 @@ export const SignaturePad = ({
|
|||||||
typedSignatureEnabled = true,
|
typedSignatureEnabled = true,
|
||||||
uploadSignatureEnabled = true,
|
uploadSignatureEnabled = true,
|
||||||
drawSignatureEnabled = true,
|
drawSignatureEnabled = true,
|
||||||
|
keyboardSignatureEnabled = true,
|
||||||
}: SignaturePadProps) => {
|
}: SignaturePadProps) => {
|
||||||
const [imageSignature, setImageSignature] = useState(isBase64Image(value) ? value : '');
|
const [imageSignature, setImageSignature] = useState(isBase64Image(value) ? value : '');
|
||||||
const [drawSignature, setDrawSignature] = useState(isBase64Image(value) ? value : '');
|
const [drawSignature, setDrawSignature] = useState(isBase64Image(value) ? value : '');
|
||||||
const [typedSignature, setTypedSignature] = useState(isBase64Image(value) ? '' : value);
|
const [typedSignature, setTypedSignature] = useState(isBase64Image(value) ? '' : value);
|
||||||
|
const [keyboardSignature, setKeyboardSignature] = useState(isBase64Image(value) ? value : '');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is cooked.
|
* This is cooked.
|
||||||
@ -51,7 +55,7 @@ export const SignaturePad = ({
|
|||||||
* the first enabled tab.
|
* the first enabled tab.
|
||||||
*/
|
*/
|
||||||
const [tab, setTab] = useState(
|
const [tab, setTab] = useState(
|
||||||
((): 'draw' | 'text' | 'image' => {
|
((): 'draw' | 'text' | 'image' | 'keyboard' => {
|
||||||
// First passthrough to check to see if there's a signature for a given tab.
|
// First passthrough to check to see if there's a signature for a given tab.
|
||||||
if (drawSignatureEnabled && drawSignature) {
|
if (drawSignatureEnabled && drawSignature) {
|
||||||
return 'draw';
|
return 'draw';
|
||||||
@ -65,6 +69,10 @@ export const SignaturePad = ({
|
|||||||
return 'image';
|
return 'image';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyboardSignatureEnabled && keyboardSignature) {
|
||||||
|
return 'keyboard';
|
||||||
|
}
|
||||||
|
|
||||||
// Second passthrough to just select the first avaliable tab.
|
// Second passthrough to just select the first avaliable tab.
|
||||||
if (drawSignatureEnabled) {
|
if (drawSignatureEnabled) {
|
||||||
return 'draw';
|
return 'draw';
|
||||||
@ -78,6 +86,10 @@ export const SignaturePad = ({
|
|||||||
return 'image';
|
return 'image';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyboardSignatureEnabled) {
|
||||||
|
return 'keyboard';
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error('No signature enabled');
|
throw new Error('No signature enabled');
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
@ -109,7 +121,16 @@ export const SignaturePad = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTabChange = (value: 'draw' | 'text' | 'image') => {
|
const onKeyboardSignatureChange = (value: string) => {
|
||||||
|
setKeyboardSignature(value);
|
||||||
|
|
||||||
|
onChange?.({
|
||||||
|
type: DocumentSignatureType.KEYBOARD,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTabChange = (value: 'draw' | 'text' | 'image' | 'keyboard') => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -126,10 +147,18 @@ export const SignaturePad = ({
|
|||||||
.with('image', () => {
|
.with('image', () => {
|
||||||
onImageSignatureChange(imageSignature);
|
onImageSignatureChange(imageSignature);
|
||||||
})
|
})
|
||||||
|
.with('keyboard', () => {
|
||||||
|
onKeyboardSignatureChange(keyboardSignature);
|
||||||
|
})
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!drawSignatureEnabled && !typedSignatureEnabled && !uploadSignatureEnabled) {
|
if (
|
||||||
|
!drawSignatureEnabled &&
|
||||||
|
!typedSignatureEnabled &&
|
||||||
|
!uploadSignatureEnabled &&
|
||||||
|
!keyboardSignatureEnabled
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +169,7 @@ export const SignaturePad = ({
|
|||||||
'pointer-events-none': disabled,
|
'pointer-events-none': disabled,
|
||||||
})}
|
})}
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
onValueChange={(value) => onTabChange(value as 'draw' | 'text' | 'image')}
|
onValueChange={(value) => onTabChange(value as 'draw' | 'text' | 'image' | 'keyboard')}
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{drawSignatureEnabled && (
|
{drawSignatureEnabled && (
|
||||||
@ -163,6 +192,13 @@ export const SignaturePad = ({
|
|||||||
Upload
|
Upload
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{keyboardSignatureEnabled && (
|
||||||
|
<TabsTrigger value="keyboard">
|
||||||
|
<Keyboard className="mr-2 size-4" />
|
||||||
|
Keyboard
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent
|
<TabsContent
|
||||||
@ -194,6 +230,13 @@ export const SignaturePad = ({
|
|||||||
>
|
>
|
||||||
<SignaturePadUpload value={imageSignature} onChange={onImageSignatureChange} />
|
<SignaturePadUpload value={imageSignature} onChange={onImageSignatureChange} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent
|
||||||
|
value="keyboard"
|
||||||
|
className="border-border aspect-signature-pad dark:bg-background relative flex items-center justify-center rounded-md border bg-neutral-50 text-center"
|
||||||
|
>
|
||||||
|
<SignaturePadKeyboard value={keyboardSignature} onChange={onKeyboardSignatureChange} />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user