The rules — how every app page is put together
- A page is
{layout $_shell}+{block content}— the shell renders the whole chrome (sidebar, topbar, footer). - The route's
title,subtitleandbreadcrumbs(route.php) render in the topbar — never write your own in-page<h1>. The one exception is the page-header block below. - Content is cards on the grid. A card owns its vertical spacing (its
margin-bottomequals the content padding) — never addmt-*/mb-*between stacked cards; the rhythm on this very page comes from zero spacing utilities. - One
.btn-primaryper page (its main action, in the page header) and at most one per card (in the card header) — everything else is.btn-secondary. - Theme-aware always: muted text is
.text-body-secondary, colored chips ride thebg-*-subtle text-*-emphasispairs, borders usevar(--bs-border-color)— never hardcoded colors. Both themes and mobile are part of done.
Page header with actions — only when the page has page-level actions
Orders
142 orders this month
- A default page renders no in-page header — the topbar already shows the route title. Add
.page-headeronly when the page carries page-level actions (or a summary line worth keeping in-page). - The title is the route's own (
$page->title) in.page-title— never a different text than the topbar. - Actions sit right, in
.page-header-actions.d-flex.gap-2: at most ONE.btn-primary(icon + label), secondaries as.btn-secondary.
The standard grids — pick one of these three
Stat row — four up:
col-12 col-sm-6 col-xl-3stat
stat
stat
stat
Content + aside (the default two-column page) —
col-12 col-xl-7 / col-12 col-xl-5main — the list / table / form
aside — secondary cards
Halves —
col-12 col-lg-6half
half
- Always start from
col-12— pages are mobile-first and columns stack on small screens. - Cards in columns use the plain
.row— its default gutter matches the page rhythm (the tinted boxes above are a visualization, not cards;g-3there is demo-only). - A page that is ONE data list is the full-width lister, no grid at all (see the data-list page) — data lists are lister CARDS by default, not html tables (tables).
- Do not invent other splits: stat row, 7/5, halves and full-width cover the app pages; nesting cards inside cards is forbidden.
Where the rest lives
- Docs (the contract):
templates/app/docs/components/layout.md— anatomy, adding a page;cards.md— card variants;foundation.md— tokens, spacing, themes. - UI vocabulary (buttons, badges, alerts, toasts): /standards/ui.
- Empty / loading / error states: /standards/states.
- Full page compositions: dashboard, data list, detail, settings.