Sprinter Docs

View and Block System v2

Unified view renderer, standalone view route, per-block Suspense loading, real-time AI iteration, and view management (copy, fork, pin).

Superseded by view-block-system-v3.

Problem

Three separate renderers exist today for views: ViewRenderer (list views), DetailViewRenderer (entity detail), and WorkspaceViewRenderer (workspace pages with regions). This creates duplicated logic, inconsistent behavior, and confusion about which renderer to use. Views can only be rendered in the context of entity list/detail pages -- there is no standalone view route for AI-generated dashboards or shareable reports. The view editor at 1,021 lines violates the 200-line component rule. Blocks load synchronously, meaning a slow chart blocks the entire page render.

Solution

Replace three view renderers with one UnifiedViewRenderer. Add a standalone /view/[id] route for opening views as full pages. Wrap each block in a Suspense boundary with type-specific skeleton loaders for independent loading. Add Supabase Realtime subscription so AI agents can iterate on views in real-time. Split the monolithic view editor into focused sub-components. Add view management operations: copy, fork, and pin-to-sidebar.

Design

Architecture

UnifiedViewRenderer

A single server component that handles all view types:

interface UnifiedViewRendererProps {
  view: ViewRecord;
  entity?: EntityRecord;        // For detail views
  entityType?: EntityTypeRecord; // For scoped resolution
  className?: string;
  editable?: boolean;           // Enable block-level settings
}

Rendering logic:

  1. If view.regions is defined and has content, render with ViewShell (region-based layout)
  2. Otherwise, render with BlockGrid (flat layout using view.blocks + view.layout)

Both paths use the same block resolution: resolveBlocks() fetches data for each block server-side. Each block is wrapped in a <Suspense> boundary with a BlockSkeleton fallback. Heavy blocks (chart, table, kanban) load independently -- the page shell renders instantly.

ViewShell (replaces WorkspaceShell)

Region-based layout using CSS Grid with four layout presets:

PresetGridDescription
stackSingle columnMain only, full width
sidebar1fr 320pxMain + right sidebar
three-panel240px 1fr 320pxLeft + main + right
document200px 1frNarrow nav + wide content

All presets support optional header (above grid) and footer (below grid) regions.

Per-Block Suspense

Each block gets an independent loading boundary:

{blocks.map((block) => (
  <div key={block.id} className={sizeClass(block.size, layout)}>
    <Suspense fallback={<BlockSkeleton type={block.type} size={block.size} />}>
      <AsyncBlockRenderer block={block} context={ctx} />
    </Suspense>
  </div>
))}

BlockSkeleton renders type-appropriate placeholders:

  • stat-cards: row of shimmer rectangles
  • chart: shimmer rectangle with axis lines
  • table: shimmer header + rows
  • field-card: label + value shimmer
  • Default: simple shimmer card

Standalone View Route -- /view/[id]

New route at app/(app)/view/[id]/page.tsx. Server component fetches the view and renders it with UnifiedViewRenderer.

ViewHeader shows: view title and description, edit button (opens view editor sheet), share/copy link button, "Save to sidebar" button (adds to nav), and fork button (creates a copy).

For transient views generated by AI in chat, the "Open full page" action saves the transient view to the views table (scope: "user") and navigates to /view/\{newId\}. The view is then editable, forkable, and shareable.

Real-Time AI Iteration

When a view is open at /view/[id], subscribe to Supabase Realtime on the views table:

function useViewRealtime(viewId: string) {
  // Subscribe to postgres_changes on views table
  // On UPDATE, invalidate React Query cache for this view
  // View re-renders with new blocks instantly
}

AI iteration flow:

  1. User opens /view/\{id\} in browser
  2. In chat, user says "Add a chart block showing revenue by stage"
  3. AI calls manageView tool to update the view's blocks
  4. Realtime subscription fires, view re-renders with the new block
  5. User sees the change instantly without page refresh

View Management

Copy: Creates an independent duplicate with a new title via copyView(sourceViewId, newTitle?).

Fork: Creates a linked copy that tracks its parent. New parent_view_id column on the views table tracks lineage.

Pin to sidebar: Views can be added to the sidebar navigation via the existing nav_configs system. When pinned, the nav item links to /view/\{id\} for standalone views or to the entity type page with ?view=\{id\} for entity-scoped views.

View Templates

Pre-built view configurations that AI or users can instantiate:

