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:
parent
089435a990
commit
5e5961fa92
18 changed files with 646 additions and 18 deletions
|
|
@ -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. |
|
||||
|
|
|
|||
85
.gsd/milestones/M011/slices/S03/S03-SUMMARY.md
Normal file
85
.gsd/milestones/M011/slices/S03/S03-SUMMARY.md
Normal 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"
|
||||
167
.gsd/milestones/M011/slices/S03/S03-UAT.md
Normal file
167
.gsd/milestones/M011/slices/S03/S03-UAT.md
Normal 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
|
||||
16
.gsd/milestones/M011/slices/S03/tasks/T02-VERIFY.json
Normal file
16
.gsd/milestones/M011/slices/S03/tasks/T02-VERIFY.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
103
.gsd/milestones/M011/slices/S04/S04-RESEARCH.md
Normal file
103
.gsd/milestones/M011/slices/S04/S04-RESEARCH.md
Normal 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.
|
||||
43
.gsd/milestones/M011/slices/S04/tasks/T01-PLAN.md
Normal file
43
.gsd/milestones/M011/slices/S04/tasks/T01-PLAN.md
Normal 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
|
||||
92
.gsd/milestones/M011/slices/S04/tasks/T01-SUMMARY.md
Normal file
92
.gsd/milestones/M011/slices/S04/tasks/T01-SUMMARY.md
Normal 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.
|
||||
56
.gsd/milestones/M011/slices/S04/tasks/T02-PLAN.md
Normal file
56
.gsd/milestones/M011/slices/S04/tasks/T02-PLAN.md
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue