diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 85e0b56..dbb610a 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -2443,6 +2443,7 @@ export type Database = { phone: string | null signature_block: string | null signature_image_path: string | null + signature_typed: string | null timezone: string | null title: string | null updated_at: string @@ -2458,6 +2459,7 @@ export type Database = { phone?: string | null signature_block?: string | null signature_image_path?: string | null + signature_typed?: string | null timezone?: string | null title?: string | null updated_at?: string @@ -2473,6 +2475,7 @@ export type Database = { phone?: string | null signature_block?: string | null signature_image_path?: string | null + signature_typed?: string | null timezone?: string | null title?: string | null updated_at?: string diff --git a/src/lib/docx-pleading.ts b/src/lib/docx-pleading.ts index e40e8aa..fd54980 100644 --- a/src/lib/docx-pleading.ts +++ b/src/lib/docx-pleading.ts @@ -27,6 +27,8 @@ export interface PleadingFootnote { export interface PleadingSignature { /** Multi-line block placed under the signature line (name, bar no., firm, etc.) */ block: string; + /** Optional typed signature (rendered in a cursive script font above the line) */ + typed?: string; /** Optional PNG/JPG bytes of a scanned signature placed above the line (for DOCX) */ imageBytes?: ArrayBuffer | Uint8Array; /** Same image as a data URL (for PDF generator, which needs a string) */ @@ -304,9 +306,12 @@ export function buildPleadingDoc(input: PleadingInput): Document { input.body.split("\n").forEach((line) => bodyParagraphs.push(p(line))); } - // Signature block (above footnotes, after body) + // Signature block (above footnotes, after body) — right-side, indented ~2/3 of page width const sig = input.signature; - if (sig && (sig.block?.trim() || sig.imageBytes)) { + const SIG_INDENT = 6240; // ~4.33" from left margin (page content is 9360 dxa wide) + const sigParagraph = (children: TextRun[]) => + new Paragraph({ indent: { left: SIG_INDENT }, children }); + if (sig && (sig.block?.trim() || sig.imageBytes || sig.typed?.trim())) { bodyParagraphs.push(emptyP()); bodyParagraphs.push(emptyP()); if (sig.imageBytes) { @@ -316,24 +321,39 @@ export function buildPleadingDoc(input: PleadingInput): Document { : new Uint8Array(sig.imageBytes); bodyParagraphs.push( new Paragraph({ + indent: { left: SIG_INDENT }, children: [ new ImageRun({ type: (sig.imageType as any) || "png", data, - transformation: { width: 220, height: 70 }, + transformation: { width: 200, height: 60 }, altText: { title: "Signature", description: "Attorney signature", name: "signature" }, }), ], }), ); + } else if (sig.typed?.trim()) { + // Typed cursive signature — script font, larger size + bodyParagraphs.push( + new Paragraph({ + indent: { left: SIG_INDENT }, + children: [ + new TextRun({ + text: sig.typed.trim(), + font: "Lucida Handwriting", + size: 36, + }), + ], + }), + ); } else { - // Reserve vertical space for a wet signature bodyParagraphs.push(emptyP()); bodyParagraphs.push(emptyP()); } - // Signature line (underscore rule, ~3 inches) - bodyParagraphs.push(p("_______________________________")); - sig.block.split("\n").forEach((line) => bodyParagraphs.push(p(line))); + bodyParagraphs.push(sigParagraph([run("_______________________________")])); + sig.block.split("\n").forEach((line) => + bodyParagraphs.push(sigParagraph([run(line)])), + ); } // Footnotes rendered as endnote-style block at bottom of document diff --git a/src/lib/pdf-pleading.ts b/src/lib/pdf-pleading.ts index f39fc32..7db4707 100644 --- a/src/lib/pdf-pleading.ts +++ b/src/lib/pdf-pleading.ts @@ -272,29 +272,34 @@ export async function downloadPleadingPdf(input: PleadingInput, filename: string }); } - // Signature block (above footnotes) + // Signature block (above footnotes) — right-side, indented ~2/3 of page width const sig = input.signature; - if (sig && (sig.block?.trim() || sig.imageDataUrl)) { + const SIG_X = MARGIN + CONTENT_W * (2 / 3); + if (sig && (sig.block?.trim() || sig.imageDataUrl || sig.typed?.trim())) { ensureSpace(LINE_H * 6); y += LINE_H * 1.5; if (sig.imageDataUrl) { try { const fmt = (sig.imageType || "png").toUpperCase(); - // ~3 inches wide, ~1 inch tall - pdf.addImage(sig.imageDataUrl, fmt as any, MARGIN, y - LINE_H, 220, 70); - y += 70 - LINE_H + 4; + pdf.addImage(sig.imageDataUrl, fmt as any, SIG_X, y - LINE_H, 150, 50); + y += 50 - LINE_H + 4; } catch { y += LINE_H * 2; } + } else if (sig.typed?.trim()) { + // Typed cursive signature — fall back to italic Bookman (script fonts not embedded) + setFont(pdf, { italic: true }, 18); + pdf.text(sig.typed.trim(), SIG_X, y); + y += LINE_H; } else { - y += LINE_H * 3; // reserve space for a wet signature + y += LINE_H * 3; } setFont(pdf, {}); - pdf.text("_______________________________", MARGIN, y); + pdf.text("_______________________________", SIG_X, y); y += LINE_H; sig.block.split("\n").forEach((line) => { ensureSpace(LINE_H); - pdf.text(line, MARGIN, y); + pdf.text(line, SIG_X, y); y += LINE_H; }); } diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx index 44be252..de25605 100644 --- a/src/routes/documents.pleading.new.tsx +++ b/src/routes/documents.pleading.new.tsx @@ -50,6 +50,7 @@ function PleadingNewPage() { // Attorney signature (loaded from profile) const [includeSignature, setIncludeSignature] = useState(true); const [sigBlock, setSigBlock] = useState(""); + const [sigTyped, setSigTyped] = useState(""); const [sigImagePath, setSigImagePath] = useState(""); const [sigImageBytes, setSigImageBytes] = useState(null); const [sigImageDataUrl, setSigImageDataUrl] = useState(""); @@ -66,6 +67,7 @@ function PleadingNewPage() { if (!data) return; const d = data as any; setSigBlock(d.signature_block ?? ""); + setSigTyped(d.signature_typed ?? ""); setSigImagePath(d.signature_image_path ?? ""); if (d.signature_image_path) { try { @@ -76,7 +78,6 @@ function PleadingNewPage() { const blob = await res.blob(); const buf = new Uint8Array(await blob.arrayBuffer()); setSigImageBytes(buf); - // Build a data URL for the PDF generator const reader = new FileReader(); reader.onload = () => setSigImageDataUrl(String(reader.result || "")); reader.readAsDataURL(blob); @@ -91,9 +92,10 @@ function PleadingNewPage() { const buildSignature = (): PleadingSignature | undefined => { if (!includeSignature) return undefined; - if (!sigBlock.trim() && !sigImageBytes) return undefined; + if (!sigBlock.trim() && !sigImageBytes && !sigTyped.trim()) return undefined; return { block: sigBlock, + typed: sigTyped || undefined, imageBytes: sigImageBytes ?? undefined, imageDataUrl: sigImageDataUrl || undefined, imageType: sigImageType, @@ -398,7 +400,7 @@ function PleadingNewPage() { {includeSignature ? ( - !sigBlock.trim() && !sigImagePath ? ( + !sigBlock.trim() && !sigImagePath && !sigTyped.trim() ? (
No signature found on your profile yet. Add one in Settings → Profile.
@@ -407,19 +409,28 @@ function PleadingNewPage() { className="border rounded-md p-4 bg-white text-black" style={{ fontFamily: '"Bookman Old Style", Georgia, serif', fontSize: 12 }} > - {sigImagePath && sigImageDataUrl ? ( - Signature - ) : ( -
- )} -
_______________________________
-
-                    {sigBlock || (no block text)}
-                  
+
+ {sigImagePath && sigImageDataUrl ? ( + Signature + ) : sigTyped.trim() ? ( +
+ {sigTyped} +
+ ) : ( +
+ )} +
_______________________________
+
+                      {sigBlock || (no block text)}
+                    
+
) ) : null} diff --git a/src/routes/settings.profile.tsx b/src/routes/settings.profile.tsx index 1bc3fb0..cd4e1ff 100644 --- a/src/routes/settings.profile.tsx +++ b/src/routes/settings.profile.tsx @@ -23,6 +23,7 @@ const EMPTY = { timezone: "", avatar_url: "" as string | null | "", signature_block: "", + signature_typed: "", signature_image_path: "" as string | null | "", }; @@ -66,6 +67,7 @@ function ProfilePage() { timezone: d.timezone ?? "", avatar_url: d.avatar_url ?? "", signature_block: d.signature_block ?? "", + signature_typed: d.signature_typed ?? "", signature_image_path: d.signature_image_path ?? "", }); } @@ -184,6 +186,7 @@ function ProfilePage() { bio: form.bio || null, timezone: form.timezone || null, signature_block: form.signature_block || null, + signature_typed: form.signature_typed || null, } as any) .eq("id", user.id); setSaving(false); @@ -323,6 +326,29 @@ function ProfilePage() { /> +
+ + update("signature_typed", e.target.value)} + placeholder="e.g. /s/ Jane Q. Attorney" + /> + {form.signature_typed && ( +
+ + {form.signature_typed} + +
+ )} +

+ Used above the signature line if no signature image is uploaded. Rendered in a script font. +

+
+ +
diff --git a/supabase/migrations/20260418182555_51d2405c-4648-466e-901f-307ae97f6f8a.sql b/supabase/migrations/20260418182555_51d2405c-4648-466e-901f-307ae97f6f8a.sql new file mode 100644 index 0000000..9ef616f --- /dev/null +++ b/supabase/migrations/20260418182555_51d2405c-4648-466e-901f-307ae97f6f8a.sql @@ -0,0 +1 @@ +ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS signature_typed TEXT; \ No newline at end of file