const VIEW_TEMPLATES = {
  "kpi-dashboard": {
    title: "KPI Dashboard",
    description: "Key metrics overview with charts",
    layout: "bento",
    blocks: [
      { type: "stat-cards", size: "full", config: { limit: 8 } },
      { type: "chart", size: "half", config: { chartType: "bar" } },
      { type: "ranking", size: "half", config: { limit: 5 } },
      { type: "activity", size: "full", config: { limit: 10 } },
    ]
  },
  "entity-overview": { ... },
  "comparison-view": { ... },
  "pipeline-tracker": { ... },
  "document-workspace": { ... },
}

Templates are available in the "New View" dialog and via AI tools.

View Editor Refactor

Split the 1,021-line view-editor.tsx into focused components:

features/views/components/
  view-editor/
    index.tsx                    (~80 lines)  -- main editor shell
    block-list.tsx               (~100 lines) -- sortable block list with DnD
    block-config-panel.tsx       (~80 lines)  -- config panel dispatcher
    block-type-picker.tsx        (~60 lines)  -- block type selection grid
    configs/
      chart-config.tsx           (~80 lines)
      table-config.tsx           (~60 lines)
      kanban-config.tsx          (~60 lines)
      field-card-config.tsx      (~50 lines)
      stat-cards-config.tsx      (~50 lines)
      connection-config.tsx      (~50 lines)
      generic-config.tsx         (~40 lines)  -- fallback for simple blocks

Each config component is self-contained with its own Zod schema for validation.

Per-Block-Type Config Interfaces

Add typed config interfaces for each block type:

type TypedBlockConfig =
  | { type: "chart"; config?: ChartBlockConfig }
  | { type: "kanban"; config?: KanbanBlockConfig }
  | { type: "field-card"; config?: FieldCardBlockConfig }
  // ... etc

This enables type-safe config editing and validation throughout the system.

API

POST /api/views/copy -- copy a view. Body: { sourceViewId: string, title?: string }. Returns the new view record.

POST /api/views/[id]/fork -- fork a view with lineage tracking. Returns the new view record with parent_view_id set.

Existing manageView AI tool continues to work for view CRUD. The Realtime subscription ensures changes propagate instantly.

Migration

ALTER TABLE views ADD COLUMN parent_view_id uuid REFERENCES views(id) ON DELETE SET NULL;
CREATE INDEX idx_views_parent ON views(parent_view_id) WHERE parent_view_id IS NOT NULL;

No other schema changes required. The existing schema supports standalone views (entity_type_id = NULL, entity_id = NULL), entity-specific overrides (entity_id IS NOT NULL), sidebar pinning (via nav_configs), and real-time via Supabase Realtime on the existing views table.

Cleanup

Delete after migration:

  • features/views/components/view-renderer.tsx -- replaced by UnifiedViewRenderer
  • features/views/components/view-renderer-client.tsx -- replaced by UnifiedViewRenderer
  • features/views/components/detail-view-renderer.tsx -- replaced by UnifiedViewRenderer
  • features/views/components/workspace-view-renderer.tsx -- replaced by UnifiedViewRenderer
  • features/views/components/workspace-shell.tsx -- renamed to view-shell.tsx
  • features/views/components/detail-view-tabs.tsx -- merged into ViewTabBar
  • features/views/components/view-editor.tsx -- split into view-editor/ directory

Trade-offs

Unified renderer vs. specialized renderers: A single renderer with two code paths (regions vs. flat blocks) is simpler to maintain than three separate renderers, but the rendering logic must handle more cases in one place. The internal branching is straightforward (regions defined? use ViewShell : use BlockGrid), so the complexity is manageable.

Supabase Realtime vs. polling: Realtime provides instant updates for AI iteration, which is critical for the "AI edits, user sees" workflow. Polling would add latency and unnecessary load. The tradeoff is a persistent WebSocket connection per open view, but Supabase handles this efficiently.

Per-block Suspense vs. page-level loading: Per-block Suspense means fast initial paint (the layout renders immediately, blocks stream in). The tradeoff is more Suspense boundaries and slightly more complex error handling (each block needs its own error boundary). The user experience improvement is significant for views with heavy blocks like charts and tables.

View templates as code vs. DB-stored: Code-stored templates are versioned, type-safe, and easy to update. DB-stored templates would allow user-created templates but add complexity. Starting with code templates is simpler; user templates can be added later by promoting saved views.

