feat: Demoted nav brand to span, promoted page headings to h1, added sk…

- "frontend/src/App.tsx"
- "frontend/src/App.css"
- "frontend/src/pages/Home.tsx"
- "frontend/src/pages/SearchResults.tsx"
- "frontend/src/pages/TopicsBrowse.tsx"
- "frontend/src/pages/CreatorsBrowse.tsx"
- "frontend/src/pages/SubTopicPage.tsx"
- "frontend/src/pages/AdminReports.tsx"

GSD-Task: S04/T01
This commit is contained in:
jlightner 2026-03-31 08:52:48 +00:00
parent 089435a990
commit 5e5961fa92
18 changed files with 646 additions and 18 deletions

View file

@ -8,5 +8,5 @@ Transform Chrysopedia from functionally adequate to engaging and accessible. Add
|----|-------|------|---------|------|------------|
| S01 | Interaction Delight & Discovery | medium | — | ✅ | Cards animate on hover with scale+shadow. Card grids stagger entrance. Featured technique has glow treatment. Random Technique button navigates to a random page. |
| S02 | Topics, Creator Stats & Card Polish | medium | — | ✅ | Topics page loads collapsed, expands with animation. Creator stats show colored topic pills. Cards limit tags to 4 with +N more. Empty subtopics show Coming soon. |
| S03 | Global Search & Mobile Navigation | medium | — | | Compact search bar in nav on all pages. Cmd+K focuses it. Mobile viewport shows hamburger menu. |
| S03 | Global Search & Mobile Navigation | medium | — | | Compact search bar in nav on all pages. Cmd+K focuses it. Mobile viewport shows hamburger menu. |
| S04 | Accessibility & SEO Fixes | low | — | ⬜ | Every page has single H1, skip-to-content link, AA contrast text, and descriptive browser tab title. |

View file

@ -0,0 +1,85 @@
---
id: S03
parent: M011
milestone: M011
provides:
- Compact nav search bar on all non-home pages with Cmd+K focus
- Mobile hamburger menu with stacked nav links at <768px
requires:
[]
affects:
- S04
key_files:
- frontend/src/components/SearchAutocomplete.tsx
- frontend/src/App.tsx
- frontend/src/App.css
- frontend/src/pages/Home.tsx
- frontend/src/pages/SearchResults.tsx
key_decisions:
- Refactored SearchAutocomplete from heroSize boolean to variant string prop for cleaner multi-context usage
- Mobile nav search uses second component instance (no globalShortcut) rather than CSS repositioning to avoid double keyboard handler registration
patterns_established:
- Variant prop pattern for component display modes (hero/inline/nav) — reusable for other components that need context-dependent sizing
observability_surfaces:
- none
drill_down_paths:
- .gsd/milestones/M011/slices/S03/tasks/T01-SUMMARY.md
- .gsd/milestones/M011/slices/S03/tasks/T02-SUMMARY.md
duration: ""
verification_result: passed
completed_at: 2026-03-31T08:46:50.106Z
blocker_discovered: false
---
# S03: Global Search & Mobile Navigation
**Added compact nav search bar with Cmd+K shortcut on all non-home pages and mobile hamburger menu with 44px touch targets and three auto-close mechanisms.**
## What Happened
Two tasks delivered the slice goal cleanly. T01 refactored SearchAutocomplete from a boolean heroSize prop to a variant string prop ('hero' | 'inline' | 'nav'), added a globalShortcut prop for Cmd+K/Ctrl+K keyboard focus, created nav-variant CSS with compact sizing and high z-index dropdown, and wired the nav search into App.tsx conditionally on non-home routes. Home.tsx and SearchResults.tsx callers were updated to use the new variant prop. The deprecated heroSize prop remains as a backwards-compat fallback.
T02 added a hamburger menu button visible below 768px that toggles a mobile nav panel. Three auto-close mechanisms were implemented: route change (useEffect on location.pathname), Escape key (keydown listener), and outside click (ref-based click detection). The hamburger icon toggles between three-line and X via conditional SVG. All mobile nav links get min-height: 44px touch targets. A second SearchAutocomplete instance (without globalShortcut to avoid double registration) renders inside the mobile panel. AdminDropdown was restyled to full-width static submenu in the mobile panel.
Build passes cleanly (51 modules, 813ms). Both tasks verified via build and browser testing.
## Verification
Frontend build succeeds: `cd frontend && npm run build` — 51 modules transformed, built in 813ms, zero errors. Code inspection confirms: variant prop with three modes, globalShortcut Cmd+K handler, hamburger button with aria-expanded, menuOpen state with three close mechanisms, 44px min-height on mobile nav links, nav search conditional on non-home routes.
## Requirements Advanced
- R020 — Nav search bar rendered on all non-home pages with Cmd+K/Ctrl+K keyboard shortcut
- R021 — Hamburger menu visible below 768px with 44px touch targets, three auto-close mechanisms
## Requirements Validated
None.
## New Requirements Surfaced
None.
## Requirements Invalidated or Re-scoped
None.
## Deviations
SearchAutocomplete kept deprecated heroSize prop as fallback (variant takes precedence). Mobile nav search rendered as second component instance rather than CSS-repositioned — avoids double Cmd+K registration. AdminDropdown restyled to full-width in mobile panel.
## Known Limitations
None.
## Follow-ups
None.
## Files Created/Modified
- `frontend/src/components/SearchAutocomplete.tsx` — Replaced heroSize boolean with variant prop, added globalShortcut prop for Cmd+K, nav-variant rendering
- `frontend/src/App.tsx` — Added hamburger menu state, three auto-close mechanisms, conditional nav search, mobile menu panel
- `frontend/src/App.css` — Nav search compact styles, hamburger button, mobile breakpoint panel, 44px touch targets
- `frontend/src/pages/Home.tsx` — Updated caller to variant="hero"
- `frontend/src/pages/SearchResults.tsx` — Updated caller to variant="inline"

View file

@ -0,0 +1,167 @@
# S03: Global Search & Mobile Navigation — UAT
**Milestone:** M011
**Written:** 2026-03-31T08:46:50.106Z
# S03 UAT: Global Search & Mobile Navigation
## Preconditions
- Chrysopedia frontend running (dev server or production build)
- Browser with DevTools available for viewport resizing
---
## Test 1: Nav Search Bar Visibility
**Steps:**
1. Navigate to homepage (`/`)
2. Observe the navigation header
**Expected:** No search bar in the nav header (homepage has its own hero search)
3. Navigate to `/topics`
4. Observe the navigation header
**Expected:** Compact search input visible in the nav bar between brand and right section
5. Navigate to `/creators`, then any `/technique/:slug` page
**Expected:** Search bar present in nav on both pages
---
## Test 2: Nav Search Functionality
**Steps:**
1. On `/topics`, click the nav search input
2. Type "reverb"
3. Observe typeahead dropdown
**Expected:** Typeahead dropdown appears with results, positioned with high z-index over page content
4. Press Enter
**Expected:** Navigates to search results page for "reverb"
---
## Test 3: Cmd+K Keyboard Shortcut
**Steps:**
1. Navigate to `/creators`
2. Click somewhere on the page body (not in search)
3. Press Cmd+K (Mac) or Ctrl+K (Windows/Linux)
**Expected:** Nav search input receives focus. Browser default Cmd+K behavior is prevented.
4. Type a query and press Enter
**Expected:** Search executes normally
---
## Test 4: Cmd+K Not Active on Homepage
**Steps:**
1. Navigate to homepage (`/`)
2. Press Cmd+K
**Expected:** No nav search to focus (homepage uses hero search). Browser default may trigger.
---
## Test 5: Hamburger Menu Visibility
**Steps:**
1. Open DevTools, set viewport to 390×844 (iPhone 14)
2. Navigate to any page
**Expected:** Hamburger button (☰) visible in header. Desktop nav links hidden.
3. Set viewport to 1024×768 (desktop)
**Expected:** Hamburger button hidden. Desktop nav links visible inline.
---
## Test 6: Hamburger Menu Toggle
**Steps:**
1. At mobile viewport (390px wide), tap hamburger button
**Expected:** Nav panel slides open below header. Hamburger icon changes to X. Links stacked vertically.
2. Tap X button
**Expected:** Nav panel closes with transition. Icon reverts to ☰.
---
## Test 7: Mobile Touch Targets
**Steps:**
1. At mobile viewport, open hamburger menu
2. Inspect nav links in DevTools
**Expected:** Each nav link has min-height of 44px. Padding provides comfortable touch area.
---
## Test 8: Auto-Close on Route Change
**Steps:**
1. At mobile viewport, open hamburger menu
2. Tap a nav link (e.g., "Topics")
**Expected:** Page navigates to Topics. Menu closes automatically.
---
## Test 9: Auto-Close on Escape
**Steps:**
1. At mobile viewport, open hamburger menu
2. Press Escape key
**Expected:** Menu closes. Focus returns to page.
---
## Test 10: Auto-Close on Outside Click
**Steps:**
1. At mobile viewport, open hamburger menu
2. Tap/click on the page content area below the menu
**Expected:** Menu closes.
---
## Test 11: Mobile Search in Menu
**Steps:**
1. At mobile viewport, open hamburger menu
2. Locate search input inside the menu panel
**Expected:** Search input is present and full-width inside mobile menu.
3. Type a query and submit
**Expected:** Search executes, menu closes on navigation.
---
## Test 12: Desktop Layout Unchanged
**Steps:**
1. At desktop viewport (1280px wide), navigate through all pages
**Expected:** No hamburger button. Nav links displayed inline. Search bar compact in header on non-home pages. No layout regressions from S01/S02 work.
---
## Edge Cases
- **Rapid toggle:** Quickly tap hamburger open/close 5 times — no stuck state
- **Resize while open:** Open menu at mobile width, drag viewport to desktop width — menu should close or hide gracefully
- **Multiple Cmd+K presses:** Press Cmd+K repeatedly — input stays focused, no errors in console

