Documentation source
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](/docs/roadmap/specs/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:
```typescript
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:
| Preset | Grid | Description |
|--------|------|-------------|
| `stack` | Single column | Main only, full width |
| `sidebar` | `1fr 320px` | Main + right sidebar |
| `three-panel` | `240px 1fr 320px` | Left + main + right |
| `document` | `200px 1fr` | Narrow 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:
```tsx
{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:
```typescript
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:
```typescript
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:
```typescript
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
```sql
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