feat(16-03): active signer selector, per-signer field coloring, unassigned field red highlight
This commit is contained in:
@@ -171,6 +171,7 @@ interface FieldPlacerProps {
|
||||
export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged, selectedFieldId, textFillData, onFieldSelect, onFieldValueChange, aiPlacementKey = 0, signers = [], unassignedFieldIds = new Set() }: FieldPlacerProps) {
|
||||
const [fields, setFields] = useState<SignatureFieldData[]>([]);
|
||||
const [isDraggingToken, setIsDraggingToken] = useState<string | null>(null);
|
||||
const [activeSignerEmail, setActiveSignerEmail] = useState<string | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
// Track rendered canvas dimensions in state so renderFields re-runs when they change
|
||||
const [containerSize, setContainerSize] = useState<{ w: number; h: number } | null>(null);
|
||||
@@ -194,6 +195,18 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
useEffect(() => { containerSizeRef.current = containerSize; }, [containerSize]);
|
||||
useEffect(() => { canvasOffsetRef.current = canvasOffset; }, [canvasOffset]);
|
||||
|
||||
// Default active signer to first signer on load; keep current if still valid (D-08)
|
||||
useEffect(() => {
|
||||
if (signers && signers.length > 0) {
|
||||
setActiveSignerEmail(prev => {
|
||||
if (prev && signers.some(s => s.email === prev)) return prev;
|
||||
return signers[0].email;
|
||||
});
|
||||
} else {
|
||||
setActiveSignerEmail(null);
|
||||
}
|
||||
}, [signers]);
|
||||
|
||||
// Configure sensors: require a minimum drag distance so clicks on delete buttons
|
||||
// are not intercepted as drag starts
|
||||
const sensors = useSensors(
|
||||
@@ -301,6 +314,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
width: fieldW,
|
||||
height: fieldH,
|
||||
type: droppedType,
|
||||
...(activeSignerEmail ? { signerEmail: activeSignerEmail } : {}),
|
||||
};
|
||||
|
||||
const next = [...fields, newField];
|
||||
@@ -308,7 +322,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
persistFields(docId, next);
|
||||
onFieldsChanged?.();
|
||||
},
|
||||
[fields, pageInfo, currentPage, docId, readOnly, onFieldsChanged],
|
||||
[fields, pageInfo, currentPage, docId, readOnly, onFieldsChanged, activeSignerEmail],
|
||||
);
|
||||
|
||||
// --- Move / Resize pointer handlers (event delegation on DroppableZone) ---
|
||||
@@ -583,7 +597,18 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
// Per-type color and label
|
||||
const fieldType = getFieldType(field);
|
||||
const tokenMeta = PALETTE_TOKENS.find((t) => t.id === fieldType);
|
||||
const fieldColor = tokenMeta?.color ?? '#2563eb';
|
||||
|
||||
// Color override: signer color when signerEmail is set (D-06), else type color (D-07)
|
||||
let fieldColor = tokenMeta?.color ?? '#2563eb';
|
||||
if (field.signerEmail && signers) {
|
||||
const matchedSigner = signers.find(s => s.email === field.signerEmail);
|
||||
if (matchedSigner) {
|
||||
fieldColor = matchedSigner.color;
|
||||
}
|
||||
}
|
||||
|
||||
// Validation overlay: unassigned field highlight (D-09/D-10)
|
||||
const isUnassigned = unassignedFieldIds?.has(field.id) ?? false;
|
||||
const fieldLabel = tokenMeta?.label ?? 'Signature';
|
||||
|
||||
// Resize handle style factory
|
||||
@@ -632,8 +657,8 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
top: top - heightPx + canvasOffset.y, // top is y of bottom-left corner; shift up by height
|
||||
width: widthPx,
|
||||
height: heightPx,
|
||||
border: `2px solid ${fieldColor}`,
|
||||
background: readOnly ? `${fieldColor}0d` : `${fieldColor}1a`,
|
||||
border: isUnassigned ? '2px solid #ef4444' : `2px solid ${fieldColor}`,
|
||||
background: isUnassigned ? '#ef444414' : (readOnly ? `${fieldColor}0d` : `${fieldColor}1a`),
|
||||
borderRadius: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -754,6 +779,53 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
onDragStart={(event) => setIsDraggingToken(event.active.id as string)}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{/* Active signer selector — only when signers configured and not read-only (D-05) */}
|
||||
{!readOnly && signers && signers.length > 0 && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '8px',
|
||||
padding: '8px 12px',
|
||||
background: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '6px',
|
||||
}}>
|
||||
<span style={{ fontSize: '12px', color: '#6b7280', fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||
Active signer:
|
||||
</span>
|
||||
<select
|
||||
value={activeSignerEmail ?? ''}
|
||||
onChange={(e) => setActiveSignerEmail(e.target.value)}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: '32px',
|
||||
border: '1px solid #D1D5DB',
|
||||
borderRadius: '0.375rem',
|
||||
fontSize: '0.875rem',
|
||||
padding: '0 8px',
|
||||
background: 'white',
|
||||
}}
|
||||
>
|
||||
{signers.map(s => (
|
||||
<option key={s.email} value={s.email}>
|
||||
{s.email}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Color indicator dot next to the dropdown */}
|
||||
{(() => {
|
||||
const activeSigner = signers.find(s => s.email === activeSignerEmail);
|
||||
return activeSigner ? (
|
||||
<span style={{
|
||||
width: '8px', height: '8px', borderRadius: '50%',
|
||||
backgroundColor: activeSigner.color, flexShrink: 0,
|
||||
}} />
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Palette — hidden in read-only mode */}
|
||||
{!readOnly && (
|
||||
<div
|
||||
@@ -797,20 +869,25 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
{isDraggingToken ? (() => {
|
||||
const tokenMeta = PALETTE_TOKENS.find((t) => t.id === isDraggingToken);
|
||||
const label = tokenMeta?.label ?? 'Field';
|
||||
const color = tokenMeta?.color ?? '#2563eb';
|
||||
// Ghost uses active signer color if one is selected, else type color (D-06)
|
||||
let ghostColor = tokenMeta?.color ?? '#2563eb';
|
||||
if (activeSignerEmail && signers) {
|
||||
const activeSigner = signers.find(s => s.email === activeSignerEmail);
|
||||
if (activeSigner) ghostColor = activeSigner.color;
|
||||
}
|
||||
const isCheckbox = isDraggingToken === 'checkbox';
|
||||
return (
|
||||
<div style={{
|
||||
width: isCheckbox ? 24 : 144,
|
||||
height: isCheckbox ? 24 : 36,
|
||||
border: `2px solid ${color}`,
|
||||
background: `${color}26`,
|
||||
border: `2px solid ${ghostColor}`,
|
||||
background: `${ghostColor}26`,
|
||||
borderRadius: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '11px',
|
||||
color,
|
||||
color: ghostColor,
|
||||
fontWeight: 600,
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
|
||||
Reference in New Issue
Block a user