Skip to main content

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]);

Performance Optimizations

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

Build docs developers (and LLMs) love