View file

@ -0,0 +1,16 @@
{
"schemaVersion": 1,
"taskId": "T02",
"unitId": "M011/S03/T02",
"timestamp": 1774946733530,
"passed": true,
"discoverySource": "task-plan",
"checks": [
{
"command": "cd frontend",
"exitCode": 0,
"durationMs": 7,
"verdict": "pass"
}
]
}

View file

@ -1,6 +1,36 @@
# S04: Accessibility & SEO Fixes
**Goal:** Bring the site to WCAG 2.1 Level AA on core metrics and add SEO page titles
**Goal:** Every page has a single H1, skip-to-content link, AA-compliant text contrast, and a descriptive browser tab title.
**Demo:** After this: Every page has single H1, skip-to-content link, AA contrast text, and descriptive browser tab title.
## Tasks
- [x] **T01: Demoted nav brand to span, promoted page headings to h1, added skip-to-content link, and raised muted text contrast to AA compliance** — Three accessibility fixes that all touch App.tsx/App.css and page heading elements:
1. **Heading hierarchy (R022):** Demote `<h1>Chrysopedia</h1>` in App.tsx nav to `<span>`. Update `.app-header h1` CSS rule to `.app-header__brand span`. Promote `<h2>` to `<h1>` on pages that lack one: TopicsBrowse, CreatorsBrowse, SubTopicPage, AdminReports, AdminPipeline. Add `<h1>` to SearchResults (currently has no heading). Fix heading level skips in Home.tsx: the 'How It Works' h3s (lines 120/127/134) become h2s; 'Featured Technique' h3 (line 191) becomes h2; 'Recently Added' h3 (line 221) becomes h2.
2. **Skip-to-content link (R023):** Add `id="main-content"` to the `<main>` tag in App.tsx. Add `<a href="#main-content" className="skip-link">Skip to content</a>` as the first child of `.app`. Add `.skip-link` CSS: visually hidden by default, visible on `:focus`, positioned at top of viewport.
3. **Contrast fix (R024):** Change `--color-text-muted` from `#6b6b7a` to `#828291` in App.css `:root` block. This achieves 5.05:1 on page bg and 4.56:1 on surface bg — both above AA 4.5:1 threshold.
- Estimate: 30m
- Files: frontend/src/App.tsx, frontend/src/App.css, frontend/src/pages/Home.tsx, frontend/src/pages/TopicsBrowse.tsx, frontend/src/pages/CreatorsBrowse.tsx, frontend/src/pages/SubTopicPage.tsx, frontend/src/pages/SearchResults.tsx, frontend/src/pages/AdminReports.tsx, frontend/src/pages/AdminPipeline.tsx
- Verify: cd frontend && npx tsc --noEmit && npm run build
- [ ] **T02: Add useDocumentTitle hook and wire descriptive titles into all pages** — Create a `useDocumentTitle` custom hook and call it from every page component so browser tabs show descriptive, route-specific titles.
1. Create `frontend/src/hooks/useDocumentTitle.ts` with a hook that sets `document.title` on mount and when the title string changes. Reset to 'Chrysopedia' on unmount (cleanup).
2. Wire the hook into all 10 page components with these title patterns:
- Home: `Chrysopedia — Production Knowledge, Distilled`
- TopicsBrowse: `Topics — Chrysopedia`
- SubTopicPage: `{subtopic} — {category} — Chrysopedia` (dynamic, updates when data loads)
- CreatorsBrowse: `Creators — Chrysopedia`
- CreatorDetail: `{name} — Chrysopedia` (dynamic, updates when creator loads)
- TechniquePage: `{title} — Chrysopedia` (dynamic, updates when technique loads)
- SearchResults: `Search: {query} — Chrysopedia` (dynamic, updates with query param)
- About: `About — Chrysopedia`
- AdminReports: `Content Reports — Chrysopedia`
- AdminPipeline: `Pipeline Management — Chrysopedia`
3. For dynamic pages (SubTopicPage, CreatorDetail, TechniquePage, SearchResults), call the hook with the resolved value so the title updates when async data loads. Use a fallback like `Chrysopedia` while loading.
- Estimate: 25m
- Files: frontend/src/hooks/useDocumentTitle.ts, frontend/src/pages/Home.tsx, frontend/src/pages/TopicsBrowse.tsx, frontend/src/pages/SubTopicPage.tsx, frontend/src/pages/CreatorsBrowse.tsx, frontend/src/pages/CreatorDetail.tsx, frontend/src/pages/TechniquePage.tsx, frontend/src/pages/SearchResults.tsx, frontend/src/pages/About.tsx, frontend/src/pages/AdminReports.tsx, frontend/src/pages/AdminPipeline.tsx
- Verify: cd frontend && npx tsc --noEmit && npm run build

