# Phase 12.1: Per-Field Text Editing and Quick-Fill — Research

**Researched:** 2026-03-21
**Domain:** React client state management, per-field inline editing, PDF text fill pipeline
**Confidence:** HIGH

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| TXTF-01 | Agent can click a placed text field box on the PDF to select it and type a value for that specific field — each text field holds its own independent value (keyed by field ID) | FieldPlacer already has per-field IDs and click handling infrastructure; need to add selectedFieldId state + inline input overlay |
| TXTF-02 | When a text field is selected, PreparePanel shows quick-fill suggestion buttons (Client Name, Property Address, Client Email) that insert the corresponding value into the selected field | selectedFieldId must be shared between FieldPlacer and PreparePanel via DocumentPageClient; quick-fill buttons call a setValue(fieldId, value) callback |
| TXTF-03 | Text fill values entered per-field appear correctly embedded in the preview PDF and the final prepared PDF; the staleness token resets on any text field value change | textFillData must become a Record<fieldId, string> (keyed by field ID, not label); preparePdf must match by field ID; onPreviewTokenChange(null) called on every value change |
</phase_requirements>

---

## Summary

Phase 12.1 replaces the current broken positional text fill approach (TextFillForm with generic label/value rows) with a per-field model where each placed `text` type field box holds its own independent value keyed by its UUID field ID. The core UX change is: agent clicks a `text` field box on the PDF canvas to select it, sees an inline input overlay, and can type directly. Simultaneously, the PreparePanel sidebar shows quick-fill buttons (Client Name, Property Address, Client Email) that insert the relevant client profile value into the currently selected field.

This phase touches three layers. First, the UI layer: FieldPlacer gains `selectedFieldId` state and per-field value display/editing; DocumentPageClient gains shared state to bridge FieldPlacer (left column) and PreparePanel (right sidebar) — same pattern already established in Phase 12. Second, the data model: `textFillData` transitions from `Record<label, value>` (human-typed keys) to `Record<fieldId, value>` (UUID keys from `SignatureFieldData.id`). Third, the PDF layer: `preparePdf` in `prepare-document.ts` switches from positional sequential assignment to direct field ID lookup when drawing text onto placed `text` field boxes.

The existing infrastructure is almost entirely reusable. The `onFieldsChanged` callback chain through DocumentPageClient already handles previewToken invalidation. The quick-fill data sources (clientName, clientPropertyAddress, defaultEmail) are already passed as props to DocumentPageClient from the server page. No new dependencies are needed.

**Primary recommendation:** Key `textFillData` by `SignatureFieldData.id` (UUID), add `selectedFieldId` state to DocumentPageClient bridging FieldPlacer and PreparePanel, replace TextFillForm with per-field inline edit + quick-fill panel, and update `preparePdf` to look up values by field ID directly.

---

## Standard Stack

### Core (all already installed — no new dependencies)

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| React 19 | 19.2.4 | State management, event handling | Project standard |
| Next.js | 16.2.0 | App Router, server components | Project standard |
| @dnd-kit/core | 6.3.1 | Drag-and-drop already used in FieldPlacer | Project standard |
| @cantoo/pdf-lib | 2.6.3 | PDF text drawing in preparePdf | Project standard |
| TypeScript | 5.x | Type safety for field ID keying | Project standard |

### No New Dependencies Needed

This phase is pure UI and data model refactoring within the existing stack. No new npm packages required.

**Installation:**
```bash
# No new packages — all dependencies already present
```

---

## Architecture Patterns

### Current Architecture (What Exists)

```
page.tsx (server component)
└── DocumentPageClient.tsx (client — holds previewToken, handleFieldsChanged)
    ├── PdfViewerWrapper → PdfViewer → FieldPlacer
    │   └── TextFieldBox overlays (current: grab, resize, delete only)
    └── PreparePanel
        └── TextFillForm (current: generic label/value rows, no field linkage)
```

**Current data flow:**
- `textFillData: Record<string, string>` — keys are human-typed labels (e.g. "PropertyAddress")
- `preparePdf` assigns values positionally: sort text fields by page/y, assign `textFillData` entries in order
- No linkage between a specific field box (by ID) and a specific value

### Target Architecture (What Phase 12.1 Builds)

```
page.tsx (server component)
└── DocumentPageClient.tsx (client — holds previewToken, selectedFieldId, textFillData, handleFieldsChanged)
    ├── PdfViewerWrapper → PdfViewer → FieldPlacer
    │   ├── props: selectedFieldId, textFillData (per-field), onFieldSelect, onFieldValueChange
    │   └── TextFieldBox overlays: click to select, inline input when selected, show value label
    └── PreparePanel
        ├── props: selectedFieldId, onQuickFill (fieldId, value) => void
        └── QuickFillPanel: Client Name / Property Address / Client Email buttons
            (visible only when a text field is selected)
```

**Target data flow:**
- `textFillData: Record<fieldId, string>` — keys are `SignatureFieldData.id` UUIDs
- `preparePdf` looks up `textFields[field.id]` directly — no positional assignment needed
- `selectedFieldId` shared between FieldPlacer and PreparePanel via DocumentPageClient
- Quick-fill inserts `textFillData[selectedFieldId] = clientValue`
- Every value change calls `onPreviewTokenChange(null)` to invalidate Send button

### Pattern 1: State Lifting for Cross-Sibling Communication

**What:** DocumentPageClient already bridges previewToken between FieldPlacer (left) and PreparePanel (right). Phase 12.1 adds two more lifted state variables: `selectedFieldId` and `textFillData`.

**When to use:** When two sibling client components (rendered by a server component) need to share state — established in Phase 12, confirmed correct pattern for this codebase.

**Example (extending DocumentPageClient):**
```typescript
// Source: DocumentPageClient.tsx (Phase 12 pattern, extending for 12.1)
'use client';
import { useState, useCallback } from 'react';

export function DocumentPageClient({ docId, clientName, clientEmail, clientPropertyAddress, ... }) {
  const [previewToken, setPreviewToken] = useState<string | null>(null);
  const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null);
  const [textFillData, setTextFillData] = useState<Record<string, string>>({});

  const handleFieldValueChange = useCallback((fieldId: string, value: string) => {
    setTextFillData(prev => ({ ...prev, [fieldId]: value }));
    setPreviewToken(null); // TXTF-03: reset staleness on any change
  }, []);

  const handleQuickFill = useCallback((fieldId: string, value: string) => {
    setTextFillData(prev => ({ ...prev, [fieldId]: value }));
    setPreviewToken(null);
  }, []);

  // ...pass selectedFieldId, textFillData, onFieldSelect, onFieldValueChange to FieldPlacer chain
  // ...pass selectedFieldId, clientName, clientPropertyAddress, defaultEmail, onQuickFill to PreparePanel
}
```

### Pattern 2: Inline Input Overlay on Field Box Click

**What:** When agent clicks a `text` field box in FieldPlacer, the box renders an `<input>` element instead of just a label. The input is positioned absolutely inside the field box div.

**When to use:** text-type fields only (`getFieldType(field) === 'text'`). Non-text fields (signature, checkbox, date, etc.) are unaffected.

**Key implementation detail:** The field box already uses `onPointerDown` for move initiation. A click (no movement) on a `text` field needs to select it WITHOUT triggering the move handler. The existing pattern uses `data-no-move` attribute and `e.stopPropagation()` for the delete button — the same mechanism works for an input element inside the field box.

```typescript
// Source: FieldPlacer.tsx pattern — data-no-move stops move handler
{fieldType === 'text' && selectedFieldId === field.id ? (
  <input
    data-no-move
    autoFocus
    value={textFillData[field.id] ?? ''}
    onChange={(e) => onFieldValueChange(field.id, e.target.value)}
    onPointerDown={(e) => e.stopPropagation()}
    style={{ /* fill field box, no border styling, transparent bg */ }}
  />
) : (
  // Show the current value as a label, or the field type label if empty
  <span>{textFillData[field.id] || fieldLabel}</span>
)}
```

