>({});
+
+ 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 (
+
+ );
+}
+```
+
+### 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 (
+ {
+ // 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 ? (
+ 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..."
+ />
+ ) : (
+
+ {currentValue || fieldLabel}
+
+ )}
+ {/* delete button and resize handles unchanged */}
+
+ );
+}
+// 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()
+// 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
+ 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;
+ selectedFieldId: string | null;
+ onQuickFill: (fieldId: string, value: string) => void;
+}
+
+// Replace the TextFillForm section with:
+{selectedFieldId ? (
+
+
Quick-fill selected field
+
+ Click a suggestion to insert into the selected text field.
+
+ {clientName && (
+
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"
+ >
+ Client Name
+ {clientName}
+
+ )}
+ {clientPropertyAddress && (
+
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"
+ >
+ Property Address
+ {clientPropertyAddress}
+
+ )}
+
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"
+ >
+ Client Email
+ {defaultEmail}
+
+
+) : (
+
+ Click a text field on the document to edit its value or use quick-fill.
+
+)}
+```
+
+### 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 — 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` | `textFillData: Record` | 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` |
+
+**Files NOT changing:**
+- `route.ts` (prepare): Passes `body.textFillData` to `preparePdf` — already works with any `Record`; no change needed
+- `route.ts` (preview): Same — no change needed
+- `schema.ts`: `textFillData` JSONB column type is already `Record` — 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)