View file

@ -0,0 +1,103 @@
# S04 Research: Accessibility & SEO Fixes
## Summary
Straightforward accessibility fixes across known files using established patterns. Four requirements: heading hierarchy (R022), skip-to-content link (R023), text contrast AA compliance (R024), and page-specific document titles (R025). No new dependencies, no architectural decisions, no unfamiliar technology.
## Recommendation
Four independent tasks, one per requirement. All touch the same small set of files (App.tsx, App.css, and page components). Order doesn't matter — none depend on each other. The heading hierarchy fix touches the most files and has the most nuance (promoting h2→h1 on some pages, demoting nav h1→span, fixing heading level skips).
## Implementation Landscape
### R022 — Heading Hierarchy Fix
**Current state:** `App.tsx:63` has `<h1>Chrysopedia</h1>` in the nav brand link. This means every page has *two* h1s — the nav one plus the page's own (Home, About, CreatorDetail, TechniquePage each have explicit `<h1>`).
**Pages with their own h1 (need nav h1 removed):**
- `Home.tsx:101``<h1>Production Knowledge, Distilled</h1>`
- `About.tsx:7``<h1>About Chrysopedia</h1>`
- `CreatorDetail.tsx:98``<h1>{creator.name}</h1>`
- `TechniquePage.tsx:243``<h1>{displayTitle}</h1>`
**Pages WITHOUT h1 (need their h2 promoted to h1):**
- `TopicsBrowse.tsx:96``<h2>Topics</h2>` → promote to h1
- `CreatorsBrowse.tsx:89``<h2>Creators</h2>` → promote to h1
- `SubTopicPage.tsx:112``<h2>{subtopicDisplay}</h2>` → promote to h1
- `SearchResults.tsx` — no heading at all, needs an h1 added
- `AdminReports.tsx:116``<h2>Content Reports</h2>` → promote to h1
- `AdminPipeline.tsx:577``<h2>Pipeline Management</h2>` → promote to h1
**Fix:** Change `App.tsx:63` from `<h1>` to `<span>` (visually styled the same via CSS class). Promote h2→h1 on pages that lack one. Update CSS selectors if any target `h1` inside the brand link.
**Heading level skips:**
- `Home.tsx` has h1 → h3 (skips h2) in the "How It Works" section. The h3s at lines 120/127/134 should be h2 or the section needs restructuring. Also h3 at line 191 (Featured label) and line 221 (Recently Added) — these should be h2.
- `SearchResults.tsx` uses h3 for group titles — should be h2 under the new h1.
### R023 — Skip-to-Content Link
**Current state:** No skip link exists. `<main className="app-main">` at `App.tsx:114` is the target.
**Fix:** Add `id="main-content"` to the `<main>` tag. Add a visually-hidden skip link as the first child of `.app`: `<a href="#main-content" className="skip-link">Skip to content</a>`. CSS: position offscreen by default, visible on `:focus`.
### R024 — Text Contrast AA Compliance
**Current values (`:root` in App.css lines 14-16):**
- `--color-text-primary: #e2e2ea` — passes easily
- `--color-text-secondary: #8b8b9a`**5.70:1 on page bg, 5.14:1 on surface** → already passes AA (4.5:1)
- `--color-text-muted: #6b6b7a`**3.65:1 on page bg, 3.29:1 on surface** → **fails AA**
**Background colors:**
- `--color-bg-page: #0f0f14`
- `--color-bg-surface: #1a1a24`
**Fix:** Bump `--color-text-muted` from `#6b6b7a` to `#828291`. This gives 5.05:1 on page bg and 4.56:1 on surface — comfortably above 4.5:1 AA threshold while maintaining the visual hierarchy (muted < secondary < primary).
Header text colors (`rgba(255,255,255,0.8)` etc.) all pass easily against the dark header background.
### R025 — Page-Specific Document Titles
**Current state:** No `document.title` usage anywhere in the codebase. Browser tab shows the default from `index.html`.
**Fix:** Add a `useDocumentTitle` custom hook (or inline `useEffect` in each page) that sets `document.title` on mount/update. Pattern:
```typescript
// In each page component:
useEffect(() => {
document.title = "Topics — Chrysopedia";
}, []);
// For dynamic pages:
useEffect(() => {
if (creator) document.title = `${creator.name} — Chrysopedia`;
}, [creator]);
```
**Pages needing titles (10 total):**
| Page | Title pattern |
|------|---------------|
| Home | `Chrysopedia — Production Knowledge, Distilled` |
| TopicsBrowse | `Topics — Chrysopedia` |
| SubTopicPage | `{subtopic} — {category} — Chrysopedia` |
| CreatorsBrowse | `Creators — Chrysopedia` |
| CreatorDetail | `{name} — Chrysopedia` |
| TechniquePage | `{title} — Chrysopedia` |
| SearchResults | `Search: {query} — Chrysopedia` |
| About | `About — Chrysopedia` |
| AdminReports | `Content Reports — Chrysopedia` |
| AdminPipeline | `Pipeline Management — Chrysopedia` |
A shared `useDocumentTitle(title: string)` hook keeps it DRY.
## Verification
- **Heading hierarchy:** Browser DevTools → `document.querySelectorAll('h1')` returns exactly 1 per page. Check with `$$('h1, h2, h3, h4, h5, h6').map(h => h.tagName)` — levels must be sequential.
- **Skip link:** Tab from fresh page load → first focus shows "Skip to content" link → activating it scrolls to main content.
- **Contrast:** The fix is a single CSS variable change. Verify with browser DevTools color picker contrast checker or computed values.
- **Document titles:** Navigate to each route and check `document.title` in console.
- **Build:** `cd frontend && npx tsc --noEmit && npm run build` — zero errors.
## Pitfalls
- **CSS selectors targeting `h1` in nav brand:** Check if any styles use `.app-header__brand h1` or similar. These need updating to target `span` after the heading demotion.
- **Heading level cascade in Home.tsx:** The "How It Works" section jumps h1→h3. Simply promoting to h2 is correct since h1 is the hero title and these are section-level headings below it.