**Field selection trigger:** A simple click (no drag) on a text field box should set `selectedFieldId`. This requires distinguishing clicks from drags. The existing pointer handling already tracks drag start — if `e.clientX/Y` delta is minimal on pointerup (or zero), it was a click. Simplest approach: add an `onClick` handler to text field boxes that calls `onFieldSelect(field.id)`.

### Pattern 3: Quick-Fill Panel in PreparePanel

**What:** When `selectedFieldId` is non-null (a text field is selected), PreparePanel shows 3 quick-fill buttons. Each button calls `onQuickFill(selectedFieldId, value)` to insert the client data value.

**When to use:** Only rendered when selectedFieldId is non-null AND a text field is selected.

```typescript
// Source: PreparePanel.tsx (new quick-fill section)
{selectedFieldId && (
  <div className="space-y-2">
    <p className="text-xs text-gray-500">Quick-fill selected field:</p>
    <button onClick={() => onQuickFill(selectedFieldId, clientName)} ...>
      Client Name — {clientName}
    </button>
    <button onClick={() => onQuickFill(selectedFieldId, clientPropertyAddress ?? '')} ...>
      Property Address — {clientPropertyAddress ?? 'not set'}
    </button>
    <button onClick={() => onQuickFill(selectedFieldId, clientEmail)} ...>
      Client Email — {clientEmail}
    </button>
  </div>
)}
```

**Data sources already available:** `clientName`, `clientPropertyAddress`, and `defaultEmail` (client email) are all already passed to `DocumentPageClient` from the server page — no new DB queries needed.

### Pattern 4: preparePdf — Field-ID-Keyed Lookup

**What:** Replace the current positional assignment (sort fields, assign entries in order) with direct field ID lookup.

**Current approach (broken — positional):**
```typescript
// prepare-document.ts — current positional assignment
const textFields_sorted = sigFields.filter(f => getFieldType(f) === 'text')
  .sort(...);
textFields_sorted.forEach((field, idx) => {
  const entry = remainingEntries[idx]; // positional — fragile
  page.drawText(entry.value, { x: field.x + 4, y: field.y + 4, ... });
});
```

**New approach (field-ID-keyed — TXTF-03):**
```typescript
// prepare-document.ts — Phase 12.1 target
for (const field of sigFields) {
  if (getFieldType(field) !== 'text') continue;
  const value = textFields[field.id]; // direct lookup by UUID
  if (!value) continue;
  const page = pages[field.page - 1];
  if (!page) continue;
  const fontSize = Math.max(6, Math.min(11, field.height - 4));
  page.drawText(value, { x: field.x + 4, y: field.y + 4, size: fontSize, font: helvetica, color: rgb(0.05, 0.05, 0.05) });
}
```

This is simpler than the current code AND more correct. The `fieldConsumedKeys` tracking set, `remainingEntries` array, and the positional sort can all be removed. Strategy B (top-of-page fallback stamp) also becomes unnecessary since values are always at known field positions.

### Pattern 5: Prop Threading Through PdfViewer Chain

**What:** New props (`selectedFieldId`, `textFillData`, `onFieldSelect`, `onFieldValueChange`) must flow from DocumentPageClient down through PdfViewerWrapper → PdfViewer → FieldPlacer, following the established Phase 12 chain pattern.

**Important:** Keep PdfViewerWrapper and PdfViewer prop changes minimal. Only thread what FieldPlacer needs. Use optional props with defaults where possible to avoid breaking existing usage (e.g., PreviewModal uses PdfViewer in readOnly mode and should not need these new props).

### Anti-Patterns to Avoid