parent_view_id for fork tracking: A simple nullable FK is the lightest way to track fork lineage. It does not support deep fork trees (fork of a fork), but one-level tracking covers the primary use case. Deep lineage could be added later with a separate provenance table if needed.

Acceptance Criteria

  • All views render through UnifiedViewRenderer -- no separate detail/workspace/list renderers
  • Views with regions use ViewShell; views without regions use BlockGrid
  • Each block loads independently via Suspense with a type-specific skeleton
  • Page shell renders instantly; slow blocks (chart, table) stream in
  • Standalone view at /view/[id] renders any persisted view
  • ViewHeader shows title, edit, share, pin-to-sidebar, and fork actions
  • AI-generated transient views can be saved and opened at /view/[id]
  • Realtime subscription updates view when AI modifies it via manageView tool
  • Copy creates an independent duplicate with a new title
  • Fork creates a linked copy with parent_view_id set
  • Pin-to-sidebar adds a nav item via nav_configs
  • View templates can be instantiated from the New View dialog
  • View editor is split into sub-components, all under 200 lines
  • Per-block config forms use typed Zod schemas
  • Old renderers deleted, no dead code remaining
  • All existing view functionality preserved (no regressions)
  • Visual verification of standalone view, skeleton loading, and editor
  • pnpm build and pnpm test pass

Files

New

  • app/(app)/view/[id]/page.tsx -- standalone view route (server component)
  • app/(app)/view/[id]/view-page-client.tsx -- client wrapper with realtime subscription
  • features/views/components/unified-view-renderer.tsx -- single unified renderer
  • features/views/components/view-shell.tsx -- region-based layout (replaces workspace-shell)
  • features/views/components/view-header.tsx -- view title, actions, management
  • features/views/components/view-tab-bar.tsx -- unified tab bar (replaces two separate tab components)
  • features/views/components/block-skeleton.tsx -- type-specific loading skeletons
  • features/views/hooks/use-view-realtime.ts -- Supabase Realtime subscription hook
  • features/views/lib/templates.ts -- view template definitions
  • features/views/components/view-editor/index.tsx -- editor shell (~80 lines)
  • features/views/components/view-editor/block-list.tsx -- sortable block list (~100 lines)
  • features/views/components/view-editor/block-config-panel.tsx -- config dispatcher (~80 lines)
  • features/views/components/view-editor/block-type-picker.tsx -- type selection grid (~60 lines)
  • features/views/components/view-editor/configs/chart-config.tsx -- chart block config form
  • features/views/components/view-editor/configs/table-config.tsx -- table block config form
  • features/views/components/view-editor/configs/kanban-config.tsx -- kanban block config form
  • features/views/components/view-editor/configs/field-card-config.tsx -- field card config form
  • features/views/components/view-editor/configs/stat-cards-config.tsx -- stat cards config form
  • features/views/components/view-editor/configs/connection-config.tsx -- connection block config form
  • features/views/components/view-editor/configs/generic-config.tsx -- fallback config form
  • documents/AI-VIEW-AUTHORING.md -- AI agent reference guide for view authoring
  • supabase/migrations/YYYYMMDD_NNN_add_parent_view_id.sql -- parent_view_id column

Modified

  • features/views/types.ts -- add parent_view_id to ViewRecord
  • features/views/server/actions.ts -- add copyView(), enhance forkViewForEntity()
  • features/blocks/types.ts -- add per-block-type config interfaces (TypedBlockConfig union)
  • features/blocks/components/block-renderer.tsx -- add card wrapper, animation, skeleton support
  • app/(app)/[typeSlug]/page.tsx -- use UnifiedViewRenderer
  • app/(app)/[typeSlug]/[id]/entity-detail-client.tsx -- use UnifiedViewRenderer
  • app/api/views/route.ts -- add copy endpoint
  • app/api/views/[id]/route.ts -- add fork endpoint

Deleted

  • features/views/components/view-renderer.tsx -- replaced by unified renderer
  • features/views/components/view-renderer-client.tsx -- replaced by unified renderer
  • features/views/components/detail-view-renderer.tsx -- replaced by unified renderer
  • features/views/components/detail-view-tabs.tsx -- replaced by ViewTabBar
  • features/views/components/workspace-view-renderer.tsx -- replaced by unified renderer
  • features/views/components/workspace-shell.tsx -- replaced by view-shell.tsx
  • features/views/components/view-editor.tsx -- split into view-editor/ directory

On this page