| name | add-page |
| description | Add a frontend page (AppPageSchema) to an existing Mozaiks app. |
| argument-hint | [page name or description] |
Before starting: git fetch origin && gh pr list --state open && git log origin/main --oneline -3 — if another agent has an open PR touching the same files you need, wait for it to merge or branch off it instead of main.
Help the user add a page to an existing Mozaiks application.
Pages in Mozaiks are declared as YAML schemas (AppPageSchema) — not raw React.
The platform shell renders them automatically using pre-built primitives.
What a Page Is
app/ui/pages/
└── <name>.yaml ← the page schema
# Or folder form:
app/ui/pages/<name>/
└── page.yaml
The SchemaPage route component fetches /api/pages/<name>, and PageRenderer
assembles the layout from the schema using the primitive registry.
Steps to Add a Page
1. Create the page schema
touch app/ui/pages/<name>.yaml
2. Write the schema
name: <name>
title: <Page Title>
layout: grid
shell_mode: standard
sections:
- id: <section_id>
title: <Section Title>
primitive: DataTable
config:
columns:
- { key: id, label: ID }
- { key: name, label: Name }
api_endpoint: /api/modules/<module>/<action>
3. Restart the backend
mozaiks serve .
Pages are loaded at startup. The route /<name> is served automatically.
4. Add shell access when needed
If the page should be globally reachable, prefer route-level navigation metadata
on the page itself:
navigation:
scope: global
icon: dashboard
order: 20
Use scope: local for workspace/module subsections that should render through
local navigation instead of crowding the global shell.
Choose shell_mode for route chrome:
standard: normal app page.
workspace: dense dashboard, admin/profile/module workspace, or local nav surface.
conversation: chat, DM, inbox thread, or support conversation where the composer owns the bottom edge.
focused: onboarding, setup, review, approval, or checkout-style route.
immersive: map, canvas, media, game, or full-viewport route.
public: legal, marketing, or unauthenticated information route.
Use compact shell shortcuts in app/config/shell.json for built-in chrome such
as profile/auth/footer items:
{
"shortcuts": {
"header": ["dashboard", "<name>"],
"mobile": ["dashboard", "<name>", "profile"]
}
}
Use explicit header, profile, or mobile.bottomBar entries only when the
label, icon, role gate, or path needs to differ from the route/page catalog.
Use app/config/shell.json -> navigation.policy when the app needs a different
placement model, such as desktop sidebar global nav or mobile local sheet nav.
Use app/config/shell.json -> chrome only to override app-wide behavior for the
standard shell modes. Do not encode per-route chrome there; the page owns
shell_mode.
Layout Options
| Layout | Description | Best for |
|---|
grid | 2–3 column responsive grid | Dashboards, overview pages |
sidebar | First section → aside, rest → main | Master-detail, filtered lists |
full-width | Single column, full width | Forms, detail views, reports |
split | Two equal columns | Comparison views, side-by-side |
Available Primitives
| Primitive | Use case | Key config |
|---|
PageHeader | Durable page title and primary actions | title, subtitle, actions[] |
ResourceTable | Primary record/index page table | columns[], api_endpoint, data_key, actions[] |
DataTable | Dense operational record lists | columns[], api_endpoint, data_key |
Form | Data entry | fields[], onSubmit, submitLabel |
SummaryStrip | 2-4 useful page metrics | items[] with value or value_key |
Metric | Single supporting metric | label, value or value_key, detail |
Panel / SurfaceCard | Purposeful grouped support surface | title, subtitle, children |
Grid | Small child primitive layout | children[], columns (2|3|4) |
Button | Call to action | label, variant, onClick |
Modal | Overlay dialog | id, title, open |
Alert | Inline message | message, variant (info|success|warning|error) |
StatusPill | Compact status label | label, tone |
Skeleton | Loading / empty state | rows, height |
Removed primitives: do not use Card, Stat, or Badge. Use SurfaceCard or Panel, SummaryStrip or Metric, and StatusPill.
Example: Dashboard Page
name: dashboard
title: Dashboard
layout: grid
shell_mode: workspace
sections:
- id: dashboard-header
primitive: PageHeader
config:
title: Dashboard
subtitle: Monitor current user and order activity.
- id: user_summary
primitive: SummaryStrip
config:
api_endpoint: /api/modules/users/stats
items:
- label: Users
value_key: total_users
format: number
- id: recent_orders
title: Recent Orders
primitive: ResourceTable
config:
api_endpoint: /api/modules/orders/list_orders
columns:
- { key: id, label: Order ID }
- { key: total, label: Total }
- { key: status, label: Status, type: status }
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 }
- { name: phone, label: Phone, type: text }
submitLabel: Create Customer
api_endpoint: /api/modules/customers/create_customer
Live Data Binding
When a section has api_endpoint, SectionRenderer fetches that endpoint on mount
and injects the response into the primitive's rows / value props.
The agent can also trigger a refresh by emitting ui.datatable.refresh with the section id.
Custom Full-Page React Routes
For cases the declarative schema cannot express, use the escape hatch:
app/ui/pages/custom/<name>.jsx ← custom React page
app/ui/route_manifest.json ← registers the route
app/ui/index.js ← registers the component key used by the route
{
"pages": [
{
"id": "<name>",
"label": "<Name>",
"path": "/custom/<name>",
"component": "<Name>Page",
"requiresAuth": true,
"order": 20,
"meta": { "title": "<Name>" },
"purpose": "Persistent interaction that cannot be expressed with declarative page primitives."
}
]
}
app/ui/index.js must export register(registerComponent) and call
registerComponent("<Name>Page", Component, meta) for every route component.
The route component string and registration key must match exactly. Prefer the
AppGenerator custom_route_bundle contract for generated apps because it
creates route_manifest.json, ui/pages/custom/*.jsx, and ui/index.js
together.
Route/component registration contract:
app/ui/route_manifest.json declares custom full-page routes only.
app/ui/index.js must register every component referenced by route_manifest.json.
- each custom route must have exactly one matching
app/ui/pages/custom/*.jsx page file.
admin/admin_registry.yaml is for admin page and panel metadata, not arbitrary full-page custom route ownership.
- custom React routes are not auto-discovered; the manifest, page file, and registration barrel must be emitted together.
- missing or mismatched component registrations must be fixed before export/download.
Use custom routes sparingly. Declarative app/ui/pages/ is the default.
When a custom route is truly needed, it must still use the shared primitive
contract. Import shared primitives from @mozaiks/chat-ui/ui for actions,
status, cards/panels, metrics, search/list/table surfaces, loading/error states,
and empty states. Do not define local visual clones such as StatusPill,
MetricTile, StatCard, or Badge. Do not render raw primary-styled buttons
such as <button className="...bg-primary..."> when Button exists.
Use semantic tokens/classes only; visual values come from
app/brand/theme_config.json and the shell/theme token layer.
Use semantic variants on shared primitives (for example Button variant,
StatusPill tone, and panel/card variants) rather than local one-off visual
systems.
Do not hardcode hex/rgb/hsl colors or direct font-family declarations in custom
route React.
Do not define page-local palette, colors, or theme objects in custom
route React.
Avoid repeated local rounded card shells; use SurfaceCard/Panel when that
surface shape already exists.
Generated React audit scope note:
- quality-gate audits target generated app route React and intentionally ignore
docs/** and tests/** fixture paths to reduce false positives.
Brand and theme rules:
app/brand/theme_config.json is the visual identity source of truth.
app/config/shell.json owns shell/navigation/chrome behavior only.
- Use semantic primitive fields such as button
variant, status tone, card
or panel variant, density, and radius.
- Do not hardcode hex/rgb/hsl colors, font-family declarations, literal brand font
names, page-local palettes, or one-off button/card/badge styles.
- Local fonts belong under
app/brand/fonts/ and are referenced as
/fonts/...; never copy font binaries elsewhere.
Rules
- Prefer declarative YAML — if the primitives don't cover the use case, extend the primitive
- Pages belong in
app/ui/pages/ — never in a module's backend directory
api_endpoint paths must be /api/modules/{name}/{action_id} routes with no query strings or fragments. Put limits in page_size and filters/selected-row values in action payload, form state, or module action input schemas.
- Page-owned shell access belongs in the page's
navigation field. Use app/config/shell.json -> shortcuts for built-in chrome and navigation.policy for app-wide placement behavior.
- Page-owned chrome intent belongs in
shell_mode; use conversation for DM/chat routes and workspace for dense module/profile/admin-like pages.
- Custom route component keys must match exactly between
route_manifest.json, ui/pages/custom/*.jsx, and ui/index.js.
When to Use This Skill
- User wants to add a new page to a generated app
- User says "add a customers page" or "I need a dashboard"
- User wants to display module data in the app UI
- User asks "how do I add a page"