- **Keeping TextFillForm:** The generic label/value form (TextFillForm.tsx) is the UX antipattern being replaced. Remove it from PreparePanel. The new per-field UI in FieldPlacer + QuickFill panel in PreparePanel replaces it entirely.
- **Keying by label instead of field ID:** Do not use human-typed labels as textFillData keys. Keys MUST be UUID field IDs to guarantee correct mapping.
- **Strategy B fallback stamp:** Once textFillData is keyed by field ID, the Strategy B top-of-page fallback is no longer needed. Remove it to avoid stamping confusing UUID-keyed entries at the top of page 1.
- **AcroForm Strategy A:** Keep Strategy A (AcroForm field filling by name) as-is — it is a separate code path that handles PDFs with embedded form fields and does not conflict with the new approach. However, textFillData entries with UUID keys will never match AcroForm field names, so Strategy A will be a no-op for Phase 12.1 data (which is correct).
- **Deselect on page change:** If agent navigates to a different PDF page, selectedFieldId should be cleared (or at least not show the quick-fill panel). The field boxes are only shown for the current page — a selectedFieldId from a different page is invisible.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Cross-sibling state | Custom event bus or Context provider | Lift state to DocumentPageClient | Already established pattern in Phase 12; DocumentPageClient is minimal and correct |
| Input focus management | Custom focus trap | Native `autoFocus` on the inline input | Sufficient for this use case; no modal/focus trap needed |
| Inline input positioning | Absolute-positioned floating popup | Input inside the field box div | The field box is already absolutely positioned; input inside it is naturally in the right place |

**Key insight:** The Phase 12 state bridge pattern (DocumentPageClient as shared state holder) is already the right solution for cross-sibling communication in this Next.js App Router architecture. Don't reach for React Context or a global store.

---

## Common Pitfalls

### Pitfall 1: Move Handler Intercepts Click on Text Field

**What goes wrong:** When agent tries to click a `text` field box to select it, the `handleMoveStart` fires on `onPointerDown` and treats the click as a drag initiation. The field box moves instead of being selected.

**Why it happens:** FieldPlacer uses `onPointerDown` to start move tracking on every field box. The `data-no-move` attribute + `e.stopPropagation()` pattern is used for the delete button and resize handles, but not yet for text selection/input.

**How to avoid:** For `text` type fields, either (a) use `onClick` for selection (fires after pointerdown+pointerup without movement) and rely on the existing minimum-distance drag constraint (MouseSensor has `{ distance: 5 }` activation), or (b) add `data-no-move` + `stopPropagation()` to the input element inside the field box. Option (a) is simpler: a click (delta < 5px) won't trigger move, so `onClick` on the field box for text type fields works cleanly.

**Warning signs:** Field box visually jumps when agent clicks it to select; no input appears.

### Pitfall 2: textFillData Keyed by UUID Breaks Strategy B

**What goes wrong:** If `textFillData` is now `{ "550e8400-e29b-...": "John Smith" }` and Strategy B is not removed, the top-of-page fallback stamps UUID keys as text on page 1 (visible in the PDF as noise).

**Why it happens:** Strategy B is designed for human-readable label/value pairs but doesn't know about field IDs.

**How to avoid:** Remove Strategy B entirely when `textFillData` transitions to field-ID-keyed format. All text values are now rendered at field box positions — there is no fallback needed.

**Warning signs:** Preview PDF shows UUID strings near the top of page 1.

### Pitfall 3: PreparePanel Receives clientEmail as a New Prop

**What goes wrong:** PreparePanel currently receives `defaultEmail` (the recipient email textarea pre-fill) but not a separate `clientEmail` for quick-fill purposes. If these are the same value, reuse `defaultEmail`. However, if they are meant to be structurally separate, a new prop must be threaded.

**Why it happens:** The server page passes `docClient?.email` as `defaultEmail`. The same email is the appropriate quick-fill value for "Client Email". Reuse `defaultEmail` — no new prop needed.

**How to avoid:** In PreparePanel, use the existing `defaultEmail` prop for the "Client Email" quick-fill button. No new data sourcing required.

**Warning signs:** Quick-fill "Client Email" button shows blank or wrong value.

### Pitfall 4: selectedFieldId Points to a Non-text Field

**What goes wrong:** If FieldPlacer calls `onFieldSelect(field.id)` for any field type (not just text), PreparePanel shows the quick-fill panel for signatures, checkboxes, etc.

**Why it happens:** The `onFieldSelect` handler doesn't discriminate by field type.

**How to avoid:** Only set `selectedFieldId` when the clicked/selected field is type `text`. In FieldPlacer: `if (getFieldType(field) === 'text') onFieldSelect?.(field.id)`.

**Warning signs:** Quick-fill panel appears when clicking a blue "Signature" field.

### Pitfall 5: textFillData Schema Mismatch with Existing DB Records

**What goes wrong:** Existing documents in the database have `textFillData` as `Record<label, string>` (e.g., `{ "propertyAddress": "123 Main St" }`). Phase 12.1 changes the client-side data model to `Record<fieldId, string>`. If existing documents are loaded, the old key format might be used.

**Why it happens:** The `documents.textFillData` JSONB column stores whatever was last passed to `/api/documents/[id]/prepare`. Old records have label keys; new records will have UUID keys.

**How to avoid:** Phase 12.1 is a fresh UX for new document preparation sessions. On page load, `textFillData` state in DocumentPageClient should always start as `{}` (empty) — do NOT seed it from `doc.textFillData` from the DB. The DB value is only used at prepare time (already the case). The UI is always a fresh editing session.

**Warning signs:** Old label-keyed values pre-fill into the new per-field inputs incorrectly.

### Pitfall 6: PdfViewer Used in ReadOnly Mode (PreviewModal) Gets New Required Props

**What goes wrong:** PreviewModal renders a PdfViewer in `readOnly` mode. If new props are added as required to PdfViewer/PdfViewerWrapper, the PreviewModal usage breaks TypeScript compilation.

**Why it happens:** Prop threading requires updates to PdfViewer and PdfViewerWrapper signatures.

**How to avoid:** Make all new props optional with `?:` typing and safe defaults. ReadOnly PdfViewer passes nothing for text-edit props; FieldPlacer receives `undefined` and handles gracefully.

**Warning signs:** TypeScript error `Property 'X' is missing` in PreviewModal or PdfViewer.

---

## Code Examples

### Extending DocumentPageClient for Phase 12.1

```typescript
// Source: DocumentPageClient.tsx — Phase 12 base extended for Phase 12.1
'use client';
import { useState, useCallback } from 'react';
import { PdfViewerWrapper } from './PdfViewerWrapper';
import { PreparePanel } from './PreparePanel';

interface DocumentPageClientProps {
  docId: string;
  docStatus: string;
  defaultEmail: string;
  clientName: string;
  agentDownloadUrl?: string | null;
  signedAt?: Date | null;
  clientPropertyAddress?: string | null;
}

export function DocumentPageClient({
  docId, docStatus, defaultEmail, clientName,
  agentDownloadUrl, signedAt, clientPropertyAddress,
}: DocumentPageClientProps) {
  const [previewToken, setPreviewToken] = useState<string | null>(null);
  const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null);
  const [textFillData, setTextFillData] = useState<Record<string, string>>({});

  const handleFieldsChanged = useCallback(() => {
    setPreviewToken(null);
  }, []);

  const handleFieldValueChange = useCallback((fieldId: string, value: string) => {
    setTextFillData(prev => ({ ...prev, [fieldId]: value }));
    setPreviewToken(null);
  }, []);

  const handleQuickFill = useCallback((fieldId: string, value: string) => {
    setTextFillData(prev => ({ ...prev, [fieldId]: value }));
    setPreviewToken(null);
  }, []);

  return (
    <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
      <div className="lg:col-span-2">
        <PdfViewerWrapper
          docId={docId}
          docStatus={docStatus}
          onFieldsChanged={handleFieldsChanged}
          selectedFieldId={selectedFieldId}
          textFillData={textFillData}
          onFieldSelect={setSelectedFieldId}
          onFieldValueChange={handleFieldValueChange}
        />
      </div>
      <div className="lg:col-span-1 lg:sticky lg:top-6 lg:self-start ...">
        <PreparePanel
          docId={docId}
          defaultEmail={defaultEmail}
          clientName={clientName}
          currentStatus={docStatus}
          agentDownloadUrl={agentDownloadUrl}
          signedAt={signedAt}
          clientPropertyAddress={clientPropertyAddress}
          previewToken={previewToken}
          onPreviewTokenChange={setPreviewToken}
          textFillData={textFillData}
          selectedFieldId={selectedFieldId}
          onQuickFill={handleQuickFill}
        />
      </div>
    </div>
  );
}
```

