>({});
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)