View file

@ -0,0 +1,43 @@
---
estimated_steps: 4
estimated_files: 9
skills_used: []
---
# T01: Fix heading hierarchy, add skip-to-content link, and fix muted text contrast
Three accessibility fixes that all touch App.tsx/App.css and page heading elements:
1. **Heading hierarchy (R022):** Demote `<h1>Chrysopedia</h1>` in App.tsx nav to `<span>`. Update `.app-header h1` CSS rule to `.app-header__brand span`. Promote `<h2>` to `<h1>` on pages that lack one: TopicsBrowse, CreatorsBrowse, SubTopicPage, AdminReports, AdminPipeline. Add `<h1>` to SearchResults (currently has no heading). Fix heading level skips in Home.tsx: the 'How It Works' h3s (lines 120/127/134) become h2s; 'Featured Technique' h3 (line 191) becomes h2; 'Recently Added' h3 (line 221) becomes h2.
2. **Skip-to-content link (R023):** Add `id="main-content"` to the `<main>` tag in App.tsx. Add `<a href="#main-content" className="skip-link">Skip to content</a>` as the first child of `.app`. Add `.skip-link` CSS: visually hidden by default, visible on `:focus`, positioned at top of viewport.
3. **Contrast fix (R024):** Change `--color-text-muted` from `#6b6b7a` to `#828291` in App.css `:root` block. This achieves 5.05:1 on page bg and 4.56:1 on surface bg — both above AA 4.5:1 threshold.
## Inputs
- ``frontend/src/App.tsx` — nav h1 to demote, main tag needs id, skip link insertion point`
- ``frontend/src/App.css``.app-header h1` rule to update, `:root` muted color to fix, skip-link styles to add`
- ``frontend/src/pages/Home.tsx` — h3 level skips to fix (lines 120/127/134/164/170/191/221)`
- ``frontend/src/pages/TopicsBrowse.tsx` — h2 to promote to h1 (line 96)`
- ``frontend/src/pages/CreatorsBrowse.tsx` — h2 to promote to h1 (line 89)`
- ``frontend/src/pages/SubTopicPage.tsx` — h2 to promote to h1 (line 112)`
- ``frontend/src/pages/SearchResults.tsx` — needs h1 added (has no heading currently)`
- ``frontend/src/pages/AdminReports.tsx` — h2 to promote to h1 (line 116)`
- ``frontend/src/pages/AdminPipeline.tsx` — h2 to promote to h1 (line 577)`
## Expected Output
- ``frontend/src/App.tsx` — nav brand uses `<span>` instead of `<h1>`, skip link added, main has id`
- ``frontend/src/App.css``.app-header h1` changed to `.app-header__brand span`, skip-link styles added, `--color-text-muted` updated to `#828291``
- ``frontend/src/pages/Home.tsx` — heading levels fixed (h3→h2 for How It Works, Featured, Recently Added, nav cards)`
- ``frontend/src/pages/TopicsBrowse.tsx` — h2→h1 promoted`
- ``frontend/src/pages/CreatorsBrowse.tsx` — h2→h1 promoted`
- ``frontend/src/pages/SubTopicPage.tsx` — h2→h1 promoted`
- ``frontend/src/pages/SearchResults.tsx` — h1 heading added`
- ``frontend/src/pages/AdminReports.tsx` — h2→h1 promoted`
- ``frontend/src/pages/AdminPipeline.tsx` — h2→h1 promoted`
## Verification
cd frontend && npx tsc --noEmit && npm run build