### FieldPlacer Text Field Box — Click-to-Select with Inline Input

```typescript
// Source: FieldPlacer.tsx renderFields() — text field rendering

// Inside renderFields() per-field .map():
const fieldType = getFieldType(field);
const isSelected = selectedFieldId === field.id;
const currentValue = textFillData?.[field.id] ?? '';

// Text fields: click to select, show inline input when selected
if (fieldType === 'text') {
  return (
    <div
      key={field.id}
      data-field-id={field.id}
      style={{ /* existing positioning styles */ border: `2px solid ${fieldColor}`, ... }}
      onClick={(e) => {
        // onClick fires after pointerdown+pointerup with no movement (< 5px — MouseSensor threshold)
        // This selects the field; move handler does not fire for clicks within threshold
        onFieldSelect?.(field.id);
      }}
      onPointerDown={(e) => {
        if ((e.target as HTMLElement).closest('[data-no-move]')) return;
        handleMoveStart(e, field.id);
      }}
    >
      {isSelected ? (
        <input
          data-no-move
          autoFocus
          value={currentValue}
          onChange={(e) => onFieldValueChange?.(field.id, e.target.value)}
          onPointerDown={(e) => e.stopPropagation()}
          style={{
            flex: 1,
            background: 'transparent',
            border: 'none',
            outline: 'none',
            fontSize: '10px',
            color: fieldColor,
            width: '100%',
            cursor: 'text',
          }}
          placeholder="Type value..."
        />
      ) : (
        <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {currentValue || fieldLabel}
        </span>
      )}
      {/* delete button and resize handles unchanged */}
    </div>
  );
}
// Non-text fields: existing rendering unchanged
```

### preparePdf — Field-ID-Keyed Text Drawing

```typescript
// Source: prepare-document.ts — replace positional assignment with ID lookup

// BEFORE Phase 12.1 (remove this entire block):
// const remainingEntries = ...
// const textFields_sorted = sigFields.filter(f => getFieldType(f) === 'text').sort(...)
// const fieldConsumedKeys = new Set<string>()
// textFields_sorted.forEach((field, idx) => { ... })

// AFTER Phase 12.1 (simple direct lookup):
for (const field of sigFields) {
  if (getFieldType(field) !== 'text') continue;
  const value = textFields[field.id]; // textFields is now Record<fieldId, string>
  if (!value) continue;
  const page = pages[field.page - 1];
  if (!page) continue;
  const fontSize = Math.max(6, Math.min(11, field.height - 4));
  page.drawText(value, {
    x: field.x + 4,
    y: field.y + 4,
    size: fontSize,
    font: helvetica,
    color: rgb(0.05, 0.05, 0.05),
  });
}

// Also remove Strategy B (unstampedEntries block) — no longer needed with field-ID keying
```

### PreparePanel Quick-Fill Panel

