Skip to content

Eagle Public Page Layout

Daniel Truong edited this page Oct 20, 2026 · 5 revisions

Eagle Public Page Layout

React eagle-public (react line) draws every page from one set of layout primitives: a page container width, a shared blue masthead band, and a breadcrumb trail.

Add a page

  1. Wrap the page's content in page-container, or nest it inside a band that already carries the class, such as PageMasthead's inner wrapper.
  2. Use PageMasthead for a page that opens with a titled band. Pass title and, unless the page is the site root, breadcrumbs ending in the current page.
  3. Add no competing width, max-width or side-padding rule. If a bespoke rule is unavoidable, use --page-max-width or --page-gutter. Add no text measure cap.
  4. Let a full-bleed page (like the map) opt out of the container on its own route class, not by overriding .page-container globally.

Page container

src/assets/styles/base/layout.css states the content width once, as CSS custom properties on :root:

Token Value Notes
--page-max-width 1440px Cap on every content band.
--page-gutter 16px below 860px, 24px from 860px up Side padding.

Text has no reading-width cap of its own. A long-text block (.compliance-tab__intro, .content-wrapper) runs the full width of its container, bounded by --page-max-width and the gutter.

The .page-container class applies max-width, centering and the side gutter. Bootstrap's .container, .container-fluid and the .container-{sm,md,lg,xl,xxl} family share the same rule, so a static page or the comments page that still carries Bootstrap markup lines up with the rest of the site.

Put .page-container on the element inside a full-width band, not on the router outlet (app-shell.tsx's <main class="app-content">). A band's background (masthead, footer, hero strip) runs edge to edge, and only its content is capped and gutter-padded. On the outlet, the class would cap every band's background too.

The map route opts out. app-shell.tsx adds a projects-route class to .app-content when the path starts with /projects. src/app/pages/projects/projects.css uses .app-content.projects-route:has(.projects-view) to let the map page run full width and full height.

PageMasthead

src/app/layout/page-masthead.tsx renders the blue band every page opens with. It is a <section> with a page-masthead__inner page-container wrapper, so its content lines up with the header, the footer and the rest of the page.

Prop Type Purpose
title ReactNode The page's h1. A node, so a page can show a loading placeholder while the real title fetches.
breadcrumbs Crumb[] Root first, current page last. Omitted on the site root.
meta ReactNode One line under the title.
actions ReactNode Controls that sit under the title and meta line, not beside them.
children ReactNode Extra content the page puts inside the band, under the title row.
busy boolean Sets aria-busy while the page's title data is loading.
className string Extra class on the band, for a page that styles its own actions or content.

The band always renders a breadcrumb row, even when a page passes no breadcrumbs. The row holds its height (min-height: 1.313rem) either way, so the title sits at the same vertical offset on every page.

From 860px wide, the band has a min-height of 14rem. That is the tallest band the site has (search, whose title is followed by a keyword field and a record-type row).

The title and meta line clip to one line with an ellipsis, so a long name cannot push the band past it. e2e/tests/page-layout.spec.ts checks that every band lands on this floor. Below 860px the band is sized by its content, because a project name and the home search row wrap by different amounts on a phone.

PageMasthead is used by:

  • the home page (src/app/pages/home/home.tsx)
  • search (src/app/pages/search/unified-search.tsx)
  • project details, through src/app/pages/project/project-masthead.tsx, which fills the band with the project's name, proponent/location as meta, and subscribe/copy-link buttons as actions

Breadcrumbs

src/app/layout/breadcrumbs.tsx exports Breadcrumbs({ items }), where each item is a Crumb:

interface Crumb {
  label: string;
  to?: string; // omitted, or ignored, on the last crumb
}

The trail is ordered root first. The last crumb is the current page and renders as plain text (aria-current="page"), never a link. The whole trail sits on one line and ellipses rather than wrapping.

The project details trail is Home > Search > the project's name. The middle crumb links to searchUrl('projects') (src/app/routes/legacy-search.ts), not a bare /search, so it lands on the projects record type.

Search grid columns

src/app/components/display-grid/types.ts marks a column defaultHidden: true when it starts switched off. The column picker still lists it so a reader can turn it on.

The grid's URL state (src/app/components/display-grid/use-grid-url-state.ts) reads that default through the cols query param:

  • no cols in the URL: the record type's own defaultHiddenColumns apply.
  • cols=none: every column shows, including ones that default to hidden.
  • cols=a,b: exactly the named columns are hidden.

Projects sort by name ascending (+name, src/app/pages/search/types/projects.ts) unless the URL names another sort.

HTML-to-text

htmlToText (src/app/utils/safe-html.ts) is the one helper for turning stored HTML into plain text. It sanitizes with DOMPurify, inserts spaces around elements that a reader would see a line break around, and collapses whitespace. Use it for any plain-text rendering of a rich-text field (a meta line, a card summary), not a one-off regex.

E2E coverage

e2e/tests/page-layout.spec.ts checks, at viewport widths 390, 860, 1024 and 1600:

  • the page container's content and the footer share the same left/right edges under a full-width header, on home, search, a static page and a project page
  • every masthead band shares one background, one top padding and one title offset
  • the masthead's floor height (14rem) holds from 860px up
  • a project band's actions sit under its description, not pushed out to the right

The visual parity suite (e2e/parity) drives /search against the design prototype, state by state, and checks structural measurements and a pixel screenshot per state.

The pixel comparison is parked behind a PIXEL_COMPARISON_PARKED flag in unified-search.parity.spec.ts, because the reference screenshots do not match the built page. Structural checks still run. Unparking needs new references captured from an updated design handoff, with yarn parity:reference.

For project pages drawn from a content file, see Extended Project Pages.

Clone this wiki locally