View file

@ -0,0 +1,92 @@
---
id: T01
parent: S04
milestone: M011
provides: []
requires: []
affects: []
key_files: ["frontend/src/App.tsx", "frontend/src/App.css", "frontend/src/pages/Home.tsx", "frontend/src/pages/SearchResults.tsx", "frontend/src/pages/TopicsBrowse.tsx", "frontend/src/pages/CreatorsBrowse.tsx", "frontend/src/pages/SubTopicPage.tsx", "frontend/src/pages/AdminReports.tsx", "frontend/src/pages/AdminPipeline.tsx"]
key_decisions: ["Used visually-hidden sr-only h1 for SearchResults since the page has no visible heading", "Changed nav brand from h1 to span preserving existing CSS styling approach"]
patterns_established: []
drill_down_paths: []
observability_surfaces: []
duration: ""
verification_result: "TypeScript compilation (tsc --noEmit) and production build (npm run build) both pass with exit code 0. Verified each page has exactly one h1, skip-link and main-content id are present in App.tsx, and muted color token is updated."
completed_at: 2026-03-31T08:52:36.572Z
blocker_discovered: false
---
# T01: Demoted nav brand to span, promoted page headings to h1, added skip-to-content link, and raised muted text contrast to AA compliance
> Demoted nav brand to span, promoted page headings to h1, added skip-to-content link, and raised muted text contrast to AA compliance
## What Happened
---
id: T01
parent: S04
milestone: M011
key_files:
- frontend/src/App.tsx
- frontend/src/App.css
- frontend/src/pages/Home.tsx
- frontend/src/pages/SearchResults.tsx
- frontend/src/pages/TopicsBrowse.tsx
- frontend/src/pages/CreatorsBrowse.tsx
- frontend/src/pages/SubTopicPage.tsx
- frontend/src/pages/AdminReports.tsx
- frontend/src/pages/AdminPipeline.tsx
key_decisions:
- Used visually-hidden sr-only h1 for SearchResults since the page has no visible heading
- Changed nav brand from h1 to span preserving existing CSS styling approach
duration: ""
verification_result: passed
completed_at: 2026-03-31T08:52:36.572Z
blocker_discovered: false
---
# T01: Demoted nav brand to span, promoted page headings to h1, added skip-to-content link, and raised muted text contrast to AA compliance
**Demoted nav brand to span, promoted page headings to h1, added skip-to-content link, and raised muted text contrast to AA compliance**
## What Happened
Applied three accessibility fixes across 9 files: (1) Fixed heading hierarchy by demoting nav h1 to span and promoting h2→h1 on all page components, added sr-only h1 to SearchResults, fixed h3→h2 level skips in Home.tsx. (2) Added skip-to-content link with keyboard-focusable skip-link pattern. (3) Changed --color-text-muted from #6b6b7a to #828291 for AA contrast compliance.
## Verification
TypeScript compilation (tsc --noEmit) and production build (npm run build) both pass with exit code 0. Verified each page has exactly one h1, skip-link and main-content id are present in App.tsx, and muted color token is updated.
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 2600ms |
| 2 | `cd frontend && npm run build` | 0 | ✅ pass | 3100ms |
## Deviations
None.
## Known Issues
None.
## Files Created/Modified
- `frontend/src/App.tsx`
- `frontend/src/App.css`
- `frontend/src/pages/Home.tsx`
- `frontend/src/pages/SearchResults.tsx`
- `frontend/src/pages/TopicsBrowse.tsx`
- `frontend/src/pages/CreatorsBrowse.tsx`
- `frontend/src/pages/SubTopicPage.tsx`
- `frontend/src/pages/AdminReports.tsx`
- `frontend/src/pages/AdminPipeline.tsx`
## Deviations
None.
## Known Issues
None.

View file

