feat: Moved Table of Contents from main prose column to sidebar top; re…
- "frontend/src/pages/TechniquePage.tsx" - "frontend/src/components/TableOfContents.tsx" - "frontend/src/App.css" GSD-Task: S04/T01
This commit is contained in:
parent
61546bf25b
commit
0743d80b6a
15 changed files with 520 additions and 32 deletions
|
|
@ -34,3 +34,4 @@
|
|||
| D026 | | requirement | R033 | validated | Creators browse page shows "Last updated: Apr 2/3" per creator with techniques, omits for 0-technique creators. Homepage recently-added cards show subtle date stamps. Both verified live on ub01:8096. | Yes | agent |
|
||||
| D027 | | requirement | R034 | validated | Homepage renders stats block with real counts from the API: GET /api/v1/stats returns {"technique_count":21,"creator_count":7}, and the frontend scorecard displays "21 ARTICLES" and "7 CREATORS" in cyan-on-dark design. Visual and API verification both pass. | Yes | agent |
|
||||
| D028 | | requirement | R036 | validated | AdminDropdown.tsx now opens on hover at desktop widths (≥769px) via matchMedia guard with 150ms leave delay, while mobile retains tap-to-toggle. Build passes. Satisfies R036 criteria. | Yes | agent |
|
||||
| D029 | | requirement | R039 | validated | Build output confirms favicon.svg, favicon-32.png, apple-touch-icon.png, og-image.png in dist/. index.html contains all required OG, Twitter card, and favicon meta tags. Inline SVG logo mark present in header. All slice plan verification checks pass. | Yes | agent |
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Modernize the public site's visual identity and reading experience: fix landing
|
|||
|----|-------|------|---------|------|------------|
|
||||
| S01 | Landing Page Visual Fixes | low | — | ✅ | Homepage has consistent 42rem max-width, unified spacing, rounded featured card, correct CTA button sizing. |
|
||||
| S02 | Pipeline Admin UI Fixes | low | — | ✅ | Pipeline admin: collapse toggle works, mobile cards truncate, chevrons between stages, filter button group right-aligned, creator dropdown populates. |
|
||||
| S03 | Brand Minimum (Favicon, OG Tags, Logo) | low | — | ⬜ | Browser tab shows custom favicon. URL sharing produces preview card. Logo visible in header next to Chrysopedia. |
|
||||
| S03 | Brand Minimum (Favicon, OG Tags, Logo) | low | — | ✅ | Browser tab shows custom favicon. URL sharing produces preview card. Logo visible in header next to Chrysopedia. |
|
||||
| S04 | ToC Modernization | medium | — | ⬜ | Technique page ToC: no counters, left accent bar, On this page heading, active section highlighting on scroll, sticky in sidebar. |
|
||||
| S05 | Sticky Reading Header | medium | S04 | ⬜ | Thin sticky bar appears when scrolling past article title, shows title + current section, slides in/out. |
|
||||
| S06 | Landing Page Personality Pass | low | S01, S03 | ⬜ | Homepage: animated stat count-up, consistent section headings, content above fold, header brand accent with logo. |
|
||||
|
|
|
|||
86
.gsd/milestones/M016/slices/S03/S03-SUMMARY.md
Normal file
86
.gsd/milestones/M016/slices/S03/S03-SUMMARY.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
id: S03
|
||||
parent: M016
|
||||
milestone: M016
|
||||
provides:
|
||||
- favicon.svg and PNG assets in frontend/public/
|
||||
- OG/Twitter meta tags in index.html
|
||||
- Inline SVG logo mark in header brand area
|
||||
requires:
|
||||
[]
|
||||
affects:
|
||||
- S06
|
||||
key_files:
|
||||
- frontend/public/favicon.svg
|
||||
- frontend/public/favicon-32.png
|
||||
- frontend/public/apple-touch-icon.png
|
||||
- frontend/public/og-image.png
|
||||
- frontend/index.html
|
||||
- frontend/src/App.tsx
|
||||
- frontend/src/App.css
|
||||
key_decisions:
|
||||
- Generated PNG assets programmatically using Python stdlib to avoid external image tool dependencies
|
||||
- Used favicon mark without background rect for inline header logo — decorative with aria-hidden
|
||||
patterns_established:
|
||||
- Brand assets in frontend/public/ with SVG primary + PNG fallback pattern
|
||||
- Inline SVG logo mark with aria-hidden for decorative header elements
|
||||
observability_surfaces:
|
||||
- none
|
||||
drill_down_paths:
|
||||
- .gsd/milestones/M016/slices/S03/tasks/T01-SUMMARY.md
|
||||
- .gsd/milestones/M016/slices/S03/tasks/T02-SUMMARY.md
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-03T05:48:09.839Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# S03: Brand Minimum (Favicon, OG Tags, Logo)
|
||||
|
||||
**Added favicon (SVG + PNG fallback), apple-touch-icon, OG/Twitter social meta tags, and inline SVG logo mark in header.**
|
||||
|
||||
## What Happened
|
||||
|
||||
Two tasks delivered the full brand minimum. T01 created four static assets in frontend/public/ — an SVG favicon (stylized C arc + dot in #22d3ee on dark background), a 32px PNG fallback, a 180px apple-touch-icon, and a 1200×630 OG social image. All PNGs were generated programmatically using Python stdlib (struct + zlib) to avoid external image tool dependencies. index.html was updated with favicon link tags, OG meta tags (title, description, image, type), Twitter card tags (summary_large_image), and a description meta tag. T02 added the same arc+dot mark as an inline SVG in the header brand area (App.tsx), wrapped in a .app-header__logo span with aria-hidden for decorative semantics. The .app-header__brand container was updated to flex layout with gap for clean logo/text alignment at 24px height. Build passes cleanly (57 modules, 915ms). All assets copy to dist/ and all meta tags appear in the built index.html.
|
||||
|
||||
## Verification
|
||||
|
||||
Ran npm run build (exit 0, 915ms). Verified all 4 assets exist in dist/ (favicon.svg, favicon-32.png, apple-touch-icon.png, og-image.png). Grepped dist/index.html for all required patterns: rel="icon", og:title, og:description, og:image, twitter:card, name="description" — all present. Confirmed app-header__logo class in App.tsx and App.css, inline SVG present in App.tsx, and #22d3ee accent color in favicon.svg.
|
||||
|
||||
## Requirements Advanced
|
||||
|
||||
- R039 — Favicon, OG tags, Twitter card tags, description meta, and inline SVG logo all delivered and verified in build output.
|
||||
|
||||
## Requirements Validated
|
||||
|
||||
- R039 — Build produces favicon.svg, favicon-32.png, apple-touch-icon.png, og-image.png in dist/. index.html contains rel=icon, og:title, og:description, og:image, og:type, twitter:card, twitter:title, twitter:description, twitter:image, and meta description. Header shows inline SVG logo mark.
|
||||
|
||||
## New Requirements Surfaced
|
||||
|
||||
None.
|
||||
|
||||
## Requirements Invalidated or Re-scoped
|
||||
|
||||
None.
|
||||
|
||||
## Deviations
|
||||
|
||||
T01 switched from flat-list pixel generation to scanline-based with bounding-box culling for large PNG generation (OG image) after initial approach timed out.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
OG image uses programmatically generated shapes rather than rendered text — social previews show the brand colors and mark but not the "Chrysopedia" wordmark. Adequate for initial brand presence; can be replaced with a designed asset later.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
Replace programmatic PNGs with professionally designed assets when brand identity matures. Add per-page OG tags (technique pages should have unique og:title/og:description).
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/public/favicon.svg` — New: SVG favicon with stylized C arc + dot in #22d3ee
|
||||
- `frontend/public/favicon-32.png` — New: 32px PNG favicon fallback
|
||||
- `frontend/public/apple-touch-icon.png` — New: 180px Apple touch icon
|
||||
- `frontend/public/og-image.png` — New: 1200x630 OG social sharing image
|
||||
- `frontend/index.html` — Added favicon links, OG tags, Twitter card tags, description meta
|
||||
- `frontend/src/App.tsx` — Added inline SVG logo mark in header brand area
|
||||
- `frontend/src/App.css` — Added .app-header__logo styles, updated .app-header__brand to flex
|
||||
68
.gsd/milestones/M016/slices/S03/S03-UAT.md
Normal file
68
.gsd/milestones/M016/slices/S03/S03-UAT.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# S03: Brand Minimum (Favicon, OG Tags, Logo) — UAT
|
||||
|
||||
**Milestone:** M016
|
||||
**Written:** 2026-04-03T05:48:09.839Z
|
||||
|
||||
# S03 UAT: Brand Minimum (Favicon, OG Tags, Logo)
|
||||
|
||||
## Preconditions
|
||||
- Frontend built successfully (`cd frontend && npm run build`)
|
||||
- App running (locally or via Docker on ub01:8096)
|
||||
|
||||
## Test Cases
|
||||
|
||||
### TC1: Favicon displays in browser tab
|
||||
1. Open the site in a browser
|
||||
2. **Expected:** Browser tab shows a cyan arc+dot mark on dark background (not the default browser/framework icon)
|
||||
3. Check favicon source: right-click tab → inspect → should reference `/favicon.svg`
|
||||
|
||||
### TC2: PNG favicon fallback
|
||||
1. Open the site in a browser that doesn't support SVG favicons (or inspect network tab)
|
||||
2. **Expected:** `/favicon-32.png` is available as 32x32 fallback
|
||||
3. Verify: `curl -s http://localhost:8096/favicon-32.png | file -` → should report PNG image data
|
||||
|
||||
### TC3: Apple touch icon
|
||||
1. Inspect page source or `curl http://localhost:8096/apple-touch-icon.png | file -`
|
||||
2. **Expected:** Valid 180x180 PNG returned
|
||||
|
||||
### TC4: OG meta tags present for social sharing
|
||||
1. View page source of the homepage
|
||||
2. **Expected:** All of these meta tags present in `<head>`:
|
||||
- `<meta property="og:title" content="Chrysopedia" />`
|
||||
- `<meta property="og:description" content="Music production technique encyclopedia" />`
|
||||
- `<meta property="og:image" content="/og-image.png" />`
|
||||
- `<meta property="og:type" content="website" />`
|
||||
3. Verify OG image accessible: `curl -sI http://localhost:8096/og-image.png` → 200 OK
|
||||
|
||||
### TC5: Twitter card meta tags
|
||||
1. View page source of the homepage
|
||||
2. **Expected:**
|
||||
- `<meta name="twitter:card" content="summary_large_image" />`
|
||||
- `<meta name="twitter:title" content="Chrysopedia" />`
|
||||
- `<meta name="twitter:description" content="Music production technique encyclopedia" />`
|
||||
- `<meta name="twitter:image" content="/og-image.png" />`
|
||||
|
||||
### TC6: Description meta tag
|
||||
1. View page source
|
||||
2. **Expected:** `<meta name="description" content="Music production technique encyclopedia" />`
|
||||
|
||||
### TC7: Logo visible in header
|
||||
1. Open any page on the site
|
||||
2. **Expected:** A cyan arc+dot mark (~24px) appears to the left of "Chrysopedia" text in the header
|
||||
3. The logo should be vertically centered with the text
|
||||
4. The logo should NOT be announced by screen readers (aria-hidden)
|
||||
|
||||
### TC8: Logo uses correct brand color
|
||||
1. Inspect the inline SVG in the header
|
||||
2. **Expected:** SVG paths use `#22d3ee` (cyan accent) — matching the favicon
|
||||
|
||||
### Edge Cases
|
||||
|
||||
### EC1: Social sharing preview
|
||||
1. Paste the site URL into a social platform preview tool (e.g., Twitter Card Validator, Facebook Sharing Debugger, or opengraph.xyz)
|
||||
2. **Expected:** Preview card shows "Chrysopedia" title, "Music production technique encyclopedia" description, and the OG image
|
||||
3. **Note:** OG image URL must be absolute for external validators — current implementation uses relative `/og-image.png` which works when the site has a canonical URL configured
|
||||
|
||||
### EC2: Mobile favicon
|
||||
1. Open the site on iOS Safari and add to home screen
|
||||
2. **Expected:** Home screen icon uses the apple-touch-icon (180px version)
|
||||
36
.gsd/milestones/M016/slices/S03/tasks/T02-VERIFY.json
Normal file
36
.gsd/milestones/M016/slices/S03/tasks/T02-VERIFY.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T02",
|
||||
"unitId": "M016/S03/T02",
|
||||
"timestamp": 1775195225292,
|
||||
"passed": false,
|
||||
"discoverySource": "task-plan",
|
||||
"checks": [
|
||||
{
|
||||
"command": "cd frontend",
|
||||
"exitCode": 0,
|
||||
"durationMs": 18,
|
||||
"verdict": "pass"
|
||||
},
|
||||
{
|
||||
"command": "npm run build",
|
||||
"exitCode": 254,
|
||||
"durationMs": 99,
|
||||
"verdict": "fail"
|
||||
},
|
||||
{
|
||||
"command": "grep -q 'app-header__logo' src/App.tsx",
|
||||
"exitCode": 2,
|
||||
"durationMs": 14,
|
||||
"verdict": "fail"
|
||||
},
|
||||
{
|
||||
"command": "grep -q 'app-header__logo' src/App.css",
|
||||
"exitCode": 2,
|
||||
"durationMs": 11,
|
||||
"verdict": "fail"
|
||||
}
|
||||
],
|
||||
"retryAttempt": 1,
|
||||
"maxRetries": 2
|
||||
}
|
||||
|
|
@ -1,6 +1,46 @@
|
|||
# S04: ToC Modernization
|
||||
|
||||
**Goal:** Modernize technique page table of contents: remove numbered counters, add left accent bar, sentence-case heading, hover backgrounds, IntersectionObserver active section tracking, sticky positioning.
|
||||
**Goal:** Technique page ToC: no counters, left accent bar, "On this page" heading, active section highlighting on scroll, sticky in sidebar.
|
||||
**Demo:** After this: Technique page ToC: no counters, left accent bar, On this page heading, active section highlighting on scroll, sticky in sidebar.
|
||||
|
||||
## Tasks
|
||||
- [x] **T01: Moved Table of Contents from main prose column to sidebar top; replaced card/counter styling with left accent bar and hover backgrounds** — Move the TableOfContents render from inside technique-columns__main (inside technique-prose) to the top of technique-columns__sidebar. Restyle: remove CSS counters, remove card background, add left accent bar, rename heading to 'On this page', add hover background states on links.
|
||||
|
||||
This slice owns R040 (Table of Contents Modernization). The ToC currently renders at line 425 of TechniquePage.tsx inside the v2 prose section. It needs to move to the sidebar div (line 470). Only render ToC in sidebar for v2 format pages.
|
||||
|
||||
CSS changes in App.css (lines 2034–2108):
|
||||
- Remove counter-reset, counter-increment, and ::before pseudo-elements
|
||||
- Remove background/border card styling
|
||||
- Add border-left: 2px solid var(--color-accent) accent bar
|
||||
- Add hover background: var(--color-accent-subtle) on links
|
||||
- Change .technique-toc__title text to be set by the component (not CSS)
|
||||
|
||||
Component changes in TableOfContents.tsx:
|
||||
- Change heading from 'Contents' to 'On this page'
|
||||
- Change <ol> to <ul> (no ordered list needed without counters)
|
||||
|
||||
Layout note: The sidebar is already position: sticky; top: 1.5rem. Placing ToC at the top of the sidebar makes it sticky automatically.
|
||||
- Estimate: 30m
|
||||
- Files: frontend/src/pages/TechniquePage.tsx, frontend/src/components/TableOfContents.tsx, frontend/src/App.css
|
||||
- Verify: cd frontend && npm run build 2>&1 | tail -5 && echo 'Build OK'
|
||||
- [ ] **T02: Add IntersectionObserver active section tracking** — Add scroll-based active section highlighting to the ToC using IntersectionObserver.
|
||||
|
||||
In TableOfContents.tsx:
|
||||
- Add state: const [activeId, setActiveId] = useState<string>('')
|
||||
- useEffect: create IntersectionObserver watching all section/subsection IDs
|
||||
- rootMargin: '0px 0px -70% 0px' — triggers when section enters top 30% of viewport
|
||||
- On intersect: find the topmost intersecting entry, setActiveId to its target.id
|
||||
- Collect all IDs from sections prop: H2 = slugify(heading), H3 = sectionSlug--slugify(sub.heading)
|
||||
- Observe each element by document.getElementById(id)
|
||||
- Cleanup: disconnect observer on unmount
|
||||
- Apply active class: technique-toc__link--active (or sublink variant) when href matches activeId
|
||||
|
||||
In App.css, add active state styles:
|
||||
- .technique-toc__link--active: color: var(--color-accent), font-weight 500, border-left or background highlight
|
||||
- .technique-toc__sublink--active: same treatment
|
||||
- Ensure active state is visually distinct from hover state
|
||||
|
||||
Slug convention (from KNOWLEDGE.md): H2 = slugify(heading), H3 = sectionSlug--slugify(sub.heading) with double-hyphen separator. These IDs are already set on the div elements in TechniquePage.tsx prose rendering.
|
||||
- Estimate: 30m
|
||||
- Files: frontend/src/components/TableOfContents.tsx, frontend/src/App.css
|
||||
- Verify: cd frontend && npm run build 2>&1 | tail -5 && echo 'Build OK'
|
||||
|
|
|
|||
75
.gsd/milestones/M016/slices/S04/S04-RESEARCH.md
Normal file
75
.gsd/milestones/M016/slices/S04/S04-RESEARCH.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# S04 Research — ToC Modernization
|
||||
|
||||
## Summary
|
||||
|
||||
Straightforward frontend slice. The TableOfContents component and its CSS exist, the page layout is already a 2-column grid with a sticky sidebar. The work is: relocate ToC from main column to sidebar, restyle it (remove counters, add left accent bar, rename heading), and add active-section tracking via IntersectionObserver.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Single-pass implementation. No new libraries needed — IntersectionObserver is native browser API. No backend changes. The work divides into two natural tasks: (1) move + restyle ToC, (2) add active section tracking.
|
||||
|
||||
## Implementation Landscape
|
||||
|
||||
### Current State
|
||||
|
||||
**`frontend/src/components/TableOfContents.tsx`** — Stateless component. Takes `BodySectionV2[]` sections, renders nested `<ol>` with anchor links. Uses `slugify()` for IDs. No scroll tracking, no active state.
|
||||
|
||||
**`frontend/src/pages/TechniquePage.tsx`** — Two-column grid layout (`technique-columns`). ToC currently renders at line 424 inside `technique-columns__main`, inside the `technique-prose` section (before the actual section content). Sidebar (`technique-columns__sidebar`) contains: key moments, signal chains, related techniques.
|
||||
|
||||
**`frontend/src/App.css`** (lines 2034–2108) — ToC styles use CSS counters (`counter-reset: toc-section`, `counter-increment`) to generate numbered prefixes like "1. ", "1.2 ". Background card style with border-radius.
|
||||
|
||||
**Layout** (lines 1849–1878) — `technique-columns`: `grid-template-columns: 1fr 22rem`, sidebar is `position: sticky; top: 1.5rem`. At ≤768px, collapses to single column with static sidebar.
|
||||
|
||||
### What Changes
|
||||
|
||||
1. **Move ToC to sidebar** — In TechniquePage.tsx, move the `<TableOfContents>` render from inside `technique-columns__main` (line 424) to the top of `technique-columns__sidebar` (before key moments, line 471). Only render in sidebar for v2 format pages.
|
||||
|
||||
2. **Restyle ToC** — In App.css:
|
||||
- Remove `counter-reset`, `counter-increment`, and `::before` pseudo-elements (the numbered prefixes)
|
||||
- Remove background/border card styling, replace with `border-left: 2px solid var(--color-accent)` accent bar
|
||||
- Change title from "Contents" to "On this page" (in component)
|
||||
- Add `--toc-active` color variable or reuse `--color-accent` for active link highlighting
|
||||
|
||||
3. **Active section tracking** — In TableOfContents.tsx:
|
||||
- Accept section element IDs (already computed via `slugify()`)
|
||||
- Create an IntersectionObserver watching all `[id]` elements matching section/subsection slugs
|
||||
- Track which section is currently in view (topmost intersecting section with `rootMargin: '0px 0px -70% 0px'` to trigger when section enters top 30%)
|
||||
- Apply `.technique-toc__link--active` class to the matching link
|
||||
- CSS: active link gets `color: var(--color-accent)` + optionally a small left-border highlight or font-weight change
|
||||
|
||||
4. **Mobile behavior** — At ≤768px the sidebar is already `position: static`. ToC should still render but won't be sticky. This is fine — no special mobile handling needed beyond what exists.
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `frontend/src/components/TableOfContents.tsx` | Component: restyle, add active tracking |
|
||||
| `frontend/src/pages/TechniquePage.tsx` | Move ToC render location from main to sidebar |
|
||||
| `frontend/src/App.css` | ToC CSS: remove counters, add accent bar, add active state |
|
||||
|
||||
### CSS Variables Available
|
||||
|
||||
- `--color-accent: #22d3ee` (cyan) — for active link and accent bar
|
||||
- `--color-accent-subtle: rgba(34, 211, 238, 0.1)` — for active link background highlight
|
||||
- `--color-text-secondary: #8b8b9a` — for inactive links
|
||||
- `--color-text-muted: #828291` — for heading text
|
||||
- `--color-border: #2a2a38` — for inactive accent bar segments
|
||||
|
||||
### Slug ID Convention
|
||||
|
||||
Section IDs use `slugify(heading)` for H2 and `${sectionSlug}--${slugify(sub.heading)}` for H3 (double-hyphen compound slug, per KNOWLEDGE.md). These same IDs are set on the `<div id={...}>` elements in TechniquePage.tsx's prose rendering. The IntersectionObserver targets these exact IDs.
|
||||
|
||||
### Risks
|
||||
|
||||
None. Standard browser API, existing layout supports it, no backend changes.
|
||||
|
||||
### Verification
|
||||
|
||||
- `cd frontend && npm run build` — zero TypeScript errors
|
||||
- Visual: ToC appears in sidebar, no numbered counters, left accent bar visible
|
||||
- Scroll test: active section highlights as user scrolls through prose sections
|
||||
- Mobile (≤768px): ToC renders inline (not sticky), still functional
|
||||
|
||||
### Skills
|
||||
|
||||
No additional skills needed. Standard React + CSS work within established codebase patterns.
|
||||
40
.gsd/milestones/M016/slices/S04/tasks/T01-PLAN.md
Normal file
40
.gsd/milestones/M016/slices/S04/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
estimated_steps: 12
|
||||
estimated_files: 3
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T01: Relocate ToC to sidebar and restyle with accent bar
|
||||
|
||||
Move the TableOfContents render from inside technique-columns__main (inside technique-prose) to the top of technique-columns__sidebar. Restyle: remove CSS counters, remove card background, add left accent bar, rename heading to 'On this page', add hover background states on links.
|
||||
|
||||
This slice owns R040 (Table of Contents Modernization). The ToC currently renders at line 425 of TechniquePage.tsx inside the v2 prose section. It needs to move to the sidebar div (line 470). Only render ToC in sidebar for v2 format pages.
|
||||
|
||||
CSS changes in App.css (lines 2034–2108):
|
||||
- Remove counter-reset, counter-increment, and ::before pseudo-elements
|
||||
- Remove background/border card styling
|
||||
- Add border-left: 2px solid var(--color-accent) accent bar
|
||||
- Add hover background: var(--color-accent-subtle) on links
|
||||
- Change .technique-toc__title text to be set by the component (not CSS)
|
||||
|
||||
Component changes in TableOfContents.tsx:
|
||||
- Change heading from 'Contents' to 'On this page'
|
||||
- Change <ol> to <ul> (no ordered list needed without counters)
|
||||
|
||||
Layout note: The sidebar is already position: sticky; top: 1.5rem. Placing ToC at the top of the sidebar makes it sticky automatically.
|
||||
|
||||
## Inputs
|
||||
|
||||
- ``frontend/src/pages/TechniquePage.tsx` — current layout with ToC inside main column at line 425`
|
||||
- ``frontend/src/components/TableOfContents.tsx` — current component with 'Contents' heading and <ol> lists`
|
||||
- ``frontend/src/App.css` — ToC styles at lines 2034–2108 with CSS counters`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- ``frontend/src/pages/TechniquePage.tsx` — ToC moved to sidebar div, only for v2 pages`
|
||||
- ``frontend/src/components/TableOfContents.tsx` — heading renamed, ol→ul, ready for active state props`
|
||||
- ``frontend/src/App.css` — ToC styles: no counters, accent bar, hover backgrounds`
|
||||
|
||||
## Verification
|
||||
|
||||
cd frontend && npm run build 2>&1 | tail -5 && echo 'Build OK'
|
||||
78
.gsd/milestones/M016/slices/S04/tasks/T01-SUMMARY.md
Normal file
78
.gsd/milestones/M016/slices/S04/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S04
|
||||
milestone: M016
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["frontend/src/pages/TechniquePage.tsx", "frontend/src/components/TableOfContents.tsx", "frontend/src/App.css"]
|
||||
key_decisions: ["ToC only renders in sidebar for v2-format pages; v1 pages have no ToC at all"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "Production build passes cleanly: `cd frontend && npm run build` — 57 modules transformed, zero errors, zero warnings."
|
||||
completed_at: 2026-04-03T05:52:44.616Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Moved Table of Contents from main prose column to sidebar top; replaced card/counter styling with left accent bar and hover backgrounds
|
||||
|
||||
> Moved Table of Contents from main prose column to sidebar top; replaced card/counter styling with left accent bar and hover backgrounds
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T01
|
||||
parent: S04
|
||||
milestone: M016
|
||||
key_files:
|
||||
- frontend/src/pages/TechniquePage.tsx
|
||||
- frontend/src/components/TableOfContents.tsx
|
||||
- frontend/src/App.css
|
||||
key_decisions:
|
||||
- ToC only renders in sidebar for v2-format pages; v1 pages have no ToC at all
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-03T05:52:44.616Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Moved Table of Contents from main prose column to sidebar top; replaced card/counter styling with left accent bar and hover backgrounds
|
||||
|
||||
**Moved Table of Contents from main prose column to sidebar top; replaced card/counter styling with left accent bar and hover backgrounds**
|
||||
|
||||
## What Happened
|
||||
|
||||
Relocated TableOfContents render from inside the v2 prose fragment to the top of technique-columns__sidebar, gated on v2 format. Changed heading to "On this page", converted ol to ul. Replaced CSS card/counter styling with border-left accent bar and hover background states on all links.
|
||||
|
||||
## Verification
|
||||
|
||||
Production build passes cleanly: `cd frontend && npm run build` — 57 modules transformed, zero errors, zero warnings.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `cd frontend && npm run build 2>&1 | tail -5 && echo 'Build OK'` | 0 | ✅ pass | 1000ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/src/pages/TechniquePage.tsx`
|
||||
- `frontend/src/components/TableOfContents.tsx`
|
||||
- `frontend/src/App.css`
|
||||
|
||||
|
||||
## Deviations
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
40
.gsd/milestones/M016/slices/S04/tasks/T02-PLAN.md
Normal file
40
.gsd/milestones/M016/slices/S04/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
estimated_steps: 15
|
||||
estimated_files: 2
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T02: Add IntersectionObserver active section tracking
|
||||
|
||||
Add scroll-based active section highlighting to the ToC using IntersectionObserver.
|
||||
|
||||
In TableOfContents.tsx:
|
||||
- Add state: const [activeId, setActiveId] = useState<string>('')
|
||||
- useEffect: create IntersectionObserver watching all section/subsection IDs
|
||||
- rootMargin: '0px 0px -70% 0px' — triggers when section enters top 30% of viewport
|
||||
- On intersect: find the topmost intersecting entry, setActiveId to its target.id
|
||||
- Collect all IDs from sections prop: H2 = slugify(heading), H3 = sectionSlug--slugify(sub.heading)
|
||||
- Observe each element by document.getElementById(id)
|
||||
- Cleanup: disconnect observer on unmount
|
||||
- Apply active class: technique-toc__link--active (or sublink variant) when href matches activeId
|
||||
|
||||
In App.css, add active state styles:
|
||||
- .technique-toc__link--active: color: var(--color-accent), font-weight 500, border-left or background highlight
|
||||
- .technique-toc__sublink--active: same treatment
|
||||
- Ensure active state is visually distinct from hover state
|
||||
|
||||
Slug convention (from KNOWLEDGE.md): H2 = slugify(heading), H3 = sectionSlug--slugify(sub.heading) with double-hyphen separator. These IDs are already set on the div elements in TechniquePage.tsx prose rendering.
|
||||
|
||||
## Inputs
|
||||
|
||||
- ``frontend/src/components/TableOfContents.tsx` — restyled component from T01 with ul lists and 'On this page' heading`
|
||||
- ``frontend/src/App.css` — restyled ToC CSS from T01 with accent bar and hover states`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- ``frontend/src/components/TableOfContents.tsx` — IntersectionObserver tracking active section, applying active classes`
|
||||
- ``frontend/src/App.css` — active link styles (.technique-toc__link--active, .technique-toc__sublink--active)`
|
||||
|
||||
## Verification
|
||||
|
||||
cd frontend && npm run build 2>&1 | tail -5 && echo 'Build OK'
|
||||
24
alembic/versions/014_add_creator_avatar.py
Normal file
24
alembic/versions/014_add_creator_avatar.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""Add avatar columns to creators table.
|
||||
|
||||
Revision ID: 014_add_creator_avatar
|
||||
Revises: 013_add_search_log
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "014_add_creator_avatar"
|
||||
down_revision = "013_add_search_log"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("creators", sa.Column("avatar_url", sa.String(1000), nullable=True))
|
||||
op.add_column("creators", sa.Column("avatar_source", sa.String(50), nullable=True))
|
||||
op.add_column("creators", sa.Column("avatar_fetched_at", sa.TIMESTAMP(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("creators", "avatar_fetched_at")
|
||||
op.drop_column("creators", "avatar_source")
|
||||
op.drop_column("creators", "avatar_url")
|
||||
|
|
@ -103,6 +103,9 @@ class Creator(Base):
|
|||
slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
||||
genres: Mapped[list[str] | None] = mapped_column(ARRAY(String), nullable=True)
|
||||
folder_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
avatar_url: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
||||
avatar_source: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
avatar_fetched_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||||
view_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
||||
hidden: Mapped[bool] = mapped_column(default=False, server_default="false")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
|
|
|
|||
|
|
@ -2032,10 +2032,8 @@ a.app-footer__repo:hover {
|
|||
/* ── Table of Contents ────────────────────────────────────────────────────── */
|
||||
|
||||
.technique-toc {
|
||||
background: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-left: 2px solid var(--color-accent);
|
||||
padding: 0 0 0 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
|
|
@ -2052,56 +2050,52 @@ a.app-footer__repo:hover {
|
|||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
counter-reset: toc-section;
|
||||
}
|
||||
|
||||
.technique-toc__item {
|
||||
counter-increment: toc-section;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.technique-toc__link {
|
||||
color: var(--color-accent);
|
||||
display: block;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.technique-toc__link::before {
|
||||
content: counter(toc-section) ". ";
|
||||
color: var(--color-text-muted);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.technique-toc__link:hover {
|
||||
text-decoration: underline;
|
||||
background: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.technique-toc__sublist {
|
||||
list-style: none;
|
||||
padding-left: 1.25rem;
|
||||
padding-left: 1rem;
|
||||
margin: 0.125rem 0 0.25rem;
|
||||
counter-reset: toc-sub;
|
||||
}
|
||||
|
||||
.technique-toc__subitem {
|
||||
counter-increment: toc-sub;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.technique-toc__sublink {
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.technique-toc__sublink::before {
|
||||
content: counter(toc-section) "." counter(toc-sub) " ";
|
||||
color: var(--color-text-muted);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.technique-toc__sublink:hover {
|
||||
background: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── V2 subsections ───────────────────────────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
|
|||
|
||||
return (
|
||||
<nav className="technique-toc" aria-label="Table of contents">
|
||||
<h3 className="technique-toc__title">Contents</h3>
|
||||
<ol className="technique-toc__list">
|
||||
<h3 className="technique-toc__title">On this page</h3>
|
||||
<ul className="technique-toc__list">
|
||||
{sections.map((section) => {
|
||||
const sectionSlug = slugify(section.heading);
|
||||
return (
|
||||
|
|
@ -33,7 +33,7 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
|
|||
{section.heading}
|
||||
</a>
|
||||
{section.subsections.length > 0 && (
|
||||
<ol className="technique-toc__sublist">
|
||||
<ul className="technique-toc__sublist">
|
||||
{section.subsections.map((sub) => {
|
||||
const subSlug = `${sectionSlug}--${slugify(sub.heading)}`;
|
||||
return (
|
||||
|
|
@ -47,12 +47,12 @@ export default function TableOfContents({ sections }: TableOfContentsProps) {
|
|||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,7 +421,6 @@ export default function TechniquePage() {
|
|||
<section className="technique-prose">
|
||||
{displayFormat === "v2" && Array.isArray(displaySections) ? (
|
||||
<>
|
||||
<TableOfContents sections={displaySections as BodySectionV2[]} />
|
||||
{(displaySections as BodySectionV2[]).map((section) => {
|
||||
const sectionSlug = slugify(section.heading);
|
||||
return (
|
||||
|
|
@ -469,6 +468,10 @@ export default function TechniquePage() {
|
|||
|
||||
</div>
|
||||
<div className="technique-columns__sidebar">
|
||||
{/* Table of Contents — v2 pages only */}
|
||||
{displayFormat === "v2" && Array.isArray(displaySections) && (displaySections as BodySectionV2[]).length > 0 && (
|
||||
<TableOfContents sections={displaySections as BodySectionV2[]} />
|
||||
)}
|
||||
{/* Key moments (always from live data — not versioned) */}
|
||||
{technique.key_moments.length > 0 && (
|
||||
<section className="technique-moments">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue