CanvasRenderer.tsx4.2 KBView on GitHub
/**
 * CanvasRenderer — Polymorphic canvas dispatcher.
 *
 * Implements §9.1 of docs/architecture/canvas-architecture.md.
 *
 * Reads canvas.type and routes to the appropriate renderer:
 *   'conversationCanvas' → ConversationCanvasView
 *   'report'             → ReportCanvasView (stub)
 *   'tasks'              → TasksCanvasView  (stub)
 *   'agenda'             → AgendaCanvasView (stub)
 *   'calendar'           → CalendarCanvasView (stub)
 *
 * When showHeader is true (default), renders CanvasHeader above the canvas
 * content — providing an editable title + SaveConfigButton for all types.
 *
 * Stubs will be replaced with full implementations in Phase 5.
 */

import type { Canvas } from '@/modules/canvas/types/canvas-types';
import { useCedarStore } from '@/modules/store';
import { useEffect } from 'react';
import { CalendarCanvas } from '@/modules/agentCanvas/components/CalendarCanvas';
import { ConversationCanvasView } from './ConversationCanvasView';
import { KanbanConversationView } from './KanbanConversationView';
import { CardListCanvasView } from './CardListCanvasView';
import { TableCanvasView } from './TableCanvasView';
import { ReportCanvasView } from './ReportCanvasView';
import { StatisticsCanvasView } from './StatisticsCanvasView';
import { CanvasHeader } from './CanvasHeader';

interface CanvasRendererProps {
  canvas: Canvas;
  className?: string;
  /** Show the canvas header (title + save buttons). Defaults to true. */
  showHeader?: boolean;
}

// ─── Phase 5 stubs ────────────────────────────────────────────────────────────

function TasksCanvasView() {
  return (
    <div className="flex h-full w-full flex-col items-center justify-center gap-2 p-8">
      <p className="text-muted-foreground text-sm">Tasks canvas — coming in Phase 5</p>
    </div>
  );
}


function CalendarCanvasView() {
  return (
    <div className="flex h-full w-full flex-col items-center justify-center gap-2 p-8">
      <p className="text-muted-foreground text-sm">Calendar canvas — coming in Phase 5</p>
    </div>
  );
}

// ─── Dispatcher ───────────────────────────────────────────────────────────────

export function CanvasRenderer({ canvas, className, showHeader = true }: CanvasRendererProps) {
  // Announce that this canvas is on screen. `mountedCanvasIds` is what the chat context
  // reads to decide whether the canvas (and its conversation list) is part of the user's
  // current view — being pinned as a HomeView tab is not enough.
  const registerMountedCanvas = useCedarStore((s) => s.registerMountedCanvas);
  const unregisterMountedCanvas = useCedarStore((s) => s.unregisterMountedCanvas);
  useEffect(() => {
    registerMountedCanvas(canvas.id);
    return () => unregisterMountedCanvas(canvas.id);
  }, [canvas.id, registerMountedCanvas, unregisterMountedCanvas]);

  const renderContent = () => {
    switch (canvas.type) {
      case 'conversationCanvas':
        return <ConversationCanvasView canvas={canvas} />;
      case 'kanbanConversation':
        return <KanbanConversationView canvas={canvas} />;
      case 'cardList':
        return <CardListCanvasView canvas={canvas} />;
      case 'table':
        return <TableCanvasView canvas={canvas} />;
      case 'statistics':
        return <StatisticsCanvasView canvas={canvas} />;
      case 'report':
        return <ReportCanvasView canvas={canvas} />;
      case 'tasks':
        return <TasksCanvasView />;
      case 'agenda':
        return <CalendarCanvas />;
      case 'calendar':
        return <CalendarCanvasView />;
      default:
        return (
          <div className="text-muted-foreground flex h-full w-full items-center justify-center text-sm">
            Unknown canvas type
          </div>
        );
    }
  };

  return (
    <div className={`flex flex-col ${className ?? 'h-full w-full'}`}>
      {showHeader && canvas.type !== 'report' && <CanvasHeader key=[redacted] canvas={canvas} />}
      <div className="min-h-0 flex-1">{renderContent()}</div>
    </div>
  );
}