@ -0,0 +1,56 @@
---
estimated_steps: 14
estimated_files: 11
skills_used: []
---
# T02: Add useDocumentTitle hook and wire descriptive titles into all pages
Create a `useDocumentTitle` custom hook and call it from every page component so browser tabs show descriptive, route-specific titles.
1. Create `frontend/src/hooks/useDocumentTitle.ts` with a hook that sets `document.title` on mount and when the title string changes. Reset to 'Chrysopedia' on unmount (cleanup).
2. Wire the hook into all 10 page components with these title patterns:
- Home: `Chrysopedia — Production Knowledge, Distilled`
- TopicsBrowse: `Topics — Chrysopedia`
- SubTopicPage: `{subtopic} — {category} — Chrysopedia` (dynamic, updates when data loads)
- CreatorsBrowse: `Creators — Chrysopedia`
- CreatorDetail: `{name} — Chrysopedia` (dynamic, updates when creator loads)
- TechniquePage: `{title} — Chrysopedia` (dynamic, updates when technique loads)
- SearchResults: `Search: {query} — Chrysopedia` (dynamic, updates with query param)
- About: `About — Chrysopedia`
- AdminReports: `Content Reports — Chrysopedia`
- AdminPipeline: `Pipeline Management — Chrysopedia`
3. For dynamic pages (SubTopicPage, CreatorDetail, TechniquePage, SearchResults), call the hook with the resolved value so the title updates when async data loads. Use a fallback like `Chrysopedia` while loading.
## Inputs
- ``frontend/src/pages/Home.tsx` — needs useDocumentTitle call`
- ``frontend/src/pages/TopicsBrowse.tsx` — needs useDocumentTitle call`
- ``frontend/src/pages/SubTopicPage.tsx` — needs dynamic useDocumentTitle call with subtopic/category`
- ``frontend/src/pages/CreatorsBrowse.tsx` — needs useDocumentTitle call`
- ``frontend/src/pages/CreatorDetail.tsx` — needs dynamic useDocumentTitle call with creator name`
- ``frontend/src/pages/TechniquePage.tsx` — needs dynamic useDocumentTitle call with technique title`
- ``frontend/src/pages/SearchResults.tsx` — needs dynamic useDocumentTitle call with search query`
- ``frontend/src/pages/About.tsx` — needs useDocumentTitle call`
- ``frontend/src/pages/AdminReports.tsx` — needs useDocumentTitle call`
- ``frontend/src/pages/AdminPipeline.tsx` — needs useDocumentTitle call`
## Expected Output
- ``frontend/src/hooks/useDocumentTitle.ts` — new custom hook file`
- ``frontend/src/pages/Home.tsx` — useDocumentTitle wired`
- ``frontend/src/pages/TopicsBrowse.tsx` — useDocumentTitle wired`
- ``frontend/src/pages/SubTopicPage.tsx` — useDocumentTitle wired with dynamic subtopic/category`
- ``frontend/src/pages/CreatorsBrowse.tsx` — useDocumentTitle wired`
- ``frontend/src/pages/CreatorDetail.tsx` — useDocumentTitle wired with dynamic creator name`
- ``frontend/src/pages/TechniquePage.tsx` — useDocumentTitle wired with dynamic technique title`
- ``frontend/src/pages/SearchResults.tsx` — useDocumentTitle wired with dynamic query`
- ``frontend/src/pages/About.tsx` — useDocumentTitle wired`
- ``frontend/src/pages/AdminReports.tsx` — useDocumentTitle wired`
- ``frontend/src/pages/AdminPipeline.tsx` — useDocumentTitle wired`
## Verification
cd frontend && npx tsc --noEmit && npm run build

View file

