Skip to content
Back to skills

Core Route Headers

ASecurity

(core) route header and body-height conformance via PageHeader. Use when fixing a (core) page's header, title bar, breadcrumbs, sub-route nav, body height (h-page, calc(100dvh…)), mobile header, or buttons hidden behind the avatar, or on a header-conformance assignment. NOT for /administration/* routes.

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 3, 2026
developmentrustgoshellbashrailsapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 3, 2026

npx -y skills add armanisadeghi/ai-matrx --skill core-route-headers --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Core Route Headers?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Core Route Headers
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/armanisadeghi-core-route-headers/badge)](https://www.skillsdirectory.com/skills/armanisadeghi-core-route-headers)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: core-route-headers
description: "(core) route header and body-height conformance via PageHeader. Use when fixing a (core) page's header, title bar, breadcrumbs, sub-route nav, body height (h-page, calc(100dvh…)), mobile header, or buttons hidden behind the avatar, or on a header-conformance assignment. NOT for /administration/* routes."
---

# core-route-headers — (core) shell header + page conformance

**Read `features/shell/components/header/variants/USAGE.md` first** — it is the canonical spec for the injection zone, variants, and body-height table. This skill is the *fix workflow*: how to classify a broken route, which exemplar to copy, and how to verify in the browser on desktop AND mobile.

For the cross-page composition rules that accompany this header contract (one
coherent control, icon/title rows, intentional spacing, tap targets, and no
competing route menus), also read the shared policy at
`/policies/no-dead-ends.md`.

## The mental model (one paragraph)

The shell header is a **transparent glass strip** over the page. The shell owns the far edges (hamburger left, org/canvas/avatar right); the route owns ONLY the center zone via `<PageHeader>` (`features/shell/components/header/PageHeader.tsx` — Server Component, portals into `#shell-header-center`; `desktop=` / `mobile=` props for per-breakpoint content). `.shell-main` is pulled up under the header with a negative margin, so the page body **already fills the viewport** and content scrolls *behind* the glass. Everything broken in this repo is one of two sins: rendering header chrome **in the body**, or **subtracting the header height** from the body.

## Gold standards — copy these, don't invent

| Route | What it proves | Key files |
|---|---|---|
| `/agents/all` | List page: `PageHeader` + injected list header, body scrolls behind glass | `app/(core)/agents/all/page.tsx`, `packages/chat/src/agents/components/shell/AgentsListHeader.tsx` |
| `/agents/[id]/build` | The masterpiece: back tap-target + `[id]` dropdown selector + measurement-collapsing mode nav + save/options — **no title/description block** | `packages/chat/src/agents/components/shared/AgentHeader.tsx`, `AgentModeController.tsx` |
| `/chat/[conversationId]` | Scroll-behind-glass body + left/right icons + **route-injected sidebar menu** (no double menu) | `app/(core)/chat/[conversationId]/page.tsx`, `features/shell/constants/route-menu-registry.ts` |
| `/organizations/[orgId]/scopes/**` | Best breadcrumb nav: full-path breadcrumb, per-level sibling dropdowns, mobile drawer — mounted ONCE at the layout | `features/scope-system/components/ScopesRouteHeader.tsx`, mounted in `app/(core)/organizations/[orgId]/layout.tsx` |
| `/tasks` | Resizable panels: per-panel `pt-[var(--shell-header-h)]` only where static top UI must clear the glass | `app/(core)/tasks/` |

**Anti-examples** — the two CMS pages formerly listed here (`app/(core)/cms/page.tsx`,
`app/(core)/cms/html-pages/page.tsx`) have both been **fixed** (verified 2026-07-28) and are
now a *positive* example instead: they share ONE `CmsHubHeader`
(`features/cms/components/CmsHubHeader.tsx`) wrapping `RouteHeader` + `RouteModeNav`, with
no title text — the section nav IS the identity, and each page passes only its contextual
tap-buttons via `right`. Copy that shape for any two-or-more-page hub.

To find live offenders, don't trust a list in this file — run `pnpm check:page-headers` and
grep the route for the failure classes below. A hardcoded anti-example list rots the moment
someone fixes it.

## Failure classes — classify before you touch anything

1. **Faux in-body header** — a body-rendered bar with title + actions (`border-b` + `bg-card`/`bg-background/*`). Move controls into `<PageHeader>`; delete the bar.
2. **Banned height math** — `h-[calc(100dvh-…)]`, `h-page`, `h-screen`, `min-h-screen` on a (core) body. Replace with `h-full overflow-hidden` (see USAGE.md body-type table).
   A **route layout boundary** is different: it must never put `{children}`
   behind unconditional vertical clipping. Use `overflow-y-auto
   overflow-x-hidden` as the fallback there; full-height leaf editors and
   tables retain their own inner scroll, while natural-height leaves cannot be
   amputated by one missed class.
3. **Title/description block in a dashboard page** — marketing copy inside app chrome. **Delete it**; the header center carries a `text-sm` title at most.
4. **Missing top clearance for floating/static content** — content that must NOT slide behind the glass (grid of cards with action buttons, sticky toolbars) needs `pt-[var(--shell-header-h)]` (never a hardcoded `pt-12`); freely-scrolling content gets **no** top padding so it floats behind the glass. This is the `/agents/all` mobile bug: card buttons float up into the header.
5. **Desktop actions vanish on mobile** — `hidden lg:flex` with no mobile counterpart. The rule: desktop actions collapse into one or two **bottom sheets** on mobile (`HeaderActions` in `features/shell/components/header/variants/shared/` renders `BottomSheet` below `lg`; or `BottomSheet` from `@ai-matrx/design-system` directly). `packages/chat/src/agents/components/shared/AgentHeaderMobile.tsx` exists but is commented out in `AgentHeader.tsx` — that class of gap.
6. **Double menus** — a page-level nav next to the sidebar. Large routes register ONE menu in `features/shell/constants/route-menu-registry.ts` (desktop `RouteMenuSlot` + `MobileRouteMenuSlot` come free).
7. **Avatar collision** — `ml-auto` / `justify-between` actions in the body drifting behind the fixed avatar (the old `pr-14` hack). Fixed automatically by moving actions into the bounded center zone.

## Templates — reach for these FIRST

Two drop-in templates in `features/shell/components/header/templates/` cover almost every route; consuming one is the default, hand-rolling is the exception:

- **`EntityModeHeader`** — the agents pattern for any `[id]` route: back chevron + entity-name **sibling dropdown** + center `RouteModeNav` + **declarative `actions`** (`{label, icon, onPress|href, primary?, destructive?}`). Desktop renders them as tap targets; **mobile renders back + name + ONE `…` that opens a bottom drawer** containing modes AND actions. Reference consumer: `/schedules/[id]` (`features/scheduling/components/detail/ScheduleDetail.tsx`).
- **`CrumbTrailHeader`** — the org/scopes pattern for drill-down hierarchies: clickable trail, per-level sibling dropdowns, mobile collapses to the last crumb.
- **`MobilePanelShell`** — any multi-pane route (sidebar+detail, IDE, inspector rails): pass the existing desktop layout **verbatim** as `desktop`, plus `main` + `panels`; below `md` each side panel becomes a bottom drawer behind ONE `…` tap target. A genuinely dense workspace may set `collapseBelow="lg" | "xl" | "2xl"` so panels collapse before they crush the primary work; the default stays `md`. It auto-dismisses on **route change only** — an in-panel action that is not navigation (picking a session, flipping a `?search_param`, firing a run) must close the drawer itself by calling **`useMobilePanelClose()`** at the action site. That hook is a no-op outside a drawer, so the same component is safe in the desktop pane. Consumers: `/rag/data-stores` (store rows), transcripts studio (`StudioSidebar` pick/create), transcripts cleanup (`CleanupPad` Clean Up), agent Hindsight (`collapseBelow="2xl"`).

**Button rules** (live catalogue: `/demos/button-demo`):
- Tap buttons stay `glass` (the default) — never pass `variant="transparent"` in a header row; icons must match the shell's own buttons.
- The page's ONE primary action → `variant="solid"` (primary). Delete/remove → solid `bgColor="bg-destructive" hoverBgColor="hover:bg-destructive/90"`.
- **Standalone tap buttons self-space (44px)** — never wrap `TapTargetButton` controls in `gap-*`/`p-*`.
- **Route navigation stays compact at every width.** `RouteModeNav` items match the Agents header; never add `min-h-11`, `min-w-11`, responsive padding, or a 44px minimum to its visible pill. Mobile reachability comes from its 52px bottom-sheet rows, not a taller shell header.
- Related actions can share a `TapTargetButtonGroup` (`TapTargetButtonForGroup` children). Search surfaces: see the SearchToolbar / SearchGroup patterns in the demo.

**Mobile doctrine:** don't cram. As few header items as possible — back + identity + one `…` → bottom drawer. A truncated 8-letter dropdown is worse than a drawer. Only keep richer mobile headers where a route has a genuinely great concept (chat).

## Compose with RouteHeader — the default

`RouteHeader` (`features/shell/components/header/RouteHeader.tsx`) is the canonical three-part injection: `left` (back tap-target + identity), `center` (the ONE nav/selection control — usually `RouteModeNav`), `right` (tap-target actions). Prefer it over hand-rolled `<PageHeader>` rows and over `HeaderStructured` whenever the route has sub-views. **No title text in the center when a nav can live there** — "Content Manager"-style labels are filler; the nav IS the identity. A lone `text-sm font-medium` title on the left is fine for single-view routes.

Sibling routes sharing one header (e.g. `/cms` + `/cms/html-pages`) get ONE shared header component in the feature (`features/cms/components/CmsHubHeader.tsx` is the reference) — never two diverging copies.

## Fix recipe by route archetype

- **List/gallery page** → copy `/agents/all`: `PageHeader` with a small injected header (search / filters / New), body `h-full overflow-hidden`, scroll container inside, floating grid content gets `pt-[var(--shell-header-h)]` if it has interactive elements at the top.
- **Detail/editor `[id]` page** → copy `/agents/[id]/build`: `ChevronLeftTapButton` back (from `@ai-matrx/tap-target/buttons`), entity **dropdown** (not a static title) for `[id]` routes, actions right. No title/description prose.
- **Sub-mode family** (build/run/templates…) → `RouteModeNav` (`features/shell/components/header/RouteModeNav.tsx`) — measurement-driven full → icons → menu collapse. It is the ONE control for mode switching; never pair it with a second selector. **Give every item an `icon`** — the icon-only stage is skipped entirely when even one lacks it, so the nav jumps straight from full text to a dropdown.
- **Drill-down hierarchy** (org → type → item…) → `ScopesRouteHeader` pattern: one layout-level breadcrumb header, pathname-gated, per-level sibling dropdowns, mobile drawer.
- **Actions overflow** → `HeaderStructured` / `HeaderActions` (see USAGE.md variants) — inline on `lg+`, glass dropdown for overflow, `BottomSheet` on mobile.
- **Right slot** (save status, page-scoped icons) → `PageHeaderRightPortal` (`features/shell/components/header/PageHeaderRightPortal.tsx`).

Per-breakpoint content: `<PageHeader desktop={…} mobile={…} />`. Mobile never gets *nothing* — at minimum back + title + one sheet trigger holding the desktop actions.

## Detection — finding offenders

```bash
pnpm check:page-headers            # faux-header markers (KNOWN NARROW — misses bg-background/* bars)
pnpm check:scroll-chain:strict     # all route pages/layouts + cross-component bounded-height chains
grep -rln "calc(100dvh\|calc(100vh\|h-screen\|h-page" app/\(core\) --include="*.tsx"
grep -rLn "PageHeader" <route dir>  # route family never injecting the header
grep -rn "pt-12\|pt-10\|pt-8" <route dir>  # hardcoded header offsets → var(--shell-header-h)
```
Find a new faux-header class combo? **Add it to `FAUX_HEADER_MARKERS` in `scripts/check-page-headers.ts`** in the same change — the guard must learn what you learned.

## RouteModeNav — the three-stage contract

The center nav MUST degrade **icon + text → icon only → `…` menu**, driven by measured space, never by breakpoints. All three stages are load-bearing; a nav that skips one is a defect. Two invariants keep it honest (both were live bugs on `/marketing`, fixed 2026-07-20):

- **Each hidden measurer carries `w-max`.** They are block-level siblings in one shrink-to-fit absolute box, so without it every measurer stretches to the widest one and the icons measurer reports the FULL width — making `iconsW <= avail` unreachable and turning the icon-only stage into dead code.
- **The fit test reserves `FLANK_GUTTER` (32px).** `centerSlotWidth` returns the theoretical maximum, so an exact-fit test picks `full` when the pill is 1px from the shell's own icons. Collapse *before* the flanks are touched, not when they collide.

**Selection must be obvious in BOTH themes.** Never style the selected item with `--matrx-glass-bg-active` directly — that low-alpha tint reads in dark and vanishes against light-mode glass, which is exactly how the selected route became invisible in light. Use the `--shell-nav-selected-bg` / `-text` / `-shadow` + `--shell-nav-unselected-text` tokens (`styles/shell.css`): light resolves to a solid raised pill with a real text-colour delta, dark keeps the glass tint. Selection also needs a genuine *unselected* baseline — `--shell-nav-text` (0.82 black) sits too close to its own hover value to signal state.

## Gotchas proven in the field

- **Radix `asChild` wrappers take ONE child.** Injecting `<PageHeader>` as a sibling inside `NonEditableContextMenu`/any trigger wrapper crashes with "Primitive.span failed to slot". Put the header OUTSIDE the wrapper.
- **Turbopack HMR corruption can 404 sub-routes** (route matches on baseline, 404s after rapid edits, root still 200). Recompile/restart the dev server before diagnosing your diff.
- **`RouteModeNav` root must be `w-full`** — it measures its own box; without `w-full` a compact first paint locks it in the "menu" variant forever (fixed 2026-07-13; don't regress it).
- **Mobile header budget:** left identity truncates (`max-w-[100px] sm:max-w-[180px]`), secondary right actions hide below `sm` (`hidden sm:inline-flex`) — one primary action stays.

## Verify in the browser — mandatory, both viewports

1. Reuse a running dev server (`pnpm dev:status`) or `preview_start`; log in via `/login` with `admin@admin.com` / `<see AI_ADMIN_PASSWORD in .env>`.
2. Navigate to the route. **Desktop (1280×800):** no in-body title bar; actions in the header center; nothing behind the avatar; no dead strip at the bottom; content scrolls behind the glass.
3. **Intermediate (~700–900px):** the center nav must have already stepped down a stage rather than sitting flush against the shell icons. Skipping this width is how a nav that only ever does full → menu passes review.
4. **Mobile (375×812 via `resize_window`):** every desktop action reachable (bottom sheet, not vanished); no interactive element floating into the glass header; single scroll area.
5. **Both themes.** The app's theme is a `.dark` class, NOT `prefers-color-scheme` — `resize_window`'s `colorScheme` does nothing. Toggle with `document.documentElement.classList.toggle('dark')`. Light mode is where selected-state contrast dies; check it explicitly.
6. Screenshot each. A visible flaw in your own screenshot is a failure — fix it, don't present it.

## Do not break

- `/administration/*` and `(transitional)`/`(legacy)` sit **below** the header — `h-page`/header-calc is *correct* there. Never "fix" them with this recipe.
- The rules in `app/(core)/_read_first_route_rules/` and the scaffold in `.claude/skills/new-route-scaffold/SKILL.md` (SSR, hydrators, skeleton `loading.tsx` — no spinners).
- Mobile rules in `.claude/skills/ios-mobile-first/SKILL.md` (`h-dvh` not `h-screen` for non-shell pages, `pb-safe`, drawer-not-dialog).
- Lucide only, no emojis, semantic color classes, component-library loading states.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…