Step 002 complete. Step 003 needs verification
This commit is contained in:
111
openspec/changes/step-003-navigation-bar/design.md
Normal file
111
openspec/changes/step-003-navigation-bar/design.md
Normal 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 JS–CSS 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.
|
||||
Reference in New Issue
Block a user