feat(10-01): parameterize DraggableToken and add four new palette tokens

- Add PALETTE_TOKENS array with 5 typed tokens (Signature/blue, Initials/purple, Checkbox/green, Date/amber, Text/slate)
- Update DraggableToken to accept id, label, color props with per-type styling
- Change isDraggingToken from boolean to string | null to track active token id
- Update onDragStart to record active.id instead of just true
- Replace single static token with PALETTE_TOKENS.map() in palette JSX
- Update DragOverlay ghost to show correct label, color, and checkbox-appropriate dimensions (24x24 vs 144x36)
This commit is contained in:
Chandler Copeland
2026-03-21 12:49:25 -06:00
parent 7510c8ee08
commit 4140c220b1

View File

@@ -12,6 +12,7 @@ import {
type DragEndEvent,
} from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
import { getFieldType, type SignatureFieldType } from '@/lib/db/schema';
import type { SignatureFieldData } from '@/lib/db/schema';
interface PageInfo {
@@ -65,18 +66,27 @@ async function persistFields(docId: string, fields: SignatureFieldData[]) {
}
}
// Token color palette — each maps to a SignatureFieldType
const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: string }> = [
{ id: 'client-signature', label: 'Signature', color: '#2563eb' }, // blue
{ id: 'initials', label: 'Initials', color: '#7c3aed' }, // purple
{ id: 'checkbox', label: 'Checkbox', color: '#059669' }, // green
{ id: 'date', label: 'Date', color: '#d97706' }, // amber
{ id: 'text', label: 'Text', color: '#64748b' }, // slate
];
// Draggable token in the palette
function DraggableToken({ id }: { id: string }) {
function DraggableToken({ id, label, color }: { id: string; label: string; color: string }) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id });
const style: React.CSSProperties = {
// No transform — DragOverlay handles the ghost. Applying transform here causes snap-back animation.
opacity: isDragging ? 0.4 : 1,
cursor: 'grab',
padding: '6px 12px',
border: '2px dashed #2563eb',
border: `2px dashed ${color}`,
borderRadius: '4px',
background: 'rgba(37,99,235,0.08)',
color: '#2563eb',
background: `${color}14`, // ~8% opacity tint
color,
fontSize: '13px',
fontWeight: 600,
userSelect: 'none',
@@ -85,7 +95,7 @@ function DraggableToken({ id }: { id: string }) {
return (
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
+ Signature Field
+ {label}
</div>
);
}
@@ -147,7 +157,7 @@ interface FieldPlacerProps {
export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false }: FieldPlacerProps) {
const [fields, setFields] = useState<SignatureFieldData[]>([]);
const [isDraggingToken, setIsDraggingToken] = useState(false);
const [isDraggingToken, setIsDraggingToken] = 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);
@@ -218,7 +228,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
setIsDraggingToken(false);
setIsDraggingToken(null);
if (readOnly) return;
@@ -661,7 +671,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
return (
<DndContext
sensors={sensors}
onDragStart={() => setIsDraggingToken(true)}
onDragStart={(event) => setIsDraggingToken(event.active.id as string)}
onDragEnd={handleDragEnd}
>
{/* Palette — hidden in read-only mode */}
@@ -679,7 +689,9 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
}}
>
<span style={{ fontSize: '12px', color: '#64748b', fontWeight: 500 }}>Field Palette:</span>
<DraggableToken id="signature-token" />
{PALETTE_TOKENS.map((token) => (
<DraggableToken key={token.id} id={token.id} label={token.label} color={token.color} />
))}
</div>
)}
@@ -696,26 +708,30 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
{/* Ghost overlay during drag */}
<DragOverlay dropAnimation={null}>
{isDraggingToken ? (
<div
style={{
width: 144,
height: 36,
border: '2px solid #2563eb',
background: 'rgba(37,99,235,0.15)',
{isDraggingToken ? (() => {
const tokenMeta = PALETTE_TOKENS.find((t) => t.id === isDraggingToken);
const label = tokenMeta?.label ?? 'Field';
const color = tokenMeta?.color ?? '#2563eb';
const isCheckbox = isDraggingToken === 'checkbox';
return (
<div style={{
width: isCheckbox ? 24 : 144,
height: isCheckbox ? 24 : 36,
border: `2px solid ${color}`,
background: `${color}26`,
borderRadius: '2px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '11px',
color: '#2563eb',
color,
fontWeight: 600,
pointerEvents: 'none',
}}
>
Signature
}}>
{!isCheckbox && label}
</div>
) : null}
);
})() : null}
</DragOverlay>
</DndContext>
);