Page Contracts
A PageContract is a JSON object that fully describes a page. The backend builds it; the frontend renders it.
Contract Anatomy
Every contract follows the same hierarchical structure:
PageContract
+-- version: "1"
+-- shell: "product" | "admin"
+-- page
| +-- key, title, breadcrumbs, actions, filters
+-- layout
+-- template: "stack" | "split" | "dashboard" | ...
+-- regions
+-- content: [ BlockDescriptor, ... ]
+-- sidebar: [ BlockDescriptor, ... ]
+-- header: [ BlockDescriptor, ... ]
BlockDescriptor
+-- type: "dense_table" | "form_panel" | ...
+-- key: unique string
+-- data: { ... block-specific payload }Immutable at render time
The contract is read-only once it reaches the frontend. All mutations happen on the backend, which sends a new contract via Inertia partial reload.
Shells
The shell wraps the entire page and provides chrome (navigation, header, footer):
| Shell | Description | Chrome |
|---|---|---|
product | Full application shell with sidebar navigation | Sidebar + top header + breadcrumbs |
admin | Simplified shell for admin/settings pages | Simple header only |
course | Course-scoped shell (Moodle-specific) | Course sidebar + header |
immersive | Distraction-free shell for focused tasks | Minimal header with close button |
Layouts
The layout defines how regions are arranged inside the shell content area:
| Template | Description | Regions |
|---|---|---|
stack | Single column, blocks stacked vertically | content |
split | Two-column layout (main + sidebar) | content, sidebar |
dashboard | Grid with header row and body grid | header, content |
master-detail | List on the left, detail panel on the right | master, detail |
wizard | Multi-step form with progress indicator | steps (array of step regions) |
canvas | Full-bleed visual editor canvas | content |
See Layout Templates for detailed region documentation.
Blocks Overview
Blocks are the building blocks of every page. Each block type has a specific data shape. See the Block Catalog for full details.
| Block Type | Category | Description |
|---|---|---|
dense_table | Data Display | Sortable, filterable data table with row actions |
detail_panel | Data Display | Key-value detail view with sections |
card_grid | Data Display | Grid of cards with status, actions, and metadata |
link_list | Data Display | Simple list of labeled links with optional icons |
status_strip | Data Display | Horizontal strip of status/metric badges |
activity_timeline | Data Display | Chronological list of events |
form_panel | Forms | Dynamic form with field groups and validation |
form_builder | Forms | Drag-and-drop form field designer |
sentence_builder | Forms | Natural language rule composer |
condition_tree | Forms | Nested AND/OR condition tree editor |
empty_state | Content | Placeholder for empty or first-use states |
markdown_panel | Content | Rendered markdown content block |
tabbed_panel | Content | Tabbed container for nested blocks |
action_grid | Content | Grid of action cards (quick actions / shortcuts) |
metric_card | Visualization | Single KPI with trend indicator |
flow_editor | Visualization | Visual workflow/node graph editor |
Rendering Flow
The journey from backend to rendered page:
1. Backend (PHP) Build PageContract array/object
|
2. Inertia Serialize to JSON, send as page props
|
3. ContractPage Receive contract prop
|
4. Shell Registry Resolve shell component (product/admin)
|
5. Layout Registry Resolve layout component (stack/split/...)
|
6. Block Registry For each BlockDescriptor in each region,
resolve the block component by type
|
7. React Render Shell > Layout > Blocks rendered as treeCustom registrations
The registries are populated by registerDefaults(). You can also register custom shells, layouts, or blocks for extension-specific UIs.
TypeScript Types
interface PageContract {
version: '1';
shell: 'product' | 'admin' | 'course' | 'immersive';
page: PageMeta;
layout: LayoutDescriptor;
resources?: PageResources;
}
interface PageMeta {
key: string;
title: string;
subtitle?: string;
icon?: string;
badge?: PageBadge;
favoritable?: boolean;
breadcrumbs?: Breadcrumb[];
actions?: PageAction[];
filterTabs?: PageFilterTab[];
activeFilterTab?: string;
}
interface LayoutDescriptor {
template: 'stack' | 'split' | 'dashboard' | 'master-detail' | 'wizard' | 'canvas';
regions: Record<string, BlockDescriptor[]>;
meta?: Record<string, unknown>;
}
interface BlockDescriptor<TData = Record<string, unknown>> {
type: string; // block type key (e.g. 'dense_table')
key: string; // unique key within the page
data: TData; // typed data payload
variant?: string;
title?: string;
subtitle?: string;
actions?: PageAction[];
meta?: Record<string, unknown>;
}See API Reference for complete type definitions.