Corkboard UI Implementation
The corkboard UI is the signature feature of JARVIS—a cinematic intelligence dashboard inspired by Call of Duty mission briefings. Documents spawn in with physics-based animations, red strings connect related people, and everything updates in real-time as agents gather intelligence.
Visual Design
Cork Texture Background
The Corkboard.tsx component renders a realistic cork texture:
// Cork background with subtle texture
< div style = {{
background : "linear-gradient(145deg, #c5a87a 0%, #b89968 40%, #ab8f5f 100%)" ,
backgroundImage : `
radial-gradient(circle at 20% 30%, rgba(0,0,0,0.05) 1px, transparent 1px),
radial-gradient(circle at 60% 70%, rgba(0,0,0,0.05) 1px, transparent 1px)
` ,
backgroundSize : "50px 50px, 80px 80px" ,
}} >
{ /* Pin holes and texture details */ }
</ div >
Paper Document Styles
Three paper gradient styles create visual variety:
const PAPERS = [
// Warm beige paper
{ bg: "linear-gradient(155deg,#f5e6d0,#ede0cc 40%,#e8d6be)" , bd: "#c9b89a" },
// Cool gray paper
{ bg: "linear-gradient(155deg,#f0f2f4,#e8eaed 40%,#eef0f2)" , bd: "#c8ccd2" },
// Aged yellow paper
{ bg: "linear-gradient(155deg,#e8d5a3,#e0cc98 40%,#d9c48e)" , bd: "#bfad7a" },
];
Animation System
Paper Spawn Animation
When a new intelligence source arrives, papers spawn with physics:
import { motion } from "framer-motion" ;
const paperVariants = {
hidden: {
opacity: 0 ,
scale: 0.8 ,
rotate: - 5 ,
y: - 20 ,
},
visible: {
opacity: 1 ,
scale: 1 ,
rotate: 0 ,
y: 0 ,
transition: {
duration: 0.4 ,
ease: "easeOut" ,
type: "spring" ,
stiffness: 100 ,
damping: 15 ,
},
},
exit: {
opacity: 0 ,
scale: 0.8 ,
transition: { duration: 0.2 },
},
};
< motion . div
variants = { paperVariants }
initial = "hidden"
animate = "visible"
exit = "exit"
>
{ /* Paper content */ }
</ motion . div >
Loading Shimmer Effect
While agents are fetching data, papers show a shimmer:
const shimmerKeyframes = `
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
` ;
< div style = {{
background : "linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent)" ,
backgroundSize : "200% 100%" ,
animation : "shimmer 1.5s infinite" ,
}} >
{ /* Loading content */ }
</ div >
Red String Connections
The ConnectionLine.tsx component draws animated red strings between related people:
const ConnectionLine = ({ fromId , toId , relationship }) => {
const pathD = calculatePath ( fromPos , toPos );
return (
< motion . svg
initial = {{ opacity : 0 }}
animate = {{ opacity : 1 }}
style = {{ position : "absolute" , inset : 0 , pointerEvents : "none" }}
>
< motion . path
d = { pathD }
stroke = "#dc2626"
strokeWidth = { 2 }
fill = "none"
initial = {{ pathLength : 0 }}
animate = {{ pathLength : 1 }}
transition = {{ duration : 0.8 , ease : "easeInOut" }}
/>
{ /* Pins at each end */ }
</ motion . svg >
);
};
Layout System
Board Dimensions
const BW = 1100 ; // Board width
const BH = 680 ; // Board height
const BDW = 220 ; // Document width
const BDH = 200 ; // Document height
const SIDE_W = 270 ; // Sidebar width
Document Positioning
Documents are positioned in a grid with randomization:
function calculateDocPosition ( index : number , total : number ) {
const cols = Math . ceil ( Math . sqrt ( total ));
const row = Math . floor ( index / cols );
const col = index % cols ;
// Base position with padding
const x = 50 + col * ( BDW + 30 );
const y = 50 + row * ( BDH + 30 );
// Add small random offset for organic feel
return {
x: x + ( Math . random () - 0.5 ) * 20 ,
y: y + ( Math . random () - 0.5 ) * 20 ,
rotation: ( Math . random () - 0.5 ) * 4 , // -2 to +2 degrees
};
}
Zoom Levels
The board supports three zoom levels:
const ZM = [ 0.48 , 1 , 1.65 ]; // Zoom levels: zoomed out, normal, zoomed in
const [ zoomIndex , setZoomIndex ] = useState ( 1 );
const zoom = ZM [ zoomIndex ];
< div style = {{
transform : `scale( ${ zoom } )` ,
transformOrigin : "center center" ,
transition : "transform 0.3s ease" ,
}} >
{ /* Board content */ }
</ div >
Interactive Features
Drag and Drop
Using @use-gesture/react for smooth dragging:
import { useDrag } from "@use-gesture/react" ;
const bind = useDrag (( state ) => {
const { offset : [ x , y ], down } = state ;
// Update position while dragging
setPosition ({ x , y });
// Persist position to Convex when released
if ( ! down ) {
updatePersonPosition ({ personId , x , y });
}
});
< div { ... bind ()} style = {{ cursor : "grab" }} >
{ /* Draggable paper */ }
</ div >
Click to Expand
Clicking a paper zooms into dossier view:
const handlePaperClick = ( personId : string ) => {
setSelectedPerson ( personId );
setView ( "dossier" ); // Switches to full dossier view
};
Data Integration
Real-Time Person List
Sidebar shows all scanned people:
const persons = useQuery ( api . persons . listAll );
< Sidebar >
{ persons ?. map ( person => (
< PersonCard
key = {person. _id }
person = { person }
onClick = {() => setActivePerson ( person )}
active = {activePerson?. _id === person . _id }
/>
))}
</ Sidebar >
Streaming Intel Fragments
As agents discover new information, fragments appear:
const intel = useQuery ( api . intel . byPerson , {
personId: activePerson ?. _id ,
});
useEffect (() => {
// When new intel arrives, add it to the board
intel ?. forEach ( fragment => {
if ( ! displayedFragments . has ( fragment . _id )) {
addDocumentToBoard ( fragment );
setDisplayedFragments ( prev => new Set ( prev ). add ( fragment . _id ));
}
});
}, [ intel ]);
Virtualization Only render visible documents (viewport culling)
GPU Acceleration Use CSS transforms for animations (not left/top)
Memoization React.memo for paper components
Debounced Updates Batch position updates on drag end
Customization
You can customize the corkboard appearance:
// In IntelBoard.tsx
const BOARD_CONFIG = {
corkColor: "#b89968" ,
paperColors: PAPERS ,
stringColor: "#dc2626" ,
pinColor: "#8b4513" ,
animationSpeed: 0.4 ,
};
For best visual results, keep paper sizes consistent (220x200px) and limit the board to 8-12 documents at once. Too many papers creates visual clutter.
Next Steps
Real-Time Updates Learn how Convex subscriptions power live updates
Dossier View Explore the detailed person intelligence view