diff --git a/docs/superpowers/plans/2026-11-03-mega-services-accordion.md b/docs/superpowers/plans/2026-11-03-mega-services-accordion.md new file mode 100644 index 0000000..4dcd49d --- /dev/null +++ b/docs/superpowers/plans/2026-11-03-mega-services-accordion.md @@ -0,0 +1,315 @@ +# MEGA Services — "Five layers" accordion, Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the MEGA Services section with a five-slice horizontal accordion that expands on hover or keyboard focus. + +**Architecture:** A pure-CSS flex accordion — each slice is `flex-1` and transitions `flex-grow` to `4.2` on hover and on `:focus-within`. No JavaScript state, no new dependency. The slice data is derived from the existing `services` array so the two cannot drift apart. + +**Tech Stack:** React 19, TypeScript 5.8, Vite 6, Tailwind 3.4. + +## Global Constraints + +- **No new runtime dependency.** `app/package.json` gains no `dependencies` entry. +- **Pure CSS interaction.** No `useState` for the accordion; hover and `:focus-within` do the work. +- **`services` must not change.** The quote form's service dropdown uses all seven services. +- **`erasableSyntaxOnly` and `verbatimModuleSyntax` are on.** Type-only imports use `import type`. +- **`allowImportingTsExtensions` is on.** Relative imports of local modules carry the `.ts` extension. +- **`#services` anchor must survive**, and the section keeps its `01` label. +- **All copy lives in `src/data/content.ts`.** +- **Commit to `main` directly.** No PR workflow; the client cannot use the Forgejo web UI to merge. + +--- + +### Task 1: Derive the five layers from `services` + +**Files:** +- Modify: `app/src/data/content.ts` + +**Interfaces:** +- Consumes: the existing `services: Service[]` array. +- Produces: `export type Layer = { id: string; kicker: string; title: string; body: string; scope: string[]; image: string }` and `export const layers: Layer[]` — exactly five entries, ordered earthworks → municipal → stormwater → roads → electrical. + +- [ ] **Step 1: Add the layer type and the derived array** + +In `app/src/data/content.ts`, immediately after the `services` array, add: + +```ts +export type Layer = { + id: string + kicker: string + title: string + body: string + scope: string[] + image: string +} + +// The five layers every site is built from, in the order they go into the +// ground. `services` stays the full list the quote form offers. +const layerIds = ['earthworks', 'municipal', 'stormwater', 'roads', 'electrical'] as const +const layerTitles: Record = { municipal: 'Water & Sewer', roads: 'Roads & Paving' } +const layerKickers = ['Layer one', 'Layer two', 'Layer three', 'Layer four', 'Layer five'] + +export const layers: Layer[] = layerIds.flatMap((id, i) => { + const s = services.find((x) => x.id === id) + if (!s) return [] + return [ + { + id: s.id, + kicker: layerKickers[i], + title: layerTitles[s.id] ?? s.title, + body: s.short, + scope: s.scope, + image: s.image, + }, + ] +}) +``` + +- [ ] **Step 2: Verify the derivation produces five layers** + +Run: `cd app && npx tsx -e "..."` — if `tsx` is unavailable, verify by reading the file instead. Simpler check that needs no extra tooling: + +Run: `cd /Users/david/DEVELOPMENT/MEGA-WEBSITE && grep -n "const layerIds" app/src/data/content.ts` +Expected: the five ids appear in the order `earthworks, municipal, stormwater, roads, electrical`. + +- [ ] **Step 3: Type-check** + +Run: `cd app && npx tsc -b` +Expected: PASS with no errors. `services.find()` returns `Service | undefined`, which the `if (!s) return []` guard handles. + +- [ ] **Step 4: Commit** + +```bash +git add app/src/data/content.ts +git commit -m "Derive the five site layers from the services list" +``` + +--- + +### Task 2: The accordion component + +**Files:** +- Create: `app/src/components/LayerAccordion.tsx` + +**Interfaces:** +- Consumes: `layers` and `img` from `@/data/content`. +- Produces: `export function LayerAccordion(): JSX.Element`. + +- [ ] **Step 1: Write the component** + +Create `app/src/components/LayerAccordion.tsx`: + +```tsx +import { img, layers, type Layer } from '@/data/content' + +// One slice of the accordion. On desktop the row is a flex line and the slice +// grows on hover or keyboard focus; on mobile it is a full-width card with +// the copy always visible. +function Slice({ layer }: { layer: Layer }) { + return ( +
+ {/* Photograph */} +
+ + + {/* Scrim, so the copy stays legible over the photograph */} +
+ ) +} + +export function LayerAccordion() { + return ( +
+ {layers.map((layer) => ( + + ))} +
+ ) +} +``` + +- [ ] **Step 2: Type-check** + +Run: `cd app && npx tsc -b` +Expected: PASS with no errors. + +- [ ] **Step 3: Commit** + +```bash +git add app/src/components/LayerAccordion.tsx +git commit -m "Add the five-layer services accordion" +``` + +--- + +### Task 3: Rewire the Services section + +**Files:** +- Modify: `app/src/components/Services.tsx` + +**Interfaces:** +- Consumes: `LayerAccordion` from `./LayerAccordion`. +- Produces: `export function Services(): JSX.Element` — unchanged export, unchanged `id="services"`. `WhyUs` is untouched. + +- [ ] **Step 1: Replace the Services component** + +In `app/src/components/Services.tsx`, replace everything from the top of the file down to the end of the `Services` function (that is, keep `export function WhyUs()` and everything after it exactly as it is) with: + +```tsx +import { ArrowUpRight } from 'lucide-react' +import { pillars } from '@/data/content' +import { LayerAccordion } from './LayerAccordion' +import { Reveal, SectionLabel } from './Reveal' + +export function Services() { + return ( +
+
+
+ + Services +

+ Five layers. None of them optional. +

+
+ +

+ This is the whole method, start to finish. Five layers on every site, in the same order, since 2007 — and it is not + going to start now. +

+
+
+ +
+ +

+ Civil design and residential development are also in-house.{' '} + + Tell us what you need + + . +

+
+
+
+ ) +} +``` + +The old `useState`, `Plus`, `img`, `services` and `LoopVideo` imports are no longer needed by `Services`; `WhyUs` needs only `pillars`, `Reveal` and `SectionLabel`. Remove the imports that are no longer referenced and keep the `WhyUs` function byte-for-byte as it is. + +- [ ] **Step 2: Confirm the old per-service anchors are gone** + +Run: `cd /Users/david/DEVELOPMENT/MEGA-WEBSITE && grep -rn "service-" app/src || echo "no references"` +Expected: `no references` — nothing outside `Services.tsx` ever linked to `#service-`. + +- [ ] **Step 3: Build and test** + +Run: `cd app && npm run build && npm test` +Expected: the build succeeds and all 12 unit tests still pass. + +- [ ] **Step 4: Commit** + +```bash +git add app/src/components/Services.tsx +git commit -m "Replace the services list with the five-layer accordion" +``` + +--- + +### Task 4: Verify in the browser + +**Files:** +- No file changes expected. Fix anything the checks surface, then re-run this task. + +**Interfaces:** +- Consumes: the site from Task 3. + +- [ ] **Step 1: Start the dev server** + +Run: `cd app && npm run dev` (background) +Expected: listening on port 5173. + +- [ ] **Step 2: Check the desktop accordion** + +Open `http://localhost:5173/#services` in the in-app Browser. Confirm: +- five slices, each collapsed to an equal width, each showing its vertical title; +- hovering a slice widens it and brightens its photograph, and the panel fades in with kicker, title, description and chips; +- moving the pointer away collapses it again. + +Take a screenshot of a slice expanded. + +- [ ] **Step 3: Check keyboard focus** + +Tab into the accordion and confirm the focused slice expands exactly as it does on hover. Confirm the tab order walks the five slices. + +- [ ] **Step 4: Check the mobile stack** + +Use chrome-devtools MCP `emulate` with viewport `375x812x2,mobile,touch`. Confirm the slices stack vertically as full-width cards, the copy is visible without any interaction, and `document.documentElement.scrollWidth` equals the viewport width. + +- [ ] **Step 5: Check the console and network** + +Query the browser console for errors and the network log for failed requests. Expect no errors and no 404s — every `img/photos/*.webp` must resolve. + +- [ ] **Step 6: Commit any fixes** + +If the checks surfaced bugs, fix them, re-run `npm test` and `npm run build`, and commit: + +```bash +git add -A app/src +git commit -m "Fix services accordion issues found in browser verification" +``` + +If nothing needed fixing, skip this step. + +--- + +## Self-review against the spec + +| Spec requirement | Covered by | +|---|---| +| Exactly five slices, in build order | Task 1, Task 4 step 2 | +| Hover expands and brightens | Task 2 (Tailwind hover variants), Task 4 step 2 | +| Keyboard focus expands identically | Task 2 (`:focus-within`), Task 4 step 3 | +| Kicker, title, description, chips on the panel | Task 2 | +| Mobile stacks vertically, no overflow | Task 2 (layout classes), Task 4 step 4 | +| Heading and intro | Task 3 | +| "Also in-house" line linking to `#contact` | Task 3 | +| Build and tests pass | Task 3 step 3, Task 4 | +| No new dependency; `services` unchanged | Global Constraints, Task 1 | +| `#services` anchor and quote dropdown intact | Task 3 (id kept), Global Constraints | diff --git a/docs/superpowers/specs/2026-11-03-mega-services-accordion-design.md b/docs/superpowers/specs/2026-11-03-mega-services-accordion-design.md new file mode 100644 index 0000000..507545b --- /dev/null +++ b/docs/superpowers/specs/2026-11-03-mega-services-accordion-design.md @@ -0,0 +1,122 @@ +# MEGA Services — "Five layers" accordion + +**Date:** 2026-11-03 +**Status:** approved (design), pending implementation +**Reference:** Stackless Coffee "Ritual" section (`src/components/Ritual.jsx` + the `.acc` rules in `src/index.css`) + +--- + +## 1. Goal + +Replace the current Services section — a sticky portrait photograph beside an expandable list of all seven services — with the horizontal accordion treatment from the Stackless Coffee site: five vertical slices that expand across the frame on hover or keyboard focus, each carrying its own photograph, kicker, title, one-line description and scope chips. + +## 2. Decisions taken + +| Decision | Choice | +|---|---| +| What becomes a slice | The **five site-building layers**: bulk earthworks, water & sewer, stormwater, roads & paving, electrical reticulation | +| The current layout | The sticky photograph and the service list are **both replaced** by the accordion | +| Mobile | The slices **stack vertically**, each a full-width card with the copy always visible | +| Heading | **"Five layers. None of them optional."** | +| Design & Residential Development | Kept as a quiet line under the accordion, not as slices | +| Interaction | Pure CSS — `flex-grow` on hover, `:focus-within` for keyboard. No JavaScript state | + +## 3. The reference mechanic + +The Stackless accordion is pure CSS: + +- The row is a flex container; each slice is `flex: 1 1 0` with `overflow: hidden`. +- On hover (or focus) the slice's `flex-grow` transitions to `4.2`, so it takes roughly 41% of the row and its neighbours compress. +- The photograph fills the slice, dimmed (`saturate(0.55) brightness(0.42)`) and scaled up slightly; on hover it brightens and settles. +- A vertical title label (`writing-mode: vertical-rl`, rotated 180°) sits at the bottom of the collapsed slice and turns ember on hover. +- A panel — kicker, title, body — sits over the slice with `opacity: 0` and a small downward offset; on hover it fades in and rises. + +MEGA keeps the mechanic but swaps the ember accent for gold and the display weights for MEGA's editorial type. + +## 4. Architecture + +### Files + +| File | Change | +|---|---| +| `app/src/data/content.ts` | Adds `Layer` type and a `layers` array derived from `services`. `services` itself is untouched. | +| `app/src/components/LayerAccordion.tsx` | **New.** The accordion and its slices. | +| `app/src/components/Services.tsx` | The Services section becomes header + accordion + the "also in-house" line. `WhyUs` is unchanged. | + +### The five layers + +| # | Kicker | Title | Source service | Photo | +|---|---|---|---|---| +| 1 | Layer one | Bulk Earthworks | `earthworks` | `dji_0587` | +| 2 | Layer two | Water & Sewer | `municipal` | `310a6423` | +| 3 | Layer three | Stormwater | `stormwater` | `310a6304` | +| 4 | Layer four | Roads & Paving | `roads` | `road-paving` | +| 5 | Layer five | Electrical Reticulation | `electrical` | `310a6904` | + +The order is the order the layers go into the ground, which is what makes the section read as a method rather than a list. + +### Data derivation + +`layers` is derived from `services` by id, not written out separately: + +- `body` comes from the service's `short` field — currently unused data that was written for exactly this purpose. +- `title` comes from the service title, with two overrides: `municipal` → "Water & Sewer" and `roads` → "Roads & Paving" (the slice titles are shorter and more concrete than the service names). +- `scope` and `image` are taken from the service unchanged. + +Deriving rather than duplicating means the accordion cannot drift away from the service data. `services` itself stays exactly as it is, because the quote form's service dropdown still uses all seven. + +### Section copy + +- Eyebrow: `01` + "Services" (the existing `SectionLabel`). +- Heading: **"Five layers. None of them optional."** — "None of them optional." in gold. +- Intro: *"This is the whole method, start to finish. Five layers on every site, in the same order, since 2007 — and it is not going to start now."* — "since 2007" is the AEB Construction founding date already in `company`. +- Under the accordion: *"Civil design and residential development are also in-house."* with a link to `#contact`. + +## 5. Layout + +### Desktop (`sm` and up) + +- Row: `flex`, height `clamp(22rem, 58vh, 34rem)`, `overflow: hidden`, hairline borders top and bottom. +- Slice: `flex-1`, `min-w-0`, `overflow: hidden`, `transition-[flex-grow] 800ms ease-expo`; `hover:flex-grow-[4.2]` and `focus-within:flex-grow-[4.2]`. +- Photograph: absolutely positioned, `background-size: cover`, `scale(1.08) saturate(.55) brightness(.42)`; on hover `scale(1.02) saturate(1) brightness(.72)`. +- Scrim: `from-ink via-ink/55 to-ink/15`, top to bottom. +- Label: absolute, full width at the bottom, `writing-mode: vertical-rl` + `rotate(180deg)`, display face at `clamp(1.1rem, 1.7vw, 1.6rem)`, with a text shadow; turns gold on hover. +- Panel: absolute, full slice, content pushed to the bottom with `clamp(1.4rem, 3vw, 2.5rem)` padding. `opacity: 0` + `translateY(1.5rem)`; on hover/focus `opacity: 1` + `translateY(0)`. + - Kicker: eyebrow, gold. + - Title: display face, `clamp(1.5rem, 2.6vw, 2.4rem)`. + - Body: `short`, `max-w-[34ch]`. + - Scope chips: the service's scope tags. + +### Mobile (below `sm`) + +One markup tree, two layouts: + +- The row becomes `flex-col` with no fixed height and no clipping. +- Each slice is a column: the photograph is a `h-44` banner, and the panel sits below it in normal flow with the copy always visible. +- The vertical label and the scrim are desktop-only. + +## 6. Interaction and accessibility + +- Each slice is focusable (`tabIndex={0}`) with an `aria-label` carrying the title. `:focus-within` reveals the panel, so keyboard users get the same reveal as hover — this is the reference's own approach. +- The panel content stays in the accessibility tree at all times, so a screen reader reaches every layer's description and scope whether or not the panel is expanded. +- Tapping a slice on a touch device focuses it, which expands it. +- The site's existing global rule collapses every transition under `prefers-reduced-motion`, so the expansion snaps rather than animates. No extra work needed. + +## 7. Out of scope + +- The aerial clip on the Roads service (`/video/road-paving.mp4`). The accordion slices use still photographs; a landscape clip cropped into a narrow slice does not read. The asset stays in the repository and in the data. +- Any change to `WhyUs`, `Company`, `Team`, `Contact` or the fly-through. +- Any change to `services` — the quote form's dropdown still offers all seven. + +## 8. Acceptance criteria + +1. The Services section shows exactly five slices, in the order earthworks → water & sewer → stormwater → roads & paving → electrical. +2. Hovering a slice expands it across the row and brightens its photograph; moving away collapses it again. +3. Tabbing to a slice expands it identically to hovering. +4. Each expanded slice shows its kicker, title, one-line description and scope chips. +5. Below `sm` the slices stack vertically as full-width cards with the copy always visible and no horizontal overflow. +6. The heading reads "Five layers. None of them optional." and the intro paragraph sits beside it on desktop. +7. The "also in-house" line names civil design and residential development and links to `#contact`. +8. `npm run build` passes and `npm test` still passes. +9. No new runtime dependency, and the `services` array is unchanged. +10. The `#services` anchor and the quote form's service dropdown still work.