Files
qumo-website/openspec/changes/step-003-navigation-bar/design.md

112 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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.