Add a Page¶
Pages in Mozaiks are declared as YAML schemas — not raw React. The platform shell renders them automatically using pre-built primitives.
Use the declarative path by default. Only reach for a custom React route when the available primitives genuinely can't cover the use case.
File Structure¶
Or folder form when the page has supporting assets:
For cases the schema cannot express:
app/ui/pages/custom/{Name}.jsx ← custom React route
app/ui/route_manifest.json ← registers the route
app/ui/index.js ← registers the component key used by the route
Minimum Page Schema¶
name: {name}
title: {Page Title}
layout: grid # grid | sidebar | full-width | split
shell_mode: standard # standard | workspace | conversation | focused | immersive | public
sections:
- id: {section_id}
title: {Section Title}
primitive: DataTable
config:
api_endpoint: /api/modules/{module}/{action}
columns:
- { key: id, label: ID }
- { key: name, label: Name }
Pages are loaded at startup. The route /{name} is served automatically — no registration step needed.
Layout Options¶
| Layout | Best for |
|---|---|
grid | Dashboards, overview pages |
sidebar | Master-detail, filtered lists |
full-width | Forms, detail views, reports |
split | Comparison views, side-by-side |
Shell Mode¶
Controls the chrome that wraps the page:
| Mode | Use for |
|---|---|
standard | Normal app pages |
workspace | Dashboards, admin surfaces, module workspaces |
conversation | Chat, DM, thread pages — composer owns the bottom edge |
focused | Onboarding, review, approval, checkout-style flows |
immersive | Map, canvas, media, full-viewport routes |
public | Marketing, legal, unauthenticated pages |
Available Primitives¶
| Primitive | Use for |
|---|---|
PageHeader | Durable page title and primary action buttons |
ResourceTable | Primary record/index tables |
DataTable | Dense operational record lists |
Form | Data entry with field definitions and submit action |
SummaryStrip | 2–4 key page metrics |
Metric | Single supporting metric |
Panel / SurfaceCard | Grouped support surfaces |
Grid | Child primitive layout container |
Button | Call to action |
Modal | Overlay dialog |
Alert | Inline message |
StatusPill | Compact status label |
Skeleton | Loading and empty states |
Do not use Card, Stat, or Badge — use SurfaceCard/Panel, SummaryStrip/Metric, and StatusPill instead.
Navigation¶
Add navigation to the page schema when it should appear in the shell nav:
Use scope: global for primary destinations. Use scope: local for workspace/module subsections that belong in local nav, not the global shell.
Example: Dashboard¶
name: dashboard
title: Dashboard
layout: grid
shell_mode: workspace
sections:
- id: header
primitive: PageHeader
config:
title: Dashboard
subtitle: Monitor current activity.
- id: summary
primitive: SummaryStrip
config:
api_endpoint: /api/modules/orders/stats
items:
- { label: Orders, value_key: total }
- id: recent-orders
title: Recent Orders
primitive: ResourceTable
config:
api_endpoint: /api/modules/orders/list_orders
columns:
- { key: id, label: Order ID }
- { key: status, label: Status, type: status }
- { key: total, label: Total }
Example: Form Page¶
name: new-customer
title: New Customer
layout: full-width
shell_mode: focused
sections:
- id: customer-form
title: Customer Details
primitive: Form
config:
fields:
- { name: name, label: Full Name, type: text, required: true }
- { name: email, label: Email, type: email, required: true }
submitLabel: Create Customer
api_endpoint: /api/modules/customers/create_customer
Custom React Route¶
When the declarative schema can't express what you need:
// app/ui/route_manifest.json
{
"pages": [
{
"id": "my-page",
"path": "/my-page",
"component": "MyPage",
"meta": { "requiresAuth": true }
}
]
}
Register the component in app/ui/index.js:
There is no implicit discovery for custom React routes. A route is valid only when app/ui/route_manifest.json, app/ui/pages/custom/{Name}.jsx, and app/ui/index.js all agree on the same component key. admin/admin_registry.yaml is not a route registry and must not own full-page custom React components.
Use custom routes sparingly. Extend an existing primitive before reaching for a custom route.
Rules¶
api_endpointmust be/api/modules/{name}/{action_id}— no query strings- Put filters and limits in the action input schema, not the URL
- Page-owned navigation belongs in the page's
navigationfield - Page-owned chrome intent belongs in
shell_mode app/config/shell.jsonis for app-wide chrome only, not per-route overrides