From eaf377d97d6cb8c5f38f2cab72f32418dbf53e73 Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Sat, 21 Mar 2026 16:22:02 -0600 Subject: [PATCH] feat(12.1-01): add optional text-edit props and click-to-select interaction - PdfViewerWrapper: add selectedFieldId?, textFillData?, onFieldSelect?, onFieldValueChange? and forward to PdfViewer - PdfViewer: add same 4 optional props, forward to FieldPlacer; call onFieldSelect?.(null) on page navigation - FieldPlacer: extend FieldPlacerProps with 4 new optional props - DroppableZone: add optional onClick prop for background deselect - renderFields: text fields show inline input when selected (isSelected), value/label otherwise - Per-field onClick: text fields call onFieldSelect(id), non-text fields call onFieldSelect(null) - Cursor updated to 'text' for text field type; boxShadow ring on selected state --- .../[docId]/_components/FieldPlacer.tsx | 65 +++++++++++++++++-- .../[docId]/_components/PdfViewer.tsx | 34 ++++++++-- .../[docId]/_components/PdfViewerWrapper.tsx | 30 ++++++++- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx index 7f59134..43340dd 100644 --- a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx +++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx @@ -123,12 +123,14 @@ function DroppableZone({ children, onZonePointerMove, onZonePointerUp, + onClick, }: { id: string; containerRef: React.RefObject; children: React.ReactNode; onZonePointerMove: (e: React.PointerEvent) => void; onZonePointerUp: (e: React.PointerEvent) => void; + onClick?: (e: React.MouseEvent) => void; }) { const { setNodeRef } = useDroppable({ id }); @@ -143,6 +145,7 @@ function DroppableZone({ style={{ position: 'relative' }} onPointerMove={onZonePointerMove} onPointerUp={onZonePointerUp} + onClick={onClick} > {children} @@ -156,9 +159,13 @@ interface FieldPlacerProps { children: React.ReactNode; readOnly?: boolean; onFieldsChanged?: () => void; + selectedFieldId?: string | null; + textFillData?: Record; + onFieldSelect?: (fieldId: string | null) => void; + onFieldValueChange?: (fieldId: string, value: string) => void; } -export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged }: FieldPlacerProps) { +export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged, selectedFieldId, textFillData, onFieldSelect, onFieldValueChange }: FieldPlacerProps) { const [fields, setFields] = useState([]); const [isDraggingToken, setIsDraggingToken] = useState(null); const containerRef = useRef(null); @@ -566,6 +573,8 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = const heightPx = (field.height / pageInfo.originalHeight) * renderedH; const isMoving = activeDragFieldId === field.id && activeDragType === 'move'; + const isSelected = selectedFieldId === field.id; + const currentValue = textFillData?.[field.id] ?? ''; // Per-type color and label const fieldType = getFieldType(field); @@ -632,8 +641,10 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = zIndex: 10, pointerEvents: readOnly ? 'none' : 'all', boxSizing: 'border-box', - cursor: readOnly ? 'default' : (isMoving ? 'grabbing' : 'grab'), - boxShadow: isMoving ? `0 4px 12px ${fieldColor}59` : undefined, + cursor: readOnly ? 'default' : (isMoving ? 'grabbing' : (fieldType === 'text' ? 'text' : 'grab')), + boxShadow: isSelected && fieldType === 'text' + ? `0 0 0 2px ${fieldColor}66${isMoving ? `, 0 4px 12px ${fieldColor}59` : ''}` + : (isMoving ? `0 4px 12px ${fieldColor}59` : undefined), userSelect: 'none', touchAction: 'none', opacity: readOnly ? 0.6 : 1, @@ -644,9 +655,47 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = if ((e.target as HTMLElement).closest('[data-no-move]')) return; handleMoveStart(e, field.id); }} + onClick={(e) => { + if (readOnly) return; + if (getFieldType(field) === 'text') { + e.stopPropagation(); // prevent DroppableZone's deselect handler from firing + onFieldSelect?.(field.id); + } else { + // Non-text field click: deselect any selected text field + onFieldSelect?.(null); + } + }} > - {fieldType !== 'checkbox' && ( - {fieldLabel} + {fieldType === 'text' ? ( + 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', + padding: 0, + }} + placeholder="Type value..." + /> + ) : ( + + {currentValue || fieldLabel} + + ) + ) : ( + fieldType !== 'checkbox' && ( + {fieldLabel} + ) )} {!readOnly && ( {pageNumber} / {numPages || '?'}