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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user