```typescript
// Source: PreparePanel.tsx — replace TextFillForm with quick-fill panel

// Props additions:
interface PreparePanelProps {
  // ... existing props ...
  textFillData: Record<string, string>;
  selectedFieldId: string | null;
  onQuickFill: (fieldId: string, value: string) => void;
}

// Replace the TextFillForm section with:
{selectedFieldId ? (
  <div className="space-y-2">
    <p className="text-sm font-medium text-gray-700">Quick-fill selected field</p>
    <p className="text-xs text-gray-400">
      Click a suggestion to insert into the selected text field.
    </p>
    {clientName && (
      <button
        type="button"
        onClick={() => onQuickFill(selectedFieldId, clientName)}
        className="w-full text-left px-3 py-2 text-sm border rounded bg-white hover:bg-blue-50 hover:border-blue-300"
      >
        <span className="text-xs text-gray-400 block">Client Name</span>
        {clientName}
      </button>
    )}
    {clientPropertyAddress && (
      <button
        type="button"
        onClick={() => onQuickFill(selectedFieldId, clientPropertyAddress)}
        className="w-full text-left px-3 py-2 text-sm border rounded bg-white hover:bg-blue-50 hover:border-blue-300"
      >
        <span className="text-xs text-gray-400 block">Property Address</span>
        {clientPropertyAddress}
      </button>
    )}
    <button
      type="button"
      onClick={() => onQuickFill(selectedFieldId, defaultEmail)}
      className="w-full text-left px-3 py-2 text-sm border rounded bg-white hover:bg-blue-50 hover:border-blue-300"
    >
      <span className="text-xs text-gray-400 block">Client Email</span>
      {defaultEmail}
    </button>
  </div>
) : (
  <p className="text-sm text-gray-400 italic">
    Click a text field on the document to edit its value or use quick-fill.
  </p>
)}
```

### handlePreview and handlePrepare Pass textFillData to API

```typescript
// Source: PreparePanel.tsx — handlePreview and handlePrepare now use textFillData prop

async function handlePreview() {
  // textFillData is now Record<fieldId, string> — passed directly to preview API
  const res = await fetch(`/api/documents/${docId}/preview`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ textFillData }), // textFillData is now a prop, not local state
  });
  // ... rest unchanged
}

async function handlePrepare() {
  const res = await fetch(`/api/documents/${docId}/prepare`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ textFillData, emailAddresses }), // same
  });
  // ... rest unchanged
}
```

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Generic label/value TextFillForm rows | Per-field click-to-edit on placed field boxes | Phase 12.1 | Agent knows exactly which field gets which value |
| `textFillData: Record<label, string>` | `textFillData: Record<fieldId, string>` | Phase 12.1 | One-to-one, guaranteed mapping; no positional fragility |
| Positional sequential assignment in preparePdf | Direct field-ID lookup in preparePdf | Phase 12.1 | Simpler code, correct for any number/order of fields |
| Strategy B top-of-page fallback stamp | Removed | Phase 12.1 | No UUID noise at top of page 1 |

**Deprecated/outdated:**
- `TextFillForm.tsx` component: Replaced by per-field inline editing in FieldPlacer + quick-fill in PreparePanel. Can be deleted after Phase 12.1.
- `textFillData` local state in PreparePanel: Moved to DocumentPageClient as shared state. PreparePanel receives it as a prop.
- `fieldConsumedKeys` / `remainingEntries` / `textFields_sorted` blocks in `prepare-document.ts`: Replaced by simple field-ID keyed loop.
- Strategy B (`unstampedEntries` block) in `prepare-document.ts`: Removed — no longer needed.

---

## Open Questions

1. **Deselect behavior when clicking outside a text field**
   - What we know: There is no global click handler to deselect `selectedFieldId`.
   - What's unclear: Should clicking the PDF background, clicking a non-text field, or scrolling the page deselect? Clicking a non-text field probably should not show quick-fill buttons.
   - Recommendation: Add an `onClick` handler on the DroppableZone that calls `onFieldSelect(null)` when the click target is NOT a `[data-field-id]` element. This gives a natural "click off" deselect. Non-text field clicks should call `onFieldSelect(null)` as well.

2. **What happens to the existing clientPropertyAddress pre-seeding in PreparePanel**
   - What we know: PreparePanel currently pre-seeds `textFillData` state with `{ propertyAddress: clientPropertyAddress }`. This becomes moot when textFillData moves to DocumentPageClient and uses field IDs as keys.
   - What's unclear: Should DocumentPageClient pre-populate any field with the property address on load? There is no way to know which field ID the agent intends for property address until they place the field.
   - Recommendation: Do NOT pre-populate. The quick-fill panel makes it trivial to insert the value. Pre-populating with an unknown field ID would be incorrect. Remove the pre-seeding logic.

