docs(phase-10): research expanded field types end-to-end
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,522 @@
|
||||
# Phase 10: Expanded Field Types End-to-End - Research
|
||||
|
||||
**Researched:** 2026-03-21
|
||||
**Domain:** PDF field type rendering (@cantoo/pdf-lib), FieldPlacer UI extension (@dnd-kit/core), signing page initials capture (signature_pad / canvas), Next.js API route branching
|
||||
**Confidence:** HIGH
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|-----------------|
|
||||
| FIELD-01 | Agent can place text field markers on a PDF (for typed content like names, addresses, prices) | Already complete (Phase 8); schema discriminant `type: 'text'` is live. Phase 10 wires up the FieldPlacer palette token for text and ensures prepare-document.ts handles it distinctly from the placeholder rectangle. |
|
||||
| FIELD-02 | Agent can place checkbox field markers on a PDF | FieldPlacer palette needs a "Checkbox" draggable token that writes `type: 'checkbox'`; prepare-document.ts must draw a boolean mark (filled box + X lines) at field coordinates at prepare time; signing page receives no checkbox overlay (already embedded). |
|
||||
| FIELD-03 | Agent can place initials field markers on a PDF | FieldPlacer palette needs an "Initials" draggable token that writes `type: 'initials'`; signing page must present an initials-capture interaction (separate from full-signature modal) for each initials field; POST /api/sign/[token] must embed the initials image at field coordinates. |
|
||||
| FIELD-04 | Agent can place date field markers that auto-fill with the signing date | FieldPlacer palette needs a "Date" draggable token that writes `type: 'date'`; prepare-document.ts must stamp the current date as text at field coordinates at prepare time; signing page receives no date overlay (already embedded). |
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 10 completes the full end-to-end field type pipeline for text, checkbox, initials, and date. Phase 8 already established the schema discriminant (`SignatureFieldData.type`), the `getFieldType()` helper, and the server-side `isClientVisibleField()` filter. Phase 10 builds on that foundation by extending three surfaces: (1) the FieldPlacer palette in the agent portal, (2) the `preparePdf()` function in `prepare-document.ts`, and (3) the client signing page.
|
||||
|
||||
The clearest architectural split is "prepare-time fields" vs "sign-time fields." Text, checkbox, and date fields are **prepare-time only** — they are baked into the prepared PDF by `preparePdf()` and are never shown to the client as interactive overlays. Initials fields are **sign-time only** — they are shown as an interactive overlay to the client (analogous to the existing `client-signature` flow) and embedded at POST submission via `embedSignatureInPdf()`. This split means: text/checkbox/date handling lives entirely in `prepare-document.ts` and never touches the signing page; initials handling lives in `SigningPageClient.tsx` and `embed-signature.ts` and never touches `preparePdf()`.
|
||||
|
||||
The most fragile surface is the `FieldPlacer` component. The existing implementation has a single `DraggableToken` that produces a generic signature field. Phase 10 must extend the palette with four typed tokens (text, checkbox, initials, date), and the `handleDragEnd` callback must write the correct `type` property on the new `SignatureFieldData` object. The field overlays on the page must also visually distinguish the four types so the agent can see what they placed.
|
||||
|
||||
**Primary recommendation:** Implement Phase 10 as two sequential plans — Plan A: FieldPlacer palette extension + prepare-document.ts new field type handling (text, checkbox, date); Plan B: Signing page initials capture flow. This ordering ensures the prepare pipeline works before building the client-facing initials UI.
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core (All Existing — No New Dependencies)
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| @cantoo/pdf-lib | ^2.6.3 | Draw text stamps, checkbox marks (lines + rectangle), and date text on PDF pages | Already used in prepare-document.ts; provides `drawText`, `drawRectangle`, `drawLine` — sufficient for all four field types |
|
||||
| @dnd-kit/core | ^6.3.1 | Drag-and-drop palette tokens in FieldPlacer | Already used; just add new token components with typed `id` values |
|
||||
| @dnd-kit/utilities | ^3.2.2 | CSS transform utilities for drag overlay | Already used |
|
||||
| react-pdf | ^10.4.1 | PDF rendering on signing page | Already used; no change needed |
|
||||
| signature_pad | ^5.1.3 | Canvas-based handwriting capture for initials | Already used in SignatureModal; initials modal reuses same mechanism |
|
||||
| drizzle-orm | ^0.45.1 | No schema change needed — `SignatureFieldData.type` already supports all four types | Already used |
|
||||
| next | 16.2.0 | API routes + React components | Already used |
|
||||
|
||||
### No New Dependencies
|
||||
|
||||
Phase 10 adds zero new npm packages. All four field types can be implemented using existing libraries: `@cantoo/pdf-lib` drawing primitives for prepare-time rendering, `signature_pad` (via the existing `SignatureModal`) for initials capture, and `@dnd-kit/core` for the FieldPlacer palette tokens.
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
# No new packages needed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended File Modification Map
|
||||
|
||||
```
|
||||
src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
|
||||
# Modified: 4 typed DraggableToken variants; handleDragEnd writes type property;
|
||||
# renderFields shows per-type labels and colors
|
||||
|
||||
src/lib/pdf/prepare-document.ts
|
||||
# Modified: field rendering loop branches on getFieldType():
|
||||
# text → drawRectangle placeholder (already done for signatures) — but in Phase 10
|
||||
# text fields placed by agent are TYPED stamps, not "Sign Here" rects
|
||||
# checkbox → drawRectangle (border) + drawLine (X diagonals) to show a checked box
|
||||
# date → drawText with signing-date value formatted as "MM/DD/YYYY"
|
||||
# initials → drawRectangle + "Initials" label (same as current "Sign Here" pattern)
|
||||
# client-signature → drawRectangle + "Sign Here" (current behavior, unchanged)
|
||||
# agent-signature → skip (already embedded or handled by Phase 11)
|
||||
|
||||
src/app/sign/[token]/_components/SigningPageClient.tsx
|
||||
# Modified: handleFieldClick now ALSO opens modal for 'initials' type;
|
||||
# field overlay render distinguishes initials vs signature visually;
|
||||
# handleSubmit counts both client-signature AND initials fields in completeness check
|
||||
|
||||
src/app/api/sign/[token]/route.ts
|
||||
# Modified (POST handler): build signaturesWithCoords for BOTH client-signature and
|
||||
# initials fields — initials fields must be embedded via embedSignatureInPdf() same as
|
||||
# signatures. Currently line 173 iterates ALL signatureFields; it already includes
|
||||
# initials fields if they pass isClientVisibleField() — but the POST handler currently
|
||||
# throws if a client signature is missing for a field. Must filter to only
|
||||
# fields that the client signed (client-signature + initials) vs already-embedded fields.
|
||||
|
||||
src/lib/signing/embed-signature.ts
|
||||
# No change needed — embedSignatureInPdf() is generic: it embeds any PNG dataURL at
|
||||
# any (x,y,width,height,page) coordinate. Initials images embed identically to signatures.
|
||||
|
||||
src/app/sign/[token]/_components/SignatureModal.tsx
|
||||
# Likely no change — the initials modal can reuse SignatureModal with a different title.
|
||||
# OR: create a new InitialsModal component if the UX needs to differ (smaller canvas,
|
||||
# "Draw your initials" heading). Evaluate at plan time.
|
||||
```
|
||||
|
||||
### Pattern 1: Typed Palette Tokens in FieldPlacer
|
||||
|
||||
**What:** Each draggable token in the FieldPlacer palette corresponds to one `SignatureFieldType`. When dropped, `handleDragEnd` sets `newField.type` to the appropriate value.
|
||||
|
||||
**When to use:** Always when adding a new field type that the agent places.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// FieldPlacer.tsx — typed token IDs drive handleDragEnd dispatch
|
||||
// Token IDs use the SignatureFieldType string directly for clarity.
|
||||
|
||||
type PaletteTokenId = 'text' | 'checkbox' | 'initials' | 'date' | 'client-signature';
|
||||
|
||||
function DraggableToken({ id, label, color }: { id: PaletteTokenId; label: string; color: string }) {
|
||||
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id });
|
||||
// ... same style as existing DraggableToken but parameterized by color/label
|
||||
}
|
||||
|
||||
// In handleDragEnd — dispatch on token ID to set field type
|
||||
const tokenType: Record<string, SignatureFieldType> = {
|
||||
'text': 'text',
|
||||
'checkbox': 'checkbox',
|
||||
'initials': 'initials',
|
||||
'date': 'date',
|
||||
'client-signature': 'client-signature',
|
||||
};
|
||||
|
||||
const newField: SignatureFieldData = {
|
||||
id: crypto.randomUUID(),
|
||||
page: currentPage,
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: 144,
|
||||
height: 36,
|
||||
type: tokenType[active.id as string] ?? 'client-signature',
|
||||
};
|
||||
```
|
||||
|
||||
**Key insight:** The `active.id` from `@dnd-kit/core` DragEndEvent is the id string passed to `useDraggable`. Using the `SignatureFieldType` string value as the token id is a clean 1:1 mapping that avoids a lookup table.
|
||||
|
||||
### Pattern 2: Field Type Dispatch in prepare-document.ts
|
||||
|
||||
**What:** The existing field rendering loop in `preparePdf()` treats every field identically (blue rectangle + "Sign Here"). Phase 10 branches on `getFieldType(field)` to render each type differently.
|
||||
|
||||
**When to use:** In `preparePdf()` for any field type that is baked into the prepared PDF.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// Source: src/lib/pdf/prepare-document.ts — Phase 10 target
|
||||
import { getFieldType } from '@/lib/db/schema';
|
||||
|
||||
// Replace the current generic loop:
|
||||
for (const field of sigFields) {
|
||||
const page = pages[field.page - 1];
|
||||
if (!page) continue;
|
||||
const fieldType = getFieldType(field);
|
||||
|
||||
if (fieldType === 'client-signature' || fieldType === 'initials') {
|
||||
// Placeholder rectangle — client will fill this at signing time
|
||||
page.drawRectangle({
|
||||
x: field.x, y: field.y, width: field.width, height: field.height,
|
||||
borderColor: rgb(0.15, 0.39, 0.92), borderWidth: 1.5,
|
||||
color: rgb(0.90, 0.94, 0.99),
|
||||
});
|
||||
const label = fieldType === 'initials' ? 'Initials' : 'Sign Here';
|
||||
page.drawText(label, { x: field.x + 4, y: field.y + 4, size: 8, font: helvetica, color: rgb(0.15, 0.39, 0.92) });
|
||||
|
||||
} else if (fieldType === 'text') {
|
||||
// Text fields are already handled above by Strategy A/B (AcroForm + stamp).
|
||||
// At prepare time, text fields placed by the agent are TYPE='text' in signatureFields.
|
||||
// They should NOT get a "Sign Here" rectangle — they were meant as typed content areas.
|
||||
// If the agent provided textFillData for this field by label, it's already stamped.
|
||||
// Draw a light background rectangle only if no matching textFillData entry exists.
|
||||
// (See Open Questions #1 — simplest approach: skip, let Strategy B handle text fill)
|
||||
|
||||
} else if (fieldType === 'checkbox') {
|
||||
// Draw a filled checked box: rectangle + X crossing diagonals
|
||||
page.drawRectangle({
|
||||
x: field.x, y: field.y, width: field.width, height: field.height,
|
||||
borderColor: rgb(0.1, 0.1, 0.1), borderWidth: 1.5,
|
||||
color: rgb(0.95, 0.95, 0.95),
|
||||
});
|
||||
// X mark: two diagonals
|
||||
page.drawLine({
|
||||
start: { x: field.x + 2, y: field.y + 2 },
|
||||
end: { x: field.x + field.width - 2, y: field.y + field.height - 2 },
|
||||
thickness: 1.5, color: rgb(0.1, 0.1, 0.1),
|
||||
});
|
||||
page.drawLine({
|
||||
start: { x: field.x + field.width - 2, y: field.y + 2 },
|
||||
end: { x: field.x + 2, y: field.y + field.height - 2 },
|
||||
thickness: 1.5, color: rgb(0.1, 0.1, 0.1),
|
||||
});
|
||||
|
||||
} else if (fieldType === 'date') {
|
||||
// Auto-stamp the signing date (= preparation date) as text
|
||||
const signingDate = new Date().toLocaleDateString('en-US', {
|
||||
month: '2-digit', day: '2-digit', year: 'numeric',
|
||||
});
|
||||
page.drawText(signingDate, {
|
||||
x: field.x + 4, y: field.y + 4,
|
||||
size: 10, font: helvetica, color: rgb(0.05, 0.05, 0.55),
|
||||
});
|
||||
|
||||
} else if (fieldType === 'agent-signature') {
|
||||
// Skip — agent signature handled by Phase 11; do not draw placeholder here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key insight:** Text fields placed as `SignatureFieldData` with `type: 'text'` are distinct from the `textFillData` map used in the Strategy A/B text-fill pipeline. The planner must decide whether `type: 'text'` fields in `signatureFields` are purely visual markers (skip them in the loop, let the agent use textFillData) or whether they should also get a light background rectangle. The simplest correct behavior is: skip them in the loop — the agent fills text via the TextFillForm, not via typed SignatureFieldData entries.
|
||||
|
||||
### Pattern 3: Initials Capture on Signing Page
|
||||
|
||||
**What:** `handleFieldClick` must open a modal for `initials` type fields, the same way it opens for `client-signature`. The modal content can be identical (draw on canvas), with different heading text.
|
||||
|
||||
**When to use:** In `SigningPageClient.tsx` for any field the client must actively complete.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// SigningPageClient.tsx — Phase 10 extension
|
||||
|
||||
// handleFieldClick: open modal for BOTH client-signature and initials
|
||||
const handleFieldClick = useCallback(
|
||||
(fieldId: string) => {
|
||||
const field = signatureFields.find((f) => f.id === fieldId);
|
||||
if (!field) return;
|
||||
const ft = getFieldType(field);
|
||||
// Phase 8 guard: only client-signature and initials open the capture modal
|
||||
if (ft !== 'client-signature' && ft !== 'initials') return;
|
||||
if (signedFields.has(fieldId)) return;
|
||||
setActiveFieldId(fieldId);
|
||||
setModalOpen(true);
|
||||
},
|
||||
[signatureFields, signedFields]
|
||||
);
|
||||
|
||||
// handleSubmit: require BOTH client-signature AND initials fields to be completed
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const requiredFields = signatureFields.filter(
|
||||
(f) => getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials'
|
||||
);
|
||||
if (signedFields.size < requiredFields.length || submitting) return;
|
||||
// ... rest unchanged
|
||||
}, [signatureFields, signedFields.size, submitting, token]);
|
||||
|
||||
// SigningProgressBar: total = required fields count
|
||||
<SigningProgressBar
|
||||
total={signatureFields.filter(
|
||||
(f) => getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials'
|
||||
).length}
|
||||
signed={signedFields.size}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
### Pattern 4: POST Route — Skip Non-Interactive Fields
|
||||
|
||||
**What:** The current POST handler in `/api/sign/[token]/route.ts` iterates ALL `signatureFields` and throws if any field lacks a client-submitted signature. With Phase 10, text/checkbox/date fields are embedded at prepare time — the client never signs them. The POST handler must only iterate client-signature and initials fields.
|
||||
|
||||
**Critical detail:** The current line 173 (`const signaturesWithCoords = (doc.signatureFields ?? []).map(...)`) throws `Error: Missing signature for field ${field.id}` for fields the client was never asked to sign. This will break Phase 10 if not fixed.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// /api/sign/[token]/route.ts — POST handler, line 173 replacement
|
||||
|
||||
// Only embed fields that the client actually signs (client-signature + initials)
|
||||
const clientSignableTypes: SignatureFieldType[] = ['client-signature', 'initials'];
|
||||
const signaturesWithCoords = (doc.signatureFields ?? [])
|
||||
.filter((field) => clientSignableTypes.includes(getFieldType(field)))
|
||||
.map((field) => {
|
||||
const clientSig = signatures.find((s) => s.fieldId === field.id);
|
||||
if (!clientSig) throw new Error(`Missing signature for field ${field.id}`);
|
||||
return {
|
||||
fieldId: field.id,
|
||||
dataURL: clientSig.dataURL,
|
||||
x: field.x, y: field.y, width: field.width, height: field.height, page: field.page,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **Treating `type: 'text'` SignatureFieldData as a prepare-pipeline text input.** Text placed via FieldPlacer as `type: 'text'` is a visual marker. The agent fills text content via `TextFillForm` + `textFillData`. Don't try to match `type: 'text'` SignatureFieldData against `textFillData` keys — they are separate concepts.
|
||||
- **Rendering checkbox fields as "Sign Here" placeholders.** Checkboxes are boolean — they are always checked (since the agent placed them). Don't show them as interactive overlays on the signing page.
|
||||
- **Date fields using the client's local timezone.** The signing date should be consistent. Use UTC or the document's preparation timezone (Mountain Time, America/Denver) and format it with `toLocaleDateString('en-US')`. Since date is stamped at prepare time (not sign time), capture `new Date()` in `preparePdf()` which runs server-side.
|
||||
- **Passing all four types to `SigningProgressBar` as required.** Only `client-signature` and `initials` fields count toward the signing progress total. Text/checkbox/date are already embedded.
|
||||
- **Reusing the same `DraggableToken` id for all types.** dnd-kit requires unique IDs for simultaneous drags. Each palette token needs a distinct `id` string (the type string itself works: `'text'`, `'checkbox'`, `'initials'`, `'date'`, `'client-signature'`).
|
||||
- **Forgetting to update `isClientVisibleField()` for the signing page filter.** The current implementation returns `true` for all types except `agent-signature`. This means `text`, `checkbox`, and `date` fields ARE passed to the signing page. The signing page must not render them as interactive overlays. The server-side filter in GET `/api/sign/[token]` does NOT need to change — the client signing page just won't render interactive overlays for non-interactive types.
|
||||
|
||||
---
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Drawing a checkmark or X on a PDF | SVG-to-canvas renderer | `page.drawLine()` in @cantoo/pdf-lib | Two diagonal lines produce a clear X mark with zero dependencies |
|
||||
| Date formatting | Custom date formatter | `new Date().toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' })` | Standard JS, server-side consistent output |
|
||||
| Initials canvas capture | New canvas drawing library | Reuse existing `SignatureModal` (which uses `signature_pad`) with `title="Draw your initials"` | signature_pad is already in the bundle; initials are drawn on a canvas identically to signatures |
|
||||
| New PDF drawing library | Replacing @cantoo/pdf-lib | @cantoo/pdf-lib `drawLine`, `drawRectangle`, `drawText` | All required primitives exist; confirmed in source inspection |
|
||||
| New drag source type logic | Custom dnd system | @dnd-kit/core `useDraggable({ id: 'checkbox' })` | One new `DraggableToken` call per type; dnd-kit already wired |
|
||||
|
||||
**Key insight:** All four new field types can be rendered using only `drawText`, `drawRectangle`, and `drawLine` from @cantoo/pdf-lib — no new library or complex SVG path rendering required.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: POST Route Throws on Non-Interactive Fields
|
||||
**What goes wrong:** After Phase 10 ships, a document with checkbox + text fields is prepared and sent. The client opens the signing link and submits their signature. The POST handler at line 173 iterates ALL `signatureFields` including checkbox/text/date fields and throws `Missing signature for field {id}` because those fields were never shown to the client.
|
||||
**Why it happens:** The current POST handler assumes every field in `signatureFields` was presented to the client as a signable field.
|
||||
**How to avoid:** Filter `doc.signatureFields` to only `['client-signature', 'initials']` types before building `signaturesWithCoords`. This is the critical fix in Plan A or Plan B.
|
||||
**Warning signs:** Signing submission returns 500 with `pdf-embed-failed`; console shows "Missing signature for field" error.
|
||||
|
||||
### Pitfall 2: Date Stamped at Wrong Time
|
||||
**What goes wrong:** The date field is intended to auto-fill with the signing date. But `preparePdf()` runs at PREPARE time (when the agent clicks "Prepare and Send"), not at sign time. If prepare time is January 10 and the client signs January 15, the embedded date is January 10.
|
||||
**Why it happens:** The requirement says "auto-fill with the signing date" — but the architecture embeds date at prepare time.
|
||||
**How to avoid:** The REQUIREMENTS.md states "date fields that auto-fill with the signing date." If the date must be the actual signing date, it should be stamped at POST `/api/sign/[token]` time, not in `preparePdf()`. However, this requires a different architecture: `preparePdf()` would leave a date placeholder rectangle, and the POST handler would stamp the actual date on the prepared PDF before embedding signatures. **Recommendation: stamp the date at POST time** — this is the correct interpretation of "signing date." The prepare step draws a light rectangle placeholder (like initials); the POST handler stamps the actual date text using pdf-lib before calling `embedSignatureInPdf()`.
|
||||
**Warning signs:** Date in embedded PDF is 5 days before the client signed.
|
||||
|
||||
### Pitfall 3: Text Fields as SignatureFieldData Creating Confusion
|
||||
**What goes wrong:** FIELD-01 says "agent can place text field markers." These are placed via FieldPlacer as `SignatureFieldData` with `type: 'text'`. But the existing `TextFillForm` + `textFillData` pipeline is a separate key-value map with no connection to SignatureFieldData coordinates. The prepare pipeline currently fills `textFillData` via Strategy A (AcroForm) and Strategy B (page 1 stamp). Phase 10's `type: 'text'` SignatureFieldData entries sit in `sigFields` — if `prepare-document.ts` tries to match them against `textFillData` entries, the match logic is ambiguous (SignatureFieldData has no `label` field).
|
||||
**Why it happens:** Two separate text mechanisms exist: positional text (SignatureFieldData type=text) and keyed text (textFillData). They are not currently connected.
|
||||
**How to avoid:** For Phase 10, treat `type: 'text'` SignatureFieldData as visual markers only — draw a light background rectangle at their coordinates to indicate where typed content should appear. The actual text is still provided via `textFillData`. Don't attempt to wire SignatureFieldData.text fields into textFillData matching — that's a more complex feature. The REQUIREMENTS.md says FIELD-01 is already complete; the primary new work is FIELD-02, FIELD-03, FIELD-04.
|
||||
**Warning signs:** Prepare pipeline crashes trying to find a `textFillData[field.id]` entry; undefined text values stamped on page.
|
||||
|
||||
### Pitfall 4: Signing Page Renders Checkbox/Date as Interactive Overlays
|
||||
**What goes wrong:** `SigningPageClient.tsx` renders all `signatureFields` returned from the server as clickable overlays. After Phase 10, the server GET route still returns `text`, `checkbox`, and `date` fields through `isClientVisibleField()` (which only excludes `agent-signature`). The client signing page receives these and renders them as blue pulsing overlays the client can click — but clicking them does nothing (handleFieldClick guards against non-interactive types). The client sees mysterious non-responsive highlighted areas on the PDF.
|
||||
**Why it happens:** `isClientVisibleField()` was designed to only exclude `agent-signature`. It does not exclude prepare-time field types.
|
||||
**How to avoid:** In `SigningPageClient.tsx`, when rendering field overlays, only render interactive overlays for `client-signature` and `initials` types. For other types, render nothing (or a static visual indicator that's not clickable). The server filter does not need to change.
|
||||
**Warning signs:** Signed PDF has a pulsing blue rectangle over the checkbox area that the client cannot dismiss.
|
||||
|
||||
### Pitfall 5: FieldPlacer DragOverlay Shows Wrong Label for Each Type
|
||||
**What goes wrong:** The current `DragOverlay` renders a static "Signature" label regardless of which token is being dragged. After Phase 10 adds 4 tokens, dragging any of them shows "Signature" as the ghost label.
|
||||
**Why it happens:** The current `DragOverlay` is not parameterized by token type.
|
||||
**How to avoid:** Track which token ID is being dragged in `onDragStart` state, and render the correct label in `DragOverlay`. dnd-kit provides `active.id` in the `onDragStart` callback.
|
||||
**Warning signs:** Agent drags "Date" token but the ghost overlay says "Signature."
|
||||
|
||||
### Pitfall 6: Palette Has Too Many Tokens — "Signature" Token Remains
|
||||
**What goes wrong:** The existing palette has one "Signature" token (which creates `type: 'client-signature'` fields). After Phase 10, the palette should clearly show: "Signature", "Initials", "Checkbox", "Date", and optionally "Text". If the old token is simply renamed or removed by mistake, agents who had already learned the UI are confused.
|
||||
**Why it happens:** Unclear whether to keep the original signature token or replace it.
|
||||
**How to avoid:** Keep the "Signature" token (maps to `client-signature`). Add four new tokens. The full palette is: Signature, Initials, Checkbox, Date, Text (5 total). Each has a distinct color in the palette for quick identification.
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
Verified patterns from codebase inspection (2026-03-21):
|
||||
|
||||
### Current FieldPlacer handleDragEnd — Field Creation (to be extended)
|
||||
```typescript
|
||||
// Source: src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
|
||||
// Current: no type property set on new field
|
||||
const newField: SignatureFieldData = {
|
||||
id: crypto.randomUUID(),
|
||||
page: currentPage,
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: 144,
|
||||
height: 36,
|
||||
// type: undefined — falls back to 'client-signature' via getFieldType()
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 10 FieldPlacer handleDragEnd — Typed Field Creation
|
||||
```typescript
|
||||
// Source: Phase 10 target
|
||||
// active.id is the dnd-kit draggable id — use the SignatureFieldType string directly
|
||||
import { getFieldType, type SignatureFieldType } from '@/lib/db/schema';
|
||||
|
||||
const validTypes = new Set<string>(['client-signature', 'initials', 'text', 'checkbox', 'date']);
|
||||
const droppedType = validTypes.has(active.id as string)
|
||||
? (active.id as SignatureFieldType)
|
||||
: 'client-signature';
|
||||
|
||||
const newField: SignatureFieldData = {
|
||||
id: crypto.randomUUID(),
|
||||
page: currentPage,
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: droppedType === 'checkbox' ? 24 : 144, // checkboxes are smaller
|
||||
height: droppedType === 'checkbox' ? 24 : 36,
|
||||
type: droppedType,
|
||||
};
|
||||
```
|
||||
|
||||
### prepare-document.ts — Checkbox Drawing with drawLine
|
||||
```typescript
|
||||
// Source: @cantoo/pdf-lib PDFPage.ts (confirmed: drawLine(options: PDFPageDrawLineOptions) exists)
|
||||
// drawLine signature: { start: {x,y}, end: {x,y}, thickness?: number, color?: Color }
|
||||
|
||||
// Checkbox: border rectangle + X diagonals
|
||||
page.drawRectangle({
|
||||
x: field.x, y: field.y, width: field.width, height: field.height,
|
||||
borderColor: rgb(0.1, 0.1, 0.1), borderWidth: 1.5,
|
||||
color: rgb(0.95, 0.95, 0.95),
|
||||
});
|
||||
page.drawLine({
|
||||
start: { x: field.x + 2, y: field.y + 2 },
|
||||
end: { x: field.x + field.width - 2, y: field.y + field.height - 2 },
|
||||
thickness: 1.5, color: rgb(0.1, 0.1, 0.1),
|
||||
});
|
||||
page.drawLine({
|
||||
start: { x: field.x + field.width - 2, y: field.y + 2 },
|
||||
end: { x: field.x + 2, y: field.y + field.height - 2 },
|
||||
thickness: 1.5, color: rgb(0.1, 0.1, 0.1),
|
||||
});
|
||||
```
|
||||
|
||||
### prepare-document.ts — Date Stamping (prepare-time approach)
|
||||
```typescript
|
||||
// Note: See Open Questions #2 — if signing-date accuracy matters, stamp at POST time instead
|
||||
const signingDate = new Date().toLocaleDateString('en-US', {
|
||||
month: '2-digit', day: '2-digit', year: 'numeric',
|
||||
});
|
||||
page.drawText(signingDate, {
|
||||
x: field.x + 4,
|
||||
y: field.y + field.height / 2 - 4, // vertically center in field
|
||||
size: 10, font: helvetica, color: rgb(0.05, 0.05, 0.55),
|
||||
});
|
||||
```
|
||||
|
||||
### SigningPageClient.tsx — Initials Field Rendering
|
||||
```typescript
|
||||
// Phase 10 target — distinguish initials overlays visually
|
||||
const fieldType = getFieldType(field);
|
||||
const isInteractive = fieldType === 'client-signature' || fieldType === 'initials';
|
||||
|
||||
// Only render overlay for interactive field types
|
||||
if (!isInteractive) return null;
|
||||
|
||||
const label = fieldType === 'initials' ? 'Initials' : 'Sign Here';
|
||||
const borderColor = fieldType === 'initials' ? '#7c3aed' : '#3b82f6'; // purple for initials, blue for sig
|
||||
```
|
||||
|
||||
### POST route — Filter to signable fields only
|
||||
```typescript
|
||||
// Source: src/app/api/sign/[token]/route.ts lines 171-188 (current — must change)
|
||||
// Phase 10 fix: only map fields the client was required to sign
|
||||
|
||||
import { getFieldType } from '@/lib/db/schema';
|
||||
|
||||
const signableFields = (doc.signatureFields ?? []).filter((f) => {
|
||||
const t = getFieldType(f);
|
||||
return t === 'client-signature' || t === 'initials';
|
||||
});
|
||||
|
||||
const signaturesWithCoords = signableFields.map((field) => {
|
||||
const clientSig = signatures.find((s) => s.fieldId === field.id);
|
||||
if (!clientSig) throw new Error(`Missing signature for field ${field.id}`);
|
||||
return {
|
||||
fieldId: field.id,
|
||||
dataURL: clientSig.dataURL,
|
||||
x: field.x, y: field.y, width: field.width, height: field.height, page: field.page,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Single "Signature" token in palette | 5 typed tokens: Signature, Initials, Checkbox, Date, Text | Phase 10 | Agent can place the right marker for each intent |
|
||||
| All fields get "Sign Here" rectangle in preparePdf | Type-branched rendering: checkbox=X, date=text stamp, initials=placeholder | Phase 10 | Prepared PDF visually represents what each field contains |
|
||||
| handleFieldClick opens modal for client-signature only | Modal opens for client-signature AND initials | Phase 10 | Clients can draw initials |
|
||||
| POST handler expects signature for every field | POST handler only expects signature for client-signature and initials | Phase 10 | Text/checkbox/date fields no longer cause 500 errors at submission |
|
||||
|
||||
**Deprecated/outdated after Phase 10:**
|
||||
- `page.drawText('Sign Here', ...)` for all sigFields — replaced by type-branched rendering
|
||||
- `if (getFieldType(field) !== 'client-signature') return` in handleFieldClick — initials now also open the modal
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should `type: 'text'` SignatureFieldData entries have any visual treatment in preparePdf()?**
|
||||
- What we know: `textFillData` is the agent's primary mechanism for typed content. `type: 'text'` field markers in `signatureFields` currently have no designated rendering path.
|
||||
- What's unclear: Whether FIELD-01 intends the text marker to be a coordinate-positioned stamp (separate from textFillData) or just a visual indicator of where text content lives.
|
||||
- Recommendation: For Phase 10, treat `type: 'text'` fields as visual-only markers. Draw a light background rectangle at the coordinates (no text content, since textFillData is the source of text). This avoids needing a `value` property on `SignatureFieldData`.
|
||||
|
||||
2. **Should the date be stamped at prepare time or signing time?**
|
||||
- What we know: FIELD-04 says "auto-fill with the signing date." Architecturally, `preparePdf()` runs before the client signs; the actual signing date is captured at POST time in `route.ts`.
|
||||
- What's unclear: "Signing date" — does this mean the date the document was prepared (close enough for real estate contracts) or the exact date/time the client submitted?
|
||||
- Recommendation: Stamp at POST time in `/api/sign/[token]/route.ts`. The implementation stamps the current date as text directly onto the prepared PDF using pdf-lib BEFORE calling `embedSignatureInPdf()`. This requires reading the preparedFilePath, loading it, stamping date text at each `date` field coordinate, and writing back a "date-stamped" PDF that then goes into `embedSignatureInPdf()`. Alternatively — stamp at prepare time and accept that the date is "prepared date." The simpler approach is prepare-time stamping; choose based on business intent. For a real estate contract, the signing date is the date the client signs.
|
||||
|
||||
3. **Should InitialsModal be a separate component or reuse SignatureModal?**
|
||||
- What we know: `SignatureModal` is an existing component that renders a canvas with `signature_pad` for freehand drawing. The initials UX is identical except the canvas might be smaller and the heading says "Draw your initials."
|
||||
- What's unclear: Whether the canvas size should differ (initials are typically 2-3 characters, not a full signature sweep).
|
||||
- Recommendation: Reuse `SignatureModal` with a `mode` or `title` prop. Add an optional `canvasWidth` prop defaulting to the smaller initials size (e.g. 200px vs 400px). Avoid duplicating the signature_pad wiring.
|
||||
|
||||
4. **Does the `isClientVisibleField()` filter need to change for the signing page GET route?**
|
||||
- What we know: `isClientVisibleField()` returns true for everything except `agent-signature`. Text, checkbox, date, and initials fields all pass through to the client.
|
||||
- What's unclear: Whether the signing page should receive text/checkbox/date fields at all (they're already embedded in the prepared PDF and don't need client interaction).
|
||||
- Recommendation: Do NOT change `isClientVisibleField()`. Instead, handle it in `SigningPageClient.tsx` by only rendering interactive overlays for `client-signature` and `initials`. This keeps the server-side API simple and preserves the field coordinate data for any future preview feature.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts` (codebase inspection 2026-03-21) — `SignatureFieldType` union confirmed to include all 4 new types; `getFieldType()` and `isClientVisibleField()` confirmed as live
|
||||
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/pdf/prepare-document.ts` (codebase inspection 2026-03-21) — generic field loop confirmed; `drawRectangle` and `drawText` confirmed as the only drawing primitives used; import path `@cantoo/pdf-lib` confirmed
|
||||
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` (codebase inspection 2026-03-21) — single `DraggableToken` with id `'signature-token'` confirmed; `handleDragEnd` creates fields without `type` property confirmed; `renderFields` shows generic "Signature" label for all fields confirmed
|
||||
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx` (codebase inspection 2026-03-21) — `handleFieldClick` guards `getFieldType(field) !== 'client-signature'` return; `handleSubmit` filters `client-signature` for count; initials handling explicitly commented "Phase 10 will expand this"
|
||||
- `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/[token]/route.ts` (codebase inspection 2026-03-21) — POST handler line 173 iterates ALL `signatureFields` and throws on missing signature — critical bug for Phase 10
|
||||
- `/Users/ccopeland/temp/red/teressa-copeland-homes/node_modules/@cantoo/pdf-lib/src/api/PDFPage.ts` (source inspection 2026-03-21) — `drawLine`, `drawRectangle`, `drawText`, `drawSvgPath`, `drawLine`, `drawSquare`, `drawCircle` all confirmed as available methods
|
||||
- `/Users/ccopeland/temp/red/teressa-copeland-homes/package.json` (2026-03-21) — @cantoo/pdf-lib 2.6.3, @dnd-kit/core 6.3.1, signature_pad 5.1.3 confirmed in production dependencies; no new packages needed
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- Phase 8 RESEARCH.md (2026-03-21) — architecture decisions about `isClientVisibleField()` scope and Phase 10 expansion points documented; consistent with codebase inspection
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — all libraries confirmed via package.json and node_modules source inspection; zero new dependencies needed
|
||||
- Architecture: HIGH — specific file and line numbers identified from codebase inspection; critical bugs (POST handler, signing overlay rendering) confirmed by reading actual code
|
||||
- Pitfalls: HIGH — all pitfalls derived from reading actual implementation, not speculation
|
||||
|
||||
**Research date:** 2026-03-21
|
||||
**Valid until:** 2026-04-20 (stable stack; no external dependencies that could change)
|
||||
Reference in New Issue
Block a user