@ -13,7 +13,7 @@
/* Text */
--color-text-primary: #e2e2ea;
--color-text-secondary: #8b8b9a;
--color-text-muted: #6b6b7a;
--color-text-muted: #828291;
--color-text-active: #e2e2ea;
--color-text-on-header: rgba(255, 255, 255, 0.8);
--color-text-on-header-hover: #fff;
@ -155,6 +155,20 @@ body {
background: var(--color-bg-page);
}
/* ── Utility ──────────────────────────────────────────────────────────────── */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ── App shell ────────────────────────────────────────────────────────────── */
.app {
@ -163,6 +177,26 @@ body {
min-height: 100vh;
}
/* ── Skip-to-content link ─────────────────────────────────────────────────── */
.skip-link {
position: absolute;
left: -9999px;
top: 0;
z-index: 999;
padding: 0.5rem 1rem;
background: var(--color-accent);
color: var(--color-bg-page);
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
border-radius: 0 0 0.375rem 0;
}
.skip-link:focus {
left: 0;
}
.app-header {
display: flex;
align-items: center;
@ -172,7 +206,7 @@ body {
color: var(--color-text-on-header-hover);
}
.app-header h1 {
.app-header__brand span {
font-size: 1.125rem;
font-weight: 600;
letter-spacing: -0.01em;

View file

@ -58,9 +58,10 @@ export default function App() {
return (
<div className="app">
<a href="#main-content" className="skip-link">Skip to content</a>
<header className="app-header" ref={headerRef}>
<Link to="/" className="app-header__brand">
<h1>Chrysopedia</h1>
<span>Chrysopedia</span>
</Link>
{showNavSearch && (
<SearchAutocomplete
@ -111,7 +112,7 @@ export default function App() {
</div>
</header>
<main className="app-main">
<main className="app-main" id="main-content">
<Routes>
{/* Public routes */}
<Route path="/" element={<Home />} />

View file

@ -574,7 +574,7 @@ export default function AdminPipeline() {
<div className="admin-pipeline">
<div className="admin-pipeline__header">
<div>
<h2 className="admin-pipeline__title">Pipeline Management</h2>
<h1 className="admin-pipeline__title">Pipeline Management</h1>
<p className="admin-pipeline__subtitle">
{videos.length} video{videos.length !== 1 ? "s" : ""}
</p>

View file

@ -113,7 +113,7 @@ export default function AdminReports() {
return (
<div className="admin-reports">
<h2 className="admin-reports__title">Content Reports</h2>
<h1 className="admin-reports__title">Content Reports</h1>
<p className="admin-reports__subtitle">
{total} report{total !== 1 ? "s" : ""} total
</p>

View file

@ -86,7 +86,7 @@ export default function CreatorsBrowse() {
return (
<div className="creators-browse">
<h2 className="creators-browse__title">Creators</h2>
<h1 className="creators-browse__title">Creators</h1>
<p className="creators-browse__subtitle">
Discover creators and their technique libraries
</p>

View file

@ -117,21 +117,21 @@ export default function Home() {
<div className="home-how-it-works">
<div className="home-how-it-works__step">
<span className="home-how-it-works__number">1</span>
<h3 className="home-how-it-works__title">Creators Share Techniques</h3>
<h2 className="home-how-it-works__title">Creators Share Techniques</h2>
<p className="home-how-it-works__desc">
Real producers and sound designers publish in-depth tutorials
</p>
</div>
<div className="home-how-it-works__step">
<span className="home-how-it-works__number">2</span>
<h3 className="home-how-it-works__title">AI Extracts Key Moments</h3>
<h2 className="home-how-it-works__title">AI Extracts Key Moments</h2>
<p className="home-how-it-works__desc">
We distill hours of video into structured, searchable knowledge
</p>
</div>
<div className="home-how-it-works__step">
<span className="home-how-it-works__number">3</span>
<h3 className="home-how-it-works__title">You Find Answers Fast</h3>
<h2 className="home-how-it-works__title">You Find Answers Fast</h2>
<p className="home-how-it-works__desc">
Search by topic, technique, or creator get straight to the insight
</p>
@ -161,13 +161,13 @@ export default function Home() {
{/* Navigation cards */}
<section className="nav-cards">
<Link to="/topics" className="nav-card card-stagger" style={{ '--stagger-index': 0 } as React.CSSProperties}>
<h3 className="nav-card__title"><IconTopics /> Topics</h3>
<h2 className="nav-card__title"><IconTopics /> Topics</h2>
<p className="nav-card__desc">
Browse techniques organized by category and sub-topic
</p>
</Link>
<Link to="/creators" className="nav-card card-stagger" style={{ '--stagger-index': 1 } as React.CSSProperties}>
<h3 className="nav-card__title"><IconCreators /> Creators</h3>
<h2 className="nav-card__title"><IconCreators /> Creators</h2>
<p className="nav-card__desc">
Discover creators and their technique libraries
</p>
@ -188,7 +188,7 @@ export default function Home() {
{/* Featured Technique Spotlight */}
{featured && (
<section className="home-featured">
<h3 className="home-featured__label">Featured Technique</h3>
<h2 className="home-featured__label">Featured Technique</h2>
<Link to={`/techniques/${featured.slug}`} className="home-featured__title">
{featured.title}
</Link>
@ -218,7 +218,7 @@ export default function Home() {
{/* Recently Added */}
<section className="recent-section">
<h3 className="recent-section__title">Recently Added</h3>
<h2 className="recent-section__title">Recently Added</h2>
{recentLoading ? (
<div className="loading">Loading</div>
) : recent.length === 0 ? (

View file

@ -52,6 +52,7 @@ export default function SearchResults() {
return (
<div className="search-results-page">
<h1 className="sr-only">Search Results</h1>
{/* Inline search bar */}
<SearchAutocomplete
variant="inline"

View file

@ -109,7 +109,7 @@ export default function SubTopicPage() {
<span className="breadcrumbs__current" aria-current="page">{subtopicDisplay}</span>
</nav>
<h2 className="subtopic-page__title">{subtopicDisplay}</h2>
<h1 className="subtopic-page__title">{subtopicDisplay}</h1>
<p className="subtopic-page__subtitle">
<span className={`badge badge--cat-${slug}`}>{categoryDisplay}</span>
<span className="subtopic-page__subtitle-sep">·</span>

View file

@ -93,7 +93,7 @@ export default function TopicsBrowse() {
return (
<div className="topics-browse">
<h2 className="topics-browse__title">Topics</h2>
<h1 className="topics-browse__title">Topics</h1>
<p className="topics-browse__subtitle">
Browse techniques organized by category and sub-topic
</p>