3. **Multiple text fields of the same visual "type"**
   - What we know: A document may have 10 placed text field boxes for 10 different form fields (names, prices, dates-as-text, etc.). Each gets a unique UUID.
   - What's unclear: Agent needs to know which field is which — there are no labels on text field boxes beyond "Text".
   - Recommendation: Out of scope for Phase 12.1. The inline click-to-edit interaction provides context through position on the document. Labels/field naming is a future enhancement. Phase 13 AI pre-fill will address this more directly.

---

## Files to Change

Based on the research, these are the exact files the planner should task:

| File | Change Type | What Changes |
|------|-------------|--------------|
| `DocumentPageClient.tsx` | Modify | Add `selectedFieldId`, `textFillData` state; add `handleFieldValueChange`, `handleQuickFill` callbacks; thread new props to PdfViewerWrapper and PreparePanel |
| `PdfViewerWrapper.tsx` | Modify | Add `selectedFieldId?`, `textFillData?`, `onFieldSelect?`, `onFieldValueChange?` optional props; pass through to PdfViewer |
| `PdfViewer.tsx` | Modify | Same optional props as above; pass through to FieldPlacer |
| `FieldPlacer.tsx` | Modify | Add `selectedFieldId?`, `textFillData?`, `onFieldSelect?`, `onFieldValueChange?` optional props; change text field box rendering to click-to-select + inline input; call `onFieldSelect(null)` on DroppableZone background click |
| `PreparePanel.tsx` | Modify | Remove TextFillForm usage; add `textFillData`, `selectedFieldId`, `onQuickFill` props; add QuickFillPanel; `textFillData` is now a prop (not local state); handlePreview/handlePrepare use prop |
| `TextFillForm.tsx` | Delete (or retain unused) | No longer used in PreparePanel; can be deleted |
| `prepare-document.ts` | Modify | Replace positional text drawing with field-ID direct lookup; remove `fieldConsumedKeys`/`remainingEntries`/`textFields_sorted`; remove Strategy B; `textFields` param is now `Record<fieldId, string>` |

**Files NOT changing:**
- `route.ts` (prepare): Passes `body.textFillData` to `preparePdf` — already works with any `Record<string, string>`; no change needed
- `route.ts` (preview): Same — no change needed
- `schema.ts`: `textFillData` JSONB column type is already `Record<string, string>` — still correct
- `page.tsx`: No change — DocumentPageClient props are unchanged at this level
- Any signing/client routes: No change

---

## Sources

### Primary (HIGH confidence)
- Direct code reading: `FieldPlacer.tsx`, `PreparePanel.tsx`, `DocumentPageClient.tsx`, `TextFillForm.tsx`, `prepare-document.ts`, `schema.ts`, `prepare/route.ts`, `preview/route.ts` — all read in full
- `12-02-SUMMARY.md` — Phase 12 decisions and patterns confirmed
- `STATE.md` — Confirmed Phase 12.1 gap deferred from Phase 12
- `REQUIREMENTS.md` — TXTF-01, TXTF-02, TXTF-03 requirements confirmed
- `package.json` — Stack versions verified

### Secondary (MEDIUM confidence)
- Next.js 16 (App Router) patterns: verified against project's own AGENTS.md guidance and existing DocumentPageClient established pattern
- `@dnd-kit/core` MouseSensor `{ distance: 5 }` activation constraint (confirmed in FieldPlacer.tsx line 190) — ensures clicks do not trigger drag

### Tertiary (LOW confidence)
- None — all findings grounded in direct source code reading

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — no new dependencies; all from direct package.json read
- Architecture: HIGH — all patterns derived from existing codebase; Phase 12 bridge pattern directly applicable
- Pitfalls: HIGH — identified from direct analysis of current code paths; not hypothetical

**Research date:** 2026-03-21
**Valid until:** 2026-04-21 (stable codebase; only this phase changes these files)
