Step 002 complete. Step 003 needs verification

This commit is contained in:
2026-04-13 09:49:51 +02:00
parent e85b3fbfb2
commit 542d8ca9f7
92 changed files with 1248 additions and 4 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-10

View File

@@ -0,0 +1,111 @@
## Context
Step 002 produced a `BaseLayout.astro` that imports `Nav.astro` and `Footer.astro` stubs — both render nothing. The navigation bar is the first visible UI element needed on every page. It must:
- Render correctly on a Midnight (`#102022`) dark background
- Work at build time (Astro SSG — no server runtime)
- Derive the current locale from the URL path (no runtime props)
- Use zero client-side framework code
Available assets confirmed:
- `src/assets/logos/logo-wide.svg` — full "qumo" wordmark, paths filled with `#161616` (near-black — needs override)
- `src/assets/icons/wum-arrow.svg` — chevron arrow, already `stroke="#f3f3f3"` (Snow — correct for dark nav)
## Goals / Non-Goals
**Goals:**
- Fully functional `Nav.astro` replacing the empty stub
- Logo visible on Midnight background
- Three nav links + language switcher + Connect CTA
- Scroll-triggered background transition (transparent → Midnight)
- Mobile hamburger menu with open/close behavior
- All text strings from `en.json` / `nl.json`
**Non-Goals:**
- Animated underline or hover effects beyond Tailwind utilities
- Active page highlighting (deferred — pages don't exist yet to test against)
- Dropdown sub-menus (not on staging site)
- Separate `LanguageSwitcher.astro` component (simple enough to inline)
## Decisions
### 1. Logo: inline SVG with fill override
**Decision:** Inline the `logo-wide.svg` SVG directly in `Nav.astro`. Change all `fill="#161616"` attributes to `fill="currentColor"`. Set `class="text-snow"` on the `<svg>` element so `currentColor` resolves to Snow (`#F3F3F3`).
**Rationale:** The logo is a small, purely decorative SVG — inlining it avoids an extra HTTP request and allows CSS-controlled coloring without filters. `fill="currentColor"` + Tailwind text color is the idiomatic approach. An `<img>` tag cannot be recolored without CSS filters, which look worse.
**Alternative considered:** `<Image>` component with a separate white-fill SVG asset. Rejected: requires maintaining two copies of the same logo.
### 2. Locale detection: Astro.url at build time
**Decision:** In `Nav.astro`, derive locale from `Astro.url.pathname`:
```ts
const isNl = Astro.url.pathname.startsWith('/nl');
const locale = isNl ? 'nl' : 'en';
const alternatePath = isNl
? Astro.url.pathname.replace(/^\/nl/, '') || '/'
: '/nl' + Astro.url.pathname;
```
Pass `locale` to `getContent()` for nav strings. Use `alternatePath` for the language switcher link.
**Rationale:** Astro SSG renders each page at build time with its own `Astro.url`. This approach is zero-JS, works for every route without configuration, and the prefix rule (`/nl/...``/...`) holds for all routes on this site. Confirmed with user during explore session (Option B).
**Alternative considered:** Prop threading from `BaseLayout` (Option A). Rejected by user — more verbose, non-standard for nav components.
### 3. Scroll behavior: window scroll event + CSS class toggle
**Decision:** A `<script>` tag in Nav adds/removes a `scrolled` class on the `<nav>` element when `window.scrollY > 20`. Tailwind's `data-scrolled` attribute or a direct class toggle drives the style change.
Implementation approach — use a `data-scrolled` attribute to avoid class name conflicts:
```js
const nav = document.getElementById('main-nav');
window.addEventListener('scroll', () => {
nav.dataset.scrolled = window.scrollY > 20 ? 'true' : 'false';
}, { passive: true });
```
CSS: nav starts `bg-transparent`, transitions to `bg-midnight` when `data-scrolled="true"`. Use Tailwind's arbitrary variant: `data-[scrolled=true]:bg-midnight`.
**Rationale:** Simple, no dependencies, no layout shift. `passive: true` avoids scroll jank. The `data-*` attribute approach keeps the JSCSS contract explicit and avoids collisions with Tailwind's JIT-generated class names.
**Alternative considered:** `IntersectionObserver` on a sentinel element. Unnecessary complexity for this use case — scroll position threshold is simpler and more predictable.
### 4. Mobile menu: vanilla JS toggle with CSS transition
**Decision:** The hamburger button toggles `aria-expanded` on itself and a corresponding `data-open` attribute on the mobile menu panel. CSS handles the visual transition (translate or max-height). A `<script>` tag wires up the toggle.
```html
<button id="menu-toggle" aria-expanded="false" aria-controls="mobile-menu">...</button>
<div id="mobile-menu" class="... -translate-y-full data-[open]:translate-y-0 transition-transform">
...
</div>
```
**Rationale:** `aria-expanded` is the correct ARIA pattern for disclosure widgets. The `data-open` attribute drives CSS state without needing to manage class lists. `translate` transitions are GPU-accelerated and smooth.
### 5. Nav content: `nav` key in en.json / nl.json
**Decision:** Add a top-level `"nav"` key to both content files:
```json
"nav": {
"links": [
{ "label": "Services", "href": "/#services" },
{ "label": "AI Launchpad", "href": "/ai-launchpad" },
{ "label": "About", "href": "/about" }
],
"cta": "Connect",
"langSwitch": { "en": "EN", "nl": "NL" }
}
```
NL links have the same hrefs with `/nl` prefix where needed; CTA becomes "Verbind".
**Rationale:** Consistent with the `meta` key established in step 002. Keeps all user-visible text out of component markup.
**Note on NL link hrefs:** The `/#services` anchor on the EN homepage becomes `/nl#services` in NL (no slash before `#`). The `href` values in `nl.json` must reflect this.
## Risks / Trade-offs
- **Logo SVG inlining grows component size** → The logo SVG is ~1.5KB. Acceptable for a component rendered on every page; Astro will inline it into each page's HTML at build time. If the logo is ever animated or reused frequently, extract to a dedicated `Logo.astro` component.
- **Scroll behavior flickers on page load** → On slow connections the nav may briefly show transparent before JS runs. Mitigation: set `data-scrolled="false"` as the default HTML attribute so no flash occurs on initial render.
- **Mobile menu accessibility** → `aria-expanded` and `aria-controls` are set, but focus trapping inside the open menu is not implemented in this step. Acceptable for now — full accessibility audit is a later concern.
- **NL href construction** → The `alternatePath` logic strips the `/nl` prefix. If a page path ever starts with `/nl` for a reason other than locale (e.g. `/nl-tech`), it would be incorrectly treated as Dutch. This site has no such routes, so it's safe — but document the assumption.

View File

@@ -0,0 +1,29 @@
## Why
The base layout shell (step 002) includes stub `Nav.astro` and `Footer.astro` components that render nothing. Every page is unusable without navigation — users can't move between pages, switch languages, or reach the contact CTA. Step 003 fills in the navigation bar, the primary wayfinding element present on every page of the site.
## What Changes
- **`src/components/Nav.astro`** — implement the full responsive navigation bar (currently an empty stub)
- **`src/content/en.json`** — add `nav` key with link labels and CTA text
- **`src/content/nl.json`** — add `nav` key with Dutch equivalents
- No changes to `BaseLayout.astro` — it already imports `Nav.astro`
## Capabilities
### New Capabilities
- `navigation`: Responsive nav bar with logo, page links, language switcher, and Connect CTA. Transparent on load, solid Midnight background on scroll. Mobile hamburger menu with slide-in drawer.
### Modified Capabilities
- `content-i18n`: Add `nav` section to both `en.json` and `nl.json` content files (new keys, no requirement changes to the existing meta section or the i18n helper).
## Impact
- `website/src/components/Nav.astro` — full implementation replaces stub
- `website/src/content/en.json` — new `nav` key added
- `website/src/content/nl.json` — new `nav` key added
- Vanilla JS `<script>` tag in Nav handles scroll behavior and mobile menu toggle — no external dependencies
- No SEO impact (nav is not indexed content, no structured data changes)
- All pages that use `BaseLayout` automatically get the nav (no per-page changes needed)

View File

@@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Locale content JSON files contain nav strings
`src/content/en.json` and `src/content/nl.json` SHALL each contain a top-level `"nav"` key with the following structure:
- `links`: array of objects with `label: string` and `href: string` for each nav link
- `cta`: string — label for the Connect CTA button
- `ctaHref`: string — href for the Connect CTA button
- `langSwitch`: object with `en: string` and `nl: string` labels for the language switcher
#### Scenario: en.json nav section has correct structure
- **WHEN** `src/content/en.json` is parsed as JSON
- **THEN** it SHALL have a `nav` object containing `links` (array of 3 items with `label` and `href`), `cta` string, `ctaHref` string, and `langSwitch` object with `en` and `nl` keys
#### Scenario: nl.json nav section has Dutch labels
- **WHEN** `src/content/nl.json` is parsed as JSON
- **THEN** `nav.links` SHALL contain Dutch labels ("Diensten", "AI Launchpad", "Over Ons"), `nav.cta` SHALL be "Verbind", and `nav.ctaHref` SHALL be "/nl/contact"
#### Scenario: Build succeeds with nav strings added
- **WHEN** `npm run build` is executed after adding `nav` keys to both JSON files
- **THEN** the build SHALL complete without TypeScript errors

View File

@@ -0,0 +1,93 @@
## ADDED Requirements
### Requirement: Nav renders logo linking to homepage
The navigation bar SHALL display the Qumo wordmark logo (`logo-wide.svg` inlined as SVG) on the left side, wrapped in an `<a>` tag linking to the locale-appropriate homepage (`/` for EN, `/nl` for NL). The logo fill SHALL use `currentColor` so it inherits the Snow text color on the Midnight nav background.
#### Scenario: Logo is visible on dark background
- **WHEN** the page renders with `Nav.astro` included
- **THEN** the logo SVG SHALL have `class="text-snow"` (or equivalent) so its paths render as Snow (`#F3F3F3`) on the Midnight background
#### Scenario: Logo links to locale homepage
- **WHEN** the user is on an English page and clicks the logo
- **THEN** the browser SHALL navigate to `/`
#### Scenario: Logo links to NL homepage on Dutch pages
- **WHEN** the user is on a Dutch page (`/nl/...`) and clicks the logo
- **THEN** the browser SHALL navigate to `/nl`
---
### Requirement: Nav renders three page links
The navigation bar SHALL render three links: **Services** (→ `/#services`), **AI Launchpad** (→ `/ai-launchpad`), **About** (→ `/about`). Label text SHALL come from the `nav.links` array in the locale content JSON. On Dutch pages, links SHALL point to `/nl#services`, `/nl/ai-launchpad`, `/nl/about`.
#### Scenario: EN nav links render with correct hrefs
- **WHEN** the page locale is `en`
- **THEN** the three links SHALL have `href` values `/#services`, `/ai-launchpad`, `/about` with labels "Services", "AI Launchpad", "About"
#### Scenario: NL nav links render with correct hrefs
- **WHEN** the page locale is `nl`
- **THEN** the three links SHALL have `href` values `/nl#services`, `/nl/ai-launchpad`, `/nl/about` with labels "Diensten", "AI Launchpad", "Over Ons"
---
### Requirement: Nav renders Connect CTA button
The navigation bar SHALL render a "Connect" CTA button on the right side linking to `/contact` (EN) or `/nl/contact` (NL), with the `wum-arrow.svg` chevron icon inline. The button SHALL be styled distinctly from nav links (border or background treatment matching the staging site).
#### Scenario: Connect button links to contact page
- **WHEN** the page locale is `en`
- **THEN** a button or link with label "Connect" and `href="/contact"` SHALL be present in the nav
#### Scenario: NL Connect button uses Dutch label and NL href
- **WHEN** the page locale is `nl`
- **THEN** the CTA SHALL display "Verbind" and link to `/nl/contact`
---
### Requirement: Nav renders language switcher
The navigation bar SHALL render a language switcher that links to the alternate locale version of the current page. The current locale SHALL be shown as active/non-linked; the alternate SHALL be a clickable link.
#### Scenario: EN page shows NL as switchable link
- **WHEN** the current page is `/about`
- **THEN** the language switcher SHALL show "EN" (active, non-linked) and "NL" linking to `/nl/about`
#### Scenario: NL page shows EN as switchable link
- **WHEN** the current page is `/nl/ai-launchpad`
- **THEN** the language switcher SHALL show "NL" (active, non-linked) and "EN" linking to `/ai-launchpad`
---
### Requirement: Nav is fixed and transitions background on scroll
The `<nav>` element SHALL be `position: fixed` at the top of the viewport with `z-index` above page content. On page load it SHALL have a transparent background. After the user scrolls more than 20px, it SHALL transition to a solid Midnight (`#102022`) background. The transition SHALL be smooth (CSS `transition-colors`).
#### Scenario: Nav is transparent on page load
- **WHEN** a page loads and `window.scrollY` is 0
- **THEN** the nav background SHALL be transparent
#### Scenario: Nav becomes solid after scrolling
- **WHEN** the user scrolls more than 20px down the page
- **THEN** the nav background SHALL be `bg-midnight` (solid `#102022`)
#### Scenario: Nav returns to transparent when scrolled back to top
- **WHEN** the user scrolls back to within 20px of the top
- **THEN** the nav background SHALL return to transparent
---
### Requirement: Nav collapses to hamburger menu on mobile
On viewports below `md` breakpoint (768px), the three nav links, language switcher, and Connect CTA SHALL be hidden. A hamburger button SHALL be shown. Clicking the hamburger SHALL reveal a full-width slide-in mobile menu containing all nav links, the language switcher, and the Connect CTA.
#### Scenario: Desktop nav links are hidden on mobile
- **WHEN** viewport width is below 768px
- **THEN** the desktop nav links SHALL have `class` including `hidden` (or equivalent responsive class)
#### Scenario: Hamburger button opens mobile menu
- **WHEN** the hamburger button is clicked
- **THEN** the mobile menu panel SHALL become visible and `aria-expanded="true"` SHALL be set on the button
#### Scenario: Hamburger button closes mobile menu
- **WHEN** the mobile menu is open and the hamburger button is clicked again
- **THEN** the mobile menu SHALL close and `aria-expanded="false"` SHALL be set on the button
#### Scenario: Mobile menu contains all nav items
- **WHEN** the mobile menu is open
- **THEN** it SHALL contain all three page links, the language switcher, and the Connect CTA

View File

@@ -0,0 +1,36 @@
## 1. Content strings
- [x] 1.1 Add `nav` key to `website/src/content/en.json` with `links` array (Services/AI Launchpad/About with hrefs), `cta: "Connect"`, `ctaHref: "/contact"`, and `langSwitch: { en: "EN", nl: "NL" }`
- [x] 1.2 Add `nav` key to `website/src/content/nl.json` with Dutch labels (Diensten/AI Launchpad/Over Ons), NL hrefs, `cta: "Verbind"`, `ctaHref: "/nl/contact"`
## 2. Nav component — structure and content
- [x] 2.1 Replace the stub in `website/src/components/Nav.astro` with a `<nav id="main-nav">` element: fixed position, full width, high z-index, with `data-scrolled="false"` attribute
- [x] 2.2 Add locale detection from `Astro.url.pathname`: `const isNl = Astro.url.pathname.startsWith('/nl')` and derive `locale` and `alternatePath`
- [x] 2.3 Call `getContent(locale)` and destructure `nav` strings for use in the template
- [x] 2.4 Inline `logo-wide.svg` on the left side: change all `fill="#161616"` attributes to `fill="currentColor"`, wrap in `<a>` linking to locale homepage (`/` or `/nl`), add `class="text-snow"` to the `<svg>`
- [x] 2.5 Render the three nav links from `nav.links` in the desktop layout (hidden on mobile via `hidden md:flex`)
- [x] 2.6 Render the language switcher: show current locale as plain text, alternate as `<a href={alternatePath}>` — both using `nav.langSwitch` labels
- [x] 2.7 Render the Connect CTA button: `<a href={nav.ctaHref}>` with label `{nav.cta}` and inlined `wum-arrow.svg` icon, styled with border/Snow treatment
## 3. Nav component — mobile menu
- [x] 3.1 Add hamburger button (3-line SVG icon) visible only on mobile (`md:hidden`), with `id="menu-toggle"`, `aria-expanded="false"`, `aria-controls="mobile-menu"`
- [x] 3.2 Add mobile menu panel `<div id="mobile-menu">` with slide-in transition classes: starts translated off-screen, transitions to visible when `data-open` attribute is set
- [x] 3.3 Populate mobile menu with the same nav links, language switcher, and Connect CTA (can share content strings already destructured)
## 4. Nav component — JavaScript behaviors
- [x] 4.1 Add `<script>` tag for scroll behavior: listen to `window.scroll` (passive), toggle `data-scrolled` attribute on `#main-nav` when `scrollY > 20`
- [x] 4.2 Add CSS to `global.css` (or scoped `<style>` in Nav) for scroll state: `nav[data-scrolled="true"] { background-color: var(--color-midnight); }` with `transition: background-color 300ms ease`
- [x] 4.3 Add `<script>` tag for mobile menu toggle: clicking `#menu-toggle` toggles `data-open` on `#mobile-menu` and flips `aria-expanded` on the button
## 5. Verification
- [x] 5.1 Run `cd website && npm run build` — build must succeed with no TypeScript errors
- [x] 5.2 Run `npm run dev` and open the smoke-test page — confirm logo renders in Snow on Midnight background
- [x] 5.3 Verify desktop layout: logo left, three links center, EN/NL + Connect right
- [ ] 5.4 Verify scroll behavior: nav transparent at top, Midnight background after scrolling 20px
- [x] 5.5 Resize browser to mobile width — confirm hamburger appears, desktop links hidden
- [x] 5.6 Click hamburger — confirm mobile menu slides in with all nav items
- [x] 5.7 Verify language switcher: on `/`, NL link points to `/nl`; on a future `/nl/about`, EN link would point to `/about`