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:
jlightner 2026-04-03 05:52:47 +00:00
parent 61546bf25b
commit 0743d80b6a
15 changed files with 520 additions and 32 deletions

View file

@ -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 |

View file

@ -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. |

View 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

View 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)

View 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
}

View file

@ -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 20342108):
- 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'

View 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 20342108) — 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 18491878) — `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.

View 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 20342108):
- 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 20342108 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'

View 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.

View 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'

View 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")

View file

@ -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(

View file

@ -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 ───────────────────────────────────────────────────────── */

View file

@ -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>
);
}

View file

@ -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">