From ec87cae6c5f2c20ed45fa0916807c49a8137d377 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Wed, 1 Jul 2026 15:26:12 -0500 Subject: [PATCH] Update inner page layout spec for hero+intro refactor - Goal: hero is now a blue-to-teal gradient (not a flat dark color) and the intro block lives inside the hero composition (not as a separate page section). - Architecture: reduced from 3-part (hero / intro / body) to 2-part (hero / body) since the intro is now embedded in the hero. - Hero Partial: dropped the bg-dark utility (gradient is set in CSS), the hero image is anchored to the right with a left-to-right fade, and the vector is full-width anchored to the bottom at z-index 2. - Hero Partial CSS: the file is at styles/blocks/page-hero.css (not views/partials/page-hero.css). Updated the rules to match the new gradient + image right-side + vector bottom-center + intro grid layout. - Intro Partial: rewritten as a fragment for embedding inside the hero (two-column heading + body grid) rather than a centered standalone section. Partial is loaded by page-hero.php, not page.php. - page.php Wiring: no longer loads the page-intro partial directly; the partial is loaded by page-hero.php. - Error Handling, Verification, and Acceptance Criteria: updated to match the new structure (no empty .page-intro wrapper check; intro is a descendant of the hero; gradient + image-right + vector-bottom-center expectations). Co-Authored-By: Claude --- .../2026-07-01-inner-page-layout-design.md | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/docs/superpowers/specs/2026-07-01-inner-page-layout-design.md b/docs/superpowers/specs/2026-07-01-inner-page-layout-design.md index f45d48d..2019bfc 100644 --- a/docs/superpowers/specs/2026-07-01-inner-page-layout-design.md +++ b/docs/superpowers/specs/2026-07-01-inner-page-layout-design.md @@ -2,7 +2,7 @@ ## Goal -Build the inner page layout shown in `notes/inner-page-mockup.png` so that every WordPress page renders the same structure: a dark hero with a background image and a decorative vector on the right, a centered narrow intro section underneath, and the standard editor body below it. Different pages vary only by hero image, vector, intro text, and body content. +Build the inner page layout shown in `notes/inner-page-mockup.png` so that every WordPress page renders the same structure: a blue-to-teal gradient hero with a background image and a decorative vector, the page title, an optional intro block (heading + body) inside the hero, and the standard editor body below it. Different pages vary only by hero image, vector, intro text, and body content. ## Scope @@ -13,13 +13,14 @@ Build the inner page layout shown in `notes/inner-page-mockup.png` so that every ## Architecture -The layout is a fixed three-part composition: +The layout is a fixed two-part composition: -1. **Hero** — `views/partials/page-hero.php` (existing), extended with two new ACF fields and two new visual slots. -2. **Intro** — new `views/partials/page-intro.php`, rendered by `page.php` between the hero and `the_content` when the `intro` ACF field is non-empty. -3. **Body** — `the_content`, unchanged. +1. **Hero** — `views/partials/page-hero.php` (existing), extended with the blue-to-teal gradient backdrop, two new ACF fields, the hero image rendered on the right (faded to transparent on the left so the heading has a clear backdrop), a decorative vector along the bottom, the heading, and the intro block. +2. **Body** — `the_content`, unchanged. -`page.php` becomes the orchestrator: it decides whether to render the intro partial based on the `intro` ACF value, then renders the body. Sidebar behavior (already gated by `hasSidebar()` and the `Page Sidebar` field group) is preserved. +`page-hero.php` is the orchestrator: it renders the gradient backdrop, the optional hero image and vector, the heading, and the intro block all inside a single `.page-hero` wrapper. The `intro` ACF field, when set, renders as a two-column heading + body block inside the hero (not as a separate page section). The intro heading is a hard-coded "Our Work" label; the body is the editor-supplied `intro` text. + +`page.php` continues to render `the_content` after the hero. Sidebar behavior (already gated by `hasSidebar()` and the `Page Sidebar` field group) is preserved. `page.php` no longer loads the `page-intro` partial directly — the partial is loaded by `page-hero.php` instead. `hasPageHeader()` in `lib/extras.php` continues to gate the hero. The function's current behavior (hero shows when `hero_style !== 'none'`) is kept; no change to that filter is required. @@ -36,62 +37,61 @@ No other ACF group changes. The existing `heading`, `intro`, and `call_to_action Changes: +- The wrapper `
` no longer carries the `bg-dark` or `bg-cover bg-no-repeat` Tailwind utilities. The wrapper inherits a blue-to-teal gradient from `.page-hero` (defined in `styles/blocks/page-hero.css`), the same gradient used by the homepage-hero block. When the editor sets a `background_color` ACF field, the inline `style` attribute on the wrapper overrides the gradient with that solid color. - Read the two new fields (`hero_image`, `hero_vector`). -- Render `hero_image` as an absolutely-positioned `` inside a new `.page-hero__media` container, masked with a left-to-right fade so the text on the left remains readable. -- Render `hero_vector` as an absolutely-positioned, right-anchored, bottom-anchored `` inside a new `.page-hero__vector` container, with `aria-hidden="true"` and `role="presentation"`. -- Stop rendering the `intro` paragraph inside the hero. The `intro` field is now rendered in the new page-intro partial. (The heading still renders inside the hero, so the page title and the optional heading override work as before.) -- Keep the `heading`, `breadcrumbs`, and `call_to_actions` rendering unchanged. -- When no `hero_image` is set, the existing `background_color` style (or the default `bg-dark` token) is the only background. +- Render `hero_image` as an absolutely-positioned `` inside a new `.page-hero__media` container. The image is anchored to the right (`object-position: right center`) and masked with a left-to-right fade (transparent on the left, visible on the right) so the heading on the left has the gradient as a clean backdrop. +- Render `hero_vector` as an absolutely-positioned, full-width `` anchored to the bottom of the hero, with `aria-hidden="true"` and `role="presentation"`. The vector sits above the media layer (`z-index: 2`) and below the content (`z-index: 10`), so it is visible across the bottom of the hero. +- Render the heading (h1) inside the new `.page-hero__content` container, above the media layers. +- Load the `page-intro` partial inside `.page-hero__content`, below the heading. The intro block only renders when the `intro` ACF field is non-empty. +- Keep the breadcrumb rendering unchanged. +- When no `hero_image` is set, the gradient (or the override `background_color`) is the only background. Both new images are `loading="lazy"` and decorative. -## Hero Partial CSS — `views/partials/page-hero.css` (new) +## Hero Partial CSS — `styles/blocks/page-hero.css` (new) -The partial needs a small CSS file for the absolute positioning and mask treatment that Tailwind utilities do not express cleanly. A dedicated file follows the project's existing pattern (`contact-block.css`, `homepage-hero.css`). +A dedicated file follows the project's existing pattern (`contact-block.css`, `homepage-hero.css`). Rules: -- `.page-hero` — the existing wrapper. Add `position: relative` and `isolation: isolate` so the new media layers stack predictably. -- `.page-hero__media` — `position: absolute; inset: 0; z-index: 0; mask-image: linear-gradient(to right, transparent 0%, black 30%);` (mirrored `-webkit-mask-image`). The image inside it is `width: 100%; height: 100%; object-fit: cover;`. -- `.page-hero__vector` — `position: absolute; right: 0; bottom: 0; width: clamp(18rem, 38vw, 32rem); height: auto; z-index: 1; pointer-events: none;`. The mobile breakpoint (≤ 767px) repositions the vector to fill the lower portion of the hero: `width: 100vw; right: auto; left: 0; bottom: 0;` and the `__media` mask switches to a top-to-bottom gradient so the text remains readable. -- `.page-hero__content` (or the existing `.content-wrapper` div inside the hero) — `position: relative; z-index: 10;` so text always sits above the media layers. +- `.page-hero` — the wrapper. `position: relative; isolation: isolate; overflow: hidden;` so the new media layers stack predictably. `background: linear-gradient(83.68deg, #032F46 3.13%, #006196 45.91%, #8FC9E6 96.27%);` — same gradient as the homepage-hero block. +- `.page-hero__media` — `position: absolute; inset: 0; z-index: 1; -webkit-mask-image: linear-gradient(to left, transparent 0%, black 50%); mask-image: linear-gradient(to left, transparent 0%, black 50%);`. The image inside it is `width: 100%; height: 100%; object-fit: cover; object-position: right center;`. The mask fades the image from invisible on the left to fully visible on the right. +- `.page-hero__vector` — `position: absolute; left: 0; bottom: 0; width: 100%; height: auto; z-index: 2; pointer-events: none;`. The mobile breakpoint (≤ 767px) repositions the vector to fill the lower portion of the hero: `width: 100vw; right: auto;` and the `__media` mask switches to a top-to-bottom gradient. +- `.page-hero__content` — `position: relative; z-index: 10;` so the heading and intro block always sit above the media and vector layers. +- `.page-hero__intro` — `margin-top: clamp(2rem, 6vw, 4rem);` provides spacing between the H1 and the intro block. +- `.page-hero__intro-inner` — `display: grid; gap: clamp(1.5rem, 4vw, 3rem); grid-template-columns: 1fr;` on mobile, `grid-template-columns: 1fr 2fr;` on desktop, so the heading and body form a two-column layout matching the mockup. These rules are scoped to `.page-hero` and only affect the page-hero partial. They do not affect the homepage-hero block, which uses a different root class. ## Intro Partial — `views/partials/page-intro.php` (new) -A small PHP partial that renders a single centered column: +A small PHP partial that renders a two-column heading + body block **inside** the page hero: -- Wrapper: `
` -- Container: `
` — note: the `container` Tailwind utility is intentionally **not** used here. The project's `styles/base/global.css` defines `.container { max-width: 80.5rem; }` (1288px), and because `global.css` is loaded after the Tailwind utilities in the cascade, `container` would override `max-w-3xl` (768px) on desktop, producing an over-wide intro. Use only `mx-auto max-w-3xl` so the cap applies. -- Inner: a single `

` element with `text-lg lg:text-xl leading-relaxed` containing `wp_kses_post( $intro )`. +- Wrapper: `

` +- Inner: `
` — a CSS grid (1 column on mobile, 1fr / 2fr on desktop) holding: + - `

` containing the hard-coded label "Our Work". + - `

` containing `wp_kses_post( $intro )`. +- The partial returns early when the `intro` field is empty, so no empty wrapper is rendered. -No new fields, no new CSS file — Tailwind utilities cover the layout. +The partial is loaded by `page-hero.php` (not `page.php`). The intro is **part of the hero composition** in the rendered page, not a separate page section. ## page.php Wiring -`page.php` changes from rendering the body directly to: - -1. Output the article container (existing behavior). -2. If `get_field( 'intro' )` is non-empty, call `get_template_part( 'views/partials/page-intro' )` **as a sibling of** the `entry-content` div (not inside it). The intro must be a peer of the body in the layout flow, not a child, so the test's order assertion `introBox.y < contentBox.y` holds (when intro is a child of entry-content, the two bounding boxes share the same top y and the order check fails). -3. Render `the_content` (existing behavior). -4. Render the sidebar if `hasSidebar()` (existing behavior). - -The hero is still rendered by `header.php` before this template runs. No other behavior changes. +`page.php` continues to render the article container, `the_content`, and the sidebar (when `hasSidebar()`). The hero is rendered by `header.php` before this template runs. The `page-intro` partial is no longer loaded directly by `page.php` — it is loaded by `page-hero.php` (inside the hero wrapper). No other behavior changes. ## Data Flow - Editor experience: on every page (and post) edit screen, the existing Page Heading meta box shows the existing fields plus two new ones (Hero Image, Vector). The editor uploads the assets, sets a background color if desired, writes the optional heading override, intro, and CTAs, then writes the body in the standard block editor. -- Render: `header.php` loads the page-hero partial → `page.php` loads the page-intro partial (if intro is set) → `the_content` renders the body blocks. +- Render: `header.php` loads the page-hero partial → `page-hero.php` renders the gradient + optional hero image + vector + heading + (when `intro` is set) the intro block, all inside `.page-hero` → `page.php` renders `the_content` for the body blocks. - Per-page variation: hero image, vector, intro text, and body content. Everything else is shared. ## Error Handling and Edge Cases -- `hero_image` empty → hero uses the existing dark background color (or the default dark text-only style). -- `hero_vector` empty → no vector renders; the right side of the hero is empty space. -- `intro` empty → page-intro partial is not loaded; the page goes straight from hero to body. +- `hero_image` empty → hero uses the gradient (or the override `background_color`) as the only background. +- `hero_vector` empty → no vector renders; the bottom of the hero is just the gradient. +- `intro` empty → page-intro partial returns early; the hero renders without the intro block, and the page goes straight from hero to body. - `heading` empty → falls back to `getTheTitle()` (existing behavior, unchanged). -- Background color set without a hero image → existing behavior; no change. +- Background color set without a hero image → existing behavior; the inline `style` overrides the gradient with the solid color. - No sidebar field set → existing `hasSidebar()` filter returns false → no sidebar (existing behavior). - Lazy-loaded images must await `load`/`complete` before Playwright reads dimensions (existing project convention; see `tests/mobile-homepage.spec.js:641`). @@ -107,11 +107,11 @@ The hero is still rendered by `header.php` before this template runs. No other b ## Verification - Add `tests/inner-page.spec.js` covering: - - Desktop (1280px): hero, intro, body render in that order. When hero image is set, it is visible behind the hero. When vector is set, it sits on the right of the hero. - - Mobile (402px): same order. Vector fills the lower portion of the hero. Intro is centered and within the 48rem max-width. No clipped text. - - 320px and 767px: nothing clips, vector scales proportionally, intro remains readable. - - A page with no `intro` field renders hero + body with no empty intro wrapper. - - A page with no `hero_image` and no `hero_vector` renders the existing dark text-only hero unchanged. + - Desktop (1280px): hero, intro, body render with the intro block as a descendant of the hero. The heading and intro heading/body are all visible. The hero exposes the gradient backdrop. + - Mobile (402px): intro is inside the hero and fits within the viewport. The vector is visible at the bottom of the hero. No clipped text. + - 320px and 767px: nothing clips, hero and intro remain inside the viewport. + - A page with no `intro` field renders the hero without a `.page-hero__intro` wrapper. + - A page with no `hero_image` and no `hero_vector` renders the existing gradient text-only hero unchanged. - Add a test page fixture (or use an existing page) to drive the test. If no fixture exists, the test seeds a page in a `beforeAll` and tears it down in `afterAll`. - Run `npm run build` to regenerate `static/dist/`. - Run the full Playwright suite (`npx playwright test`) — including the existing `tests/mobile-homepage.spec.js` and `tests/site-a11y.spec.js` — to confirm no regressions. @@ -121,10 +121,10 @@ The hero is still rendered by `header.php` before this template runs. No other b ## Acceptance Criteria 1. Every WordPress page renders the new layout when the page-hero partial is active. -2. The hero shows the page title (or heading override) and, when supplied, a background image and a decorative vector on the right. -3. When the `intro` field is set, a centered narrow column appears under the hero. When empty, it is omitted entirely (no empty wrapper). +2. The hero shows the blue-to-teal gradient backdrop, the page title (or heading override), and — when supplied — a background image on the right side and a decorative vector along the bottom. +3. When the `intro` field is set, a two-column heading + body block appears inside the hero below the H1. When empty, it is omitted entirely (no empty wrapper). 4. The body renders `the_content` as before, with the standard block editor. -5. The hero background image is masked so text on the left remains readable at all viewports. +5. The hero background image is masked so it is visible on the right and fades to transparent on the left, so the heading on the left has the gradient as a clean backdrop at all viewports. 6. The vector scales proportionally from 320px through desktop and is repositioned below 768px to fill the lower portion of the hero. 7. The page renders without console errors, axe violations, or PHPCS errors. 8. Existing single posts, archives, search, and 404 pages render the page-hero partial unchanged.