/** * CanvasViewport — Virtualized scrolling canvas. * Renders CanvasComponent objects sorted by layout.orderIndex. * Uses CSS content-visibility for large component lists (virtualization-lite). * Stable scroll anchoring: new components append without scroll jump. */ import { useRef } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import type { CanvasComponent, PromptExecution } from '../types/canvas'; import { ComponentRegistry, type ComponentRenderContext } from './ComponentRegistry'; import { Loader2 } from 'lucide-react'; interface CanvasViewportProps { components: CanvasComponent[]; ctx: ComponentRenderContext; inFlightExecution: PromptExecution | null; selectedComponentId?: string | null; onSelectComponent: (id: string | null) => void; } const SECTION_LABELS: Record = { sec_pipeline: 'Pipeline', sec_leads: 'Lead Intelligence', sec_team: 'Team Performance', sec_actions: 'Action Queue', sec_prompt_generated: 'Oracle Responses', sec_geography: 'Geographic', sec_forecast: 'Forecasting', }; /** Groups components by sectionId, preserving orderIndex sort */ function groupBySection(components: CanvasComponent[]): Array<{ sectionId: string; components: CanvasComponent[] }> { const sorted = [...components].sort((a, b) => a.layout.orderIndex - b.layout.orderIndex); const sectionMap = new Map(); for (const comp of sorted) { const sid = comp.layout.sectionId; if (!sectionMap.has(sid)) sectionMap.set(sid, []); sectionMap.get(sid)!.push(comp); } return Array.from(sectionMap.entries()).map(([sectionId, comps]) => ({ sectionId, components: comps })); } /** CSS content-visibility wrapper for off-screen components, applying width mode to the flex item */ function ComponentFlexWrapper({ comp, children }: { comp: CanvasComponent; children: React.ReactNode }) { const styles: Record = { full: 'w-full', half: 'w-full xl:w-[calc(50%-8px)]', third: 'w-full xl:w-[calc(33.333%-11px)]', }; return (
{children}
); } export function CanvasViewport({ components, ctx, inFlightExecution, selectedComponentId, onSelectComponent, }: CanvasViewportProps) { const viewportRef = useRef(null); const sections = groupBySection(components); return (
{ // Click outside component to deselect if (e.currentTarget === e.target) onSelectComponent(null); }} >
{sections.length === 0 && !inFlightExecution && ( )} {sections.map(({ sectionId, components: sectionComps }) => (
{/* Section header */}

{SECTION_LABELS[sectionId] ?? sectionId.replace(/^sec_/, '').replace(/_/g, ' ')}

{sectionComps.length}
{/* Flex wrap for half/third width components */}
{sectionComps.map((comp) => ( ))}
))} {/* In-flight execution placeholder */} {inFlightExecution && (

Oracle is analyzing your prompt…

"{inFlightExecution.prompt}"

{inFlightExecution.status}
)}
{/* Bottom padding for floating prompt bar */}
); } function EmptyCanvasState() { return (

Canvas is empty

Ask Oracle anything — type a prompt below to generate analytical components on your canvas.

); }