UI Contract (page_contract)¶
Namespace: local_middag\framework\shared\ui.
Extension proxy: local_middag\base\ui\page_contract.
Overview¶
page_contract is the universal server-driven UI protocol for MIDDAG pages (ADR-807). Every page rendered via Inertia — whether generated by page_builder or assembled manually — produces a single page_contract envelope. The frontend receives this JSON and resolves shells, layouts and blocks through registries. The backend describes intent, data and allowed actions. The frontend decides materialization.
The contract is @internal by default. Extensions consume it through the base\ui\ proxy layer.
JSON Schema¶
{
"version": "1",
"shell": "product",
"page": {
"key": "segments.index",
"title": "Segments",
"subtitle": "Audience segmentation engine",
"breadcrumbs": [],
"actions": []
},
"layout": {
"template": "stack",
"regions": {
"header": [],
"content": [],
"footer": []
}
},
"resources": {
"auth": {},
"capabilities": [],
"featureFlags": {},
"locale": "pt-BR"
}
}
Top-level keys¶
| Key | Type | Required | Description |
|---|---|---|---|
version |
string | yes | Contract version ("1") |
shell |
string | yes | Macro frame of the experience |
page |
object | yes | Page identity, breadcrumbs, actions |
layout |
object | yes | Structural template + named regions |
resources |
object | no | Shared transversal data (auth, locale) |
Shells¶
The shell key resolves to an entry in shellRegistry. It defines the macro chrome around the page.
| Shell | Purpose | Default |
|---|---|---|
product |
Full MIDDAG experience — 3-level sidebar, full-width content | yes |
admin |
Moodle admin integration — contextual sidebar, standard content | no |
course |
Course/cohort experiences — no sidebar, standard content | no |
immersive |
Focused flows — no sidebar, full-width, minimal chrome | no |
Navigation for the product shell sidebar is transported as an Inertia shared prop, separate from the page contract. Extensions register navigation items via navigation_registry_interface during boot().
Layout Templates¶
The layout.template key resolves to an entry in layoutRegistry. Each template defines which region names are valid.
Active templates¶
| Template | Regions | Use case |
|---|---|---|
stack |
header, content, footer? |
Forms, linear pages |
split |
header, main, aside |
Side-by-side inspection |
dashboard |
header, content, aside? |
Home, status, overview |
master-detail |
header, content, detail |
List + detail |
Reserved templates (declared, not yet implemented)¶
| Template | Regions | Future use |
|---|---|---|
wizard |
steps, content, actions |
Multi-step forms, onboarding flows |
canvas |
toolbar, canvas, inspector |
Visual builders (workflow, form builder) |
Region names outside the template catalog are invalid. The backend describes region content; the frontend does not receive grid instructions beyond the template key.
Regions¶
Regions are named slots within a layout template. Each region holds an ordered array of block_descriptor objects.
Standard region names¶
| Region | Purpose |
|---|---|
header |
Top strip — status, KPIs, page header |
content |
Primary content area |
footer |
Bottom strip (optional in stack) |
aside |
Sidebar content (split, dashboard) |
main |
Primary pane in split |
detail |
Detail pane in master-detail |
steps |
Step indicators (reserved: wizard) |
actions |
Step actions (reserved: wizard) |
toolbar |
Toolbar strip (reserved: canvas) |
canvas |
Main canvas area (reserved: canvas) |
inspector |
Property inspector (reserved: canvas) |
Block Types¶
A block_descriptor is the composable unit inside a region. The type key resolves to a React component via blockRegistry.
block_descriptor shape¶
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | Block type key in blockRegistry |
key |
string | yes | Unique instance identifier |
data |
object | yes | Typed payload (shape depends on type) |
variant |
string | no | Visual/semantic variation |
title |
string | no | Block heading |
subtitle |
string | no | Short supporting text |
actions |
array | no | Local actions (page_action[]) |
meta |
object | no | Non-visual metadata |
Standard block catalog¶
| Type | Purpose | Key data fields |
|---|---|---|
dense_table |
Operational listings | columns, rows, pagination, sort, filters, rowActions, bulkActions |
form_panel |
Schema-driven forms | action, method, schema, values, errors, meta |
detail_panel |
Contextual read view | sections |
metric_card |
KPI summary | value, label, delta?, icon?, href? |
empty_state |
Empty state with CTA | variant, description?, cta?, icon? |
status_strip |
Status badges/strips | items, tone? |
activity_timeline |
Recent history | groups, has_more?, load_more_href? |
markdown_panel |
Short explanatory content | content, max_height? |
dense_table data shape¶
{
"columns": [
{"key": "name", "label": "Name", "sortable": true, "searchable": true},
{"key": "status", "label": "Status", "variant": "badge"}
],
"rows": [],
"pagination": {"page": 1, "perPage": 25, "total": 0, "lastPage": 1},
"sort": {"column": "created_at", "direction": "desc"},
"filters": {"available": [], "applied": {}},
"rowActions": [],
"bulkActions": []
}
form_panel data shape¶
{
"action": "/local/middag/index.php/segments",
"method": "post",
"schema": [],
"values": {},
"errors": {},
"meta": {"multiStep": false, "cancelHref": "/local/middag/index.php/segments"}
}
The schema array follows the inertia_renderer serialization (ADR-805): nodes of {kind, component, props} for fields and {kind, id, label, children} for sections/groups.
Custom blocks (extensions)¶
Extensions register custom block types by implementing block_type_interface (PHP) and registering the corresponding React component in blockRegistry. Custom blocks are resolved identically to standard blocks.
Page Actions¶
page_action describes an actionable button at page level or block level.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Stable action key |
label |
string | yes | Visible label |
intent |
string | yes | primary, secondary, danger, ghost |
href |
string | no | Target URL or endpoint |
method |
string | no | get, post, put, delete |
icon |
string | no | Semantic icon identifier |
requiresConfirmation |
bool | no | Requires explicit user confirmation |
disabled |
bool | no | Visually unavailable |
Breadcrumbs¶
breadcrumb describes a single entry in the navigation trail.
| Field | Type | Required | Description |
|---|---|---|---|
label |
string | yes | Display text |
href |
string | no | Navigation target |
external |
bool | no | Opens in new tab if true |
Resources¶
page_resources carries minimal transversal data shared across the page. Heavy or page-specific data belongs in block_descriptor.data.
| Field | Type | Description |
|---|---|---|
auth |
object |
Current user, tenant, active context |
capabilities |
list<string> |
Relevant capabilities for the shell |
featureFlags |
map<string,bool> |
Rollout toggles |
locale |
string |
Effective UI locale (default pt-BR) |
PHP Classes¶
| Class | Role |
|---|---|
shared\ui\page_contract |
Top-level envelope (readonly, JsonSerializable) |
shared\ui\page_meta |
Page identity (key, title, breadcrumbs, actions) |
shared\ui\layout_descriptor |
Template + regions map |
shared\ui\block_descriptor |
Typed block within a region |
shared\ui\page_action |
Action button descriptor |
shared\ui\breadcrumb |
Navigation breadcrumb entry |
shared\ui\page_resources |
Shared page resources |
shared\ui\block |
Static factory for standard block types |
shared\ui\page_builder |
Composable builder producing page_contract |
base\ui\page_contract |
Public proxy for extensions |
Restrictions¶
The contract does not transport:
- React component names (use
typekeys resolved via registries) className,styleor arbitrary CSS- Nested component trees
- Free-text visibility expressions
- Inline JS handlers
Cross-references¶
- ADR-807 — Frontend interface architecture and dynamic layout
- ADR-805 — Form serialization and inertia_renderer
- ADR-806 — Form DSL
- ADR-803 — Rendering modes (Moodle pages, Inertia SPA, API JSON)