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,
|
type DragEndEvent,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { getFieldType, type SignatureFieldType } from '@/lib/db/schema';
|
||||||
import type { SignatureFieldData } from '@/lib/db/schema';
|
import type { SignatureFieldData } from '@/lib/db/schema';
|
||||||
|
|
||||||
interface PageInfo {
|
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
|
// 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 { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id });
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
// No transform — DragOverlay handles the ghost. Applying transform here causes snap-back animation.
|
// No transform — DragOverlay handles the ghost. Applying transform here causes snap-back animation.
|
||||||
opacity: isDragging ? 0.4 : 1,
|
opacity: isDragging ? 0.4 : 1,
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
padding: '6px 12px',
|
padding: '6px 12px',
|
||||||
border: '2px dashed #2563eb',
|
border: `2px dashed ${color}`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
background: 'rgba(37,99,235,0.08)',
|
background: `${color}14`, // ~8% opacity tint
|
||||||
color: '#2563eb',
|
color,
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
@@ -85,7 +95,7 @@ function DraggableToken({ id }: { id: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||||
+ Signature Field
|
+ {label}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -147,7 +157,7 @@ interface FieldPlacerProps {
|
|||||||
|
|
||||||
export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false }: FieldPlacerProps) {
|
export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false }: FieldPlacerProps) {
|
||||||
const [fields, setFields] = useState<SignatureFieldData[]>([]);
|
const [fields, setFields] = useState<SignatureFieldData[]>([]);
|
||||||
const [isDraggingToken, setIsDraggingToken] = useState(false);
|
const [isDraggingToken, setIsDraggingToken] = useState<string | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
// Track rendered canvas dimensions in state so renderFields re-runs when they change
|
// Track rendered canvas dimensions in state so renderFields re-runs when they change
|
||||||
const [containerSize, setContainerSize] = useState<{ w: number; h: number } | null>(null);
|
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(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
setIsDraggingToken(false);
|
setIsDraggingToken(null);
|
||||||
|
|
||||||
if (readOnly) return;
|
if (readOnly) return;
|
||||||
|
|
||||||
@@ -661,7 +671,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
|||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
onDragStart={() => setIsDraggingToken(true)}
|
onDragStart={(event) => setIsDraggingToken(event.active.id as string)}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
{/* Palette — hidden in read-only mode */}
|
{/* 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>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -696,26 +708,30 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
|||||||
|
|
||||||
{/* Ghost overlay during drag */}
|
{/* Ghost overlay during drag */}
|
||||||
<DragOverlay dropAnimation={null}>
|
<DragOverlay dropAnimation={null}>
|
||||||
{isDraggingToken ? (
|
{isDraggingToken ? (() => {
|
||||||
<div
|
const tokenMeta = PALETTE_TOKENS.find((t) => t.id === isDraggingToken);
|
||||||
style={{
|
const label = tokenMeta?.label ?? 'Field';
|
||||||
width: 144,
|
const color = tokenMeta?.color ?? '#2563eb';
|
||||||
height: 36,
|
const isCheckbox = isDraggingToken === 'checkbox';
|
||||||
border: '2px solid #2563eb',
|
return (
|
||||||
background: 'rgba(37,99,235,0.15)',
|
<div style={{
|
||||||
|
width: isCheckbox ? 24 : 144,
|
||||||
|
height: isCheckbox ? 24 : 36,
|
||||||
|
border: `2px solid ${color}`,
|
||||||
|
background: `${color}26`,
|
||||||
borderRadius: '2px',
|
borderRadius: '2px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
color: '#2563eb',
|
color,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}>
|
||||||
>
|
{!isCheckbox && label}
|
||||||
Signature
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
);
|
||||||
|
})() : null}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user