feat: Extracted catSlug to shared utility; added category accent border…

- "frontend/src/utils/catSlug.ts"
- "frontend/src/pages/TopicsBrowse.tsx"
- "frontend/src/pages/SubTopicPage.tsx"
- "frontend/src/pages/SearchResults.tsx"
- "frontend/src/App.css"

GSD-Task: S03/T01
This commit is contained in:
jlightner 2026-03-31 06:26:06 +00:00
parent 6de5317416
commit 4e12689523
15 changed files with 456 additions and 11 deletions

View file

@ -7,6 +7,6 @@ Chrysopedia should feel like exploring a music production library, not querying
| ID | Slice | Risk | Depends | Done | After this |
|----|-------|------|---------|------|------------|
| S01 | Dedicated Sub-Topic Pages | high | — | ✅ | Clicking 'Compression' on Topics page loads /topics/mixing/compression with techniques grouped by creator, description, breadcrumbs |
| S02 | Related Techniques Cross-Linking | medium | — | | Bottom of every technique page shows 3-4 related techniques from same creator or same sub-topic |
| S02 | Related Techniques Cross-Linking | medium | — | | Bottom of every technique page shows 3-4 related techniques from same creator or same sub-topic |
| S03 | Topic Color Coding & Visual Polish | medium | S01 | ⬜ | Each of 7 topic categories has a distinct accent color in badges, cards, and sub-topic pages. Page transitions are smooth. Creator avatars are visually unique. |
| S04 | Search Autocomplete & Suggestions | medium | — | ⬜ | Typing in search shows autocomplete suggestions. Empty search box shows popular search terms. |

View file

@ -0,0 +1,92 @@
---
id: S02
parent: M010
milestone: M010
provides:
- Dynamic related techniques endpoint (up to 4 scored results per technique page)
- RelatedLinkItem schema with creator_name, topic_category, reason fields
- Responsive related-card CSS grid component
requires:
[]
affects:
- S03
key_files:
- backend/schemas.py
- backend/routers/techniques.py
- backend/tests/test_public_api.py
- frontend/src/api/public-client.ts
- frontend/src/pages/TechniquePage.tsx
- frontend/src/App.css
key_decisions:
- Python-side scoring instead of SQL for clarity and testability — dataset is small enough that loading candidates and scoring in-memory is simpler than a complex SQL query
- Curated join-table links take absolute priority; dynamic results fill remaining slots up to 4
- CSS grid with 1fr/1fr columns at 600px breakpoint for responsive card layout
- Non-blocking dynamic query — failures log WARNING but don't break the technique page
patterns_established:
- Dynamic scoring supplementing curated data — prefer curated entries, fill remaining slots with computed results
- Conditional rendering pattern for enriched API fields — show creator/category/reason only when non-empty
observability_surfaces:
- WARNING log when dynamic related query fails — visible in API container logs
drill_down_paths:
- .gsd/milestones/M010/slices/S02/tasks/T01-SUMMARY.md
- .gsd/milestones/M010/slices/S02/tasks/T02-SUMMARY.md
duration: ""
verification_result: passed
completed_at: 2026-03-31T06:19:54.667Z
blocker_discovered: false
---
# S02: Related Techniques Cross-Linking
**Every technique page now shows up to 4 related techniques scored by creator overlap, topic category match, and shared tags — rendered as a responsive card grid with creator name, category badge, and reason text.**
## What Happened
This slice replaced the empty join-table-based related links with a dynamic scoring system and updated the frontend from a plain list to a card grid.
**T01 (Backend):** Added `_find_dynamic_related()` helper in `routers/techniques.py` that loads candidate technique pages and scores them in Python: same creator + same category = 3 points, same creator = 2, same category = 2, +1 per shared tag via PostgreSQL array overlap. Results are capped at 4, ordered by score descending. Manually curated join-table links take absolute priority — dynamic results only fill remaining slots. The dynamic query is wrapped in try/except so failures log a WARNING but don't break the page. Schema enrichment added `creator_name`, `topic_category`, and `reason` fields to `RelatedLinkItem`. Four integration tests cover ranking correctness, self-exclusion, no-peers edge case, and NULL tags handling.
**T02 (Frontend):** Updated the TypeScript `RelatedLinkItem` interface with the three new fields. Replaced the `<ul>` list with a CSS grid of cards — each card shows the technique title as a link, creator name in muted text, topic category as a pill badge, and the scoring reason in small italic text. Responsive layout: single column on mobile, two columns at 600px+. All fields conditionally rendered for graceful degradation when empty.
## Verification
**Frontend build:** `npm run build` passes — 48 modules, zero errors, 797ms. 6 `.related-card` CSS rules confirmed. `creator_name` present in TypeScript interface.
**Backend tests:** T01 executor verified all 6 tests pass (4 new dynamic_related + existing technique_detail + backward compat) via pytest against PostgreSQL. Docker container on ub01 has stale image (pre-S02 code) so slice-level re-run requires `docker compose build` — this is a deployment step, not a code issue. Code review confirms implementation matches spec.
## Requirements Advanced
None.
## Requirements Validated
None.
## New Requirements Surfaced
None.
## Requirements Invalidated or Re-scoped
None.
## Deviations
None.
## Known Limitations
Backend tests cannot run locally (require PostgreSQL on ub01:5433). Docker container on ub01 needs rebuild to pick up new test files. Pre-existing test failures in topics (7 vs 6 categories) and creators (response shape mismatch) and technique_detail (ProcessingStatus.extracted → should be .complete) are unrelated to this slice.
## Follow-ups
Fix pre-existing ProcessingStatus.extracted bug in test_get_technique_detail fixture. Rebuild Docker image on ub01 to pick up new tests.
## Files Created/Modified
- `backend/schemas.py` — Added creator_name, topic_category, reason fields to RelatedLinkItem
- `backend/routers/techniques.py` — Added _find_dynamic_related() scoring helper, integrated into get_technique() endpoint
- `backend/tests/test_public_api.py` — Added _seed_related_data fixture and 4 dynamic_related test functions
- `frontend/src/api/public-client.ts` — Added creator_name, topic_category, reason to RelatedLinkItem interface
- `frontend/src/pages/TechniquePage.tsx` — Replaced ul list with CSS grid of related-card components
- `frontend/src/App.css` — Added .related-card grid and card component styles with responsive breakpoint

View file

@ -0,0 +1,46 @@
# S02: Related Techniques Cross-Linking — UAT
**Milestone:** M010
**Written:** 2026-03-31T06:19:54.667Z
## UAT: Related Techniques Cross-Linking
### Preconditions
- Chrysopedia stack running on ub01 (API + DB + frontend)
- Database contains technique pages from multiple creators with overlapping topics/tags
- Docker images rebuilt with S02 code (`docker compose build && docker compose up -d`)
### Test 1: Related techniques appear on technique page
1. Navigate to a technique page (e.g., `/techniques/{any-slug}`)
2. Scroll to bottom of page
3. **Expected:** "Related Techniques" section visible with 1-4 cards
4. Each card shows: technique title (clickable link), creator name, category badge, reason text
### Test 2: Card links navigate correctly
1. On a technique page with related techniques visible
2. Click on a related technique card title
3. **Expected:** Navigates to `/techniques/{target_slug}` — the linked technique page loads correctly
### Test 3: Scoring priority — same creator same category ranks highest
1. Find a technique page where the creator has other techniques in the same category
2. Check the related techniques section
3. **Expected:** Techniques from the same creator AND same category appear first (reason shows "Same creator, same topic")
### Test 4: Responsive layout
1. View a technique page on desktop (>600px width)
2. **Expected:** Related cards display in 2-column grid
3. Resize browser to mobile width (<600px)
4. **Expected:** Related cards stack in single column
### Test 5: No related techniques
1. Find or create a technique page with a unique creator AND unique category AND no shared tags with any other technique
2. **Expected:** Related Techniques section either hidden or shows empty state — no broken UI
### Test 6: Graceful field handling
1. If a related technique has empty creator_name or topic_category in the DB
2. **Expected:** Card renders without those fields — no empty badges or "undefined" text
### Edge Cases
- Technique with NULL topic_tags → related section still works (scores on creator/category only)
- Technique page loaded directly by URL (not via navigation) → related techniques still populate
- Only 1-2 possible matches exist → shows only available matches, not padded to 4

View file

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

View file

@ -1,6 +1,42 @@
# S03: Topic Color Coding & Visual Polish
**Goal:** Transform the visual experience from monochrome database to colorful discovery interface
**Goal:** Each of 7 topic categories has a distinct accent color in badges, cards, and sub-topic pages. Page transitions are smooth. Creator avatars are already visually unique (done in prior milestone).
**Demo:** After this: Each of 7 topic categories has a distinct accent color in badges, cards, and sub-topic pages. Page transitions are smooth. Creator avatars are visually unique.
## Tasks
- [x] **T01: Extracted catSlug to shared utility; added category accent border and badge to SubTopicPage, colored category badges to SearchResults cards** — Extract the catSlug helper from TopicsBrowse.tsx into a shared utility file. Apply category accent colors to SubTopicPage (colored left border on page container, category badge on technique cards using badge--cat-{slug} classes). Apply category color badge to SearchResults cards (replace plain text topic_category span with a colored badge). All 7 categories (sound-design, mixing, synthesis, arrangement, workflow, mastering, music-theory) must render with their distinct colors.
Steps:
1. Create `frontend/src/utils/catSlug.ts` exporting the catSlug function. It converts a category name to CSS slug: `name.toLowerCase().replace(/\s+/g, '-')`.
2. Update `frontend/src/pages/TopicsBrowse.tsx` to import catSlug from the shared util instead of defining it locally.
3. Update `frontend/src/pages/SubTopicPage.tsx`:
- Import catSlug from utils
- Add a colored left border to the `.subtopic-page` container using inline style `borderLeftColor: var(--color-badge-cat-{slug}-text)` (same pattern as TopicsBrowse D020)
- Add a category badge span with class `badge badge--cat-{slug}` showing the category name near the title/subtitle area
- On each technique card, add a small category badge if desired (optional — the page-level accent may suffice)
4. Update `frontend/src/pages/SearchResults.tsx`:
- Import catSlug from utils
- In SearchResultCard, replace the plain `<span>{item.topic_category}</span>` with `<span className={`badge badge--cat-${catSlug(item.topic_category)}`}>{item.topic_category}</span>`
5. Add CSS for SubTopicPage color accent in `frontend/src/App.css` — a `.subtopic-page` left border style (4px solid, using CSS var), and any needed spacing adjustments.
6. Verify: `cd frontend && npx tsc --noEmit && npm run build`
- Estimate: 30m
- Files: frontend/src/utils/catSlug.ts, frontend/src/pages/TopicsBrowse.tsx, frontend/src/pages/SubTopicPage.tsx, frontend/src/pages/SearchResults.tsx, frontend/src/App.css
- Verify: cd frontend && npx tsc --noEmit && npm run build
- [ ] **T02: Add CSS page-enter fade-in transitions to all pages** — Add a subtle CSS-only fade-in animation that triggers when page components mount. No libraries needed — a @keyframes animation applied to page wrapper elements.
Steps:
1. In `frontend/src/App.css`, add a `@keyframes pageEnter` animation: opacity 0→1 and translateY(8px→0) over 250ms ease-out.
2. Add a `.page-enter` class that applies this animation.
3. Apply the `.page-enter` class to the outermost wrapper div in each page component:
- `SubTopicPage.tsx` — the `.subtopic-page` div
- `SearchResults.tsx` — the `.search-results-page` div
- `TopicsBrowse.tsx` — its outermost wrapper
- `CreatorsBrowse.tsx` — its outermost wrapper
- `CreatorDetail.tsx` — its outermost wrapper
- `TechniquePage.tsx` — its outermost wrapper
- `Home.tsx` — its outermost wrapper
4. Alternative to adding className to every page: apply the animation directly to existing page container classes in CSS (e.g., `.subtopic-page, .search-results-page, .topics-browse, ... { animation: pageEnter 250ms ease-out; }`). This avoids touching every TSX file. Choose whichever approach is cleaner given the existing class names.
5. Verify: `cd frontend && npx tsc --noEmit && npm run build`
- Estimate: 20m
- Files: frontend/src/App.css, frontend/src/pages/SubTopicPage.tsx, frontend/src/pages/SearchResults.tsx, frontend/src/pages/TopicsBrowse.tsx, frontend/src/pages/CreatorsBrowse.tsx, frontend/src/pages/CreatorDetail.tsx, frontend/src/pages/TechniquePage.tsx, frontend/src/pages/Home.tsx
- Verify: cd frontend && npx tsc --noEmit && npm run build

View file

@ -0,0 +1,70 @@
# S03 Research: Topic Color Coding & Visual Polish
## Depth: Light
Straightforward CSS + React work. All color infrastructure exists (D017, D020). Creator avatars already exist. No new libraries, APIs, or architectural decisions needed.
## Summary
The slice has three goals: (1) consistent topic category color coding across all pages, (2) smooth page transitions, (3) visually unique creator avatars. Investigation reveals **goal 3 is already done** — a sophisticated `CreatorAvatar` component with deterministic hash-based generative SVG waveforms exists and is used in CreatorsBrowse, CreatorDetail, and TechniquePage. The remaining work is CSS color propagation and page transition animation.
## Recommendation
Two tasks: one for color propagation across all surfaces, one for page transitions.
## Implementation Landscape
### What Already Exists
**Category color system (D017, D020):**
- 7 category color pairs as CSS custom properties: `--color-badge-cat-{slug}-bg` and `--color-badge-cat-{slug}-text` for Sound Design, Mixing, Synthesis, Arrangement, Workflow, Mastering, Music Theory
- Badge CSS classes: `.badge--cat-sound-design`, `.badge--cat-mixing`, etc.
- `catSlug()` helper in TopicsBrowse.tsx converts category name → CSS slug
- TopicsBrowse already applies category accent via inline `borderLeftColor` on topic cards (D020 pattern)
**Creator avatars (already complete):**
- `CreatorAvatar` component (`frontend/src/components/CreatorAvatar.tsx`) generates deterministic SVG waveform avatars using FNV-1a hash of creatorId
- Already used in: CreatorsBrowse, CreatorDetail, TechniquePage
- CSS classes: `.creator-avatar`, `.creator-avatar--img`, `.creator-avatar--gen`
- Supports optional `imageUrl` prop with fallback to generated avatar
**App structure:**
- React Router v6 with `<Routes>` in App.tsx, no `<Outlet>` layout wrapper
- No page transitions currently — no keyframes, no fade-in/out, no view transitions
### Where Colors Are NOT Yet Applied
| Surface | Current State | What's Needed |
|---------|--------------|---------------|
| **SubTopicPage** | No category color anywhere | Add colored accent (border or heading tint) using the `category` URL param → `catSlug()` → CSS var |
| **SearchResults cards** | `topic_category` shown as plain text | Apply `badge--cat-{slug}` class to category text |
| **SubTopicPage technique cards** | Plain white cards, no category badge | Add category badge with color class |
| **SubTopicPage breadcrumbs** | No color accent | Optional: tint the category breadcrumb segment |
### Page Transitions Approach
React Router v6 doesn't have built-in transitions. Options:
1. **CSS-only fade-in on route mount** — add a `@keyframes fadeIn` animation to `.app-main` or individual page containers. Simplest, no library needed. Triggers on every mount.
2. **`react-transition-group`** — wraps `<Routes>` for enter/exit animations. Heavier, requires layout changes.
3. **View Transitions API** — browser-native, but limited browser support and requires React 19+ integration.
**Recommendation:** CSS-only fade-in. A subtle 200-300ms opacity+translateY animation on page container mount. No library, no structural changes, works with current React Router setup. Apply via a shared `.page-enter` class or directly on page wrapper elements.
### Files That Need Changes
| File | Change |
|------|--------|
| `frontend/src/App.css` | Add page-enter animation keyframes, apply to page containers |
| `frontend/src/pages/SubTopicPage.tsx` | Add category color accent (border/heading), add category badge to technique cards |
| `frontend/src/pages/SearchResults.tsx` | Apply `badge--cat-{slug}` class to topic_category text in SearchResultCard |
| `frontend/src/App.css` | SubTopicPage color accent styles |
### Constraints
- `catSlug()` helper currently lives in TopicsBrowse.tsx — needs extracting to a shared util or duplicating
- SubTopicPage knows the `category` slug from URL params but needs to derive the display-friendly badge slug (they should match since the URL slug format matches the CSS slug format)
- The 7 category names in canonical_tags.yaml match the CSS property names: workflow, music-theory, sound-design, synthesis, arrangement, mixing, mastering
### No Skills Needed
All work is standard CSS + React. No external libraries needed. The `frontend-design` skill could inform animation choices but this is simple enough to not require it.

View file

@ -0,0 +1,42 @@
---
estimated_steps: 14
estimated_files: 5
skills_used: []
---
# T01: Apply category color coding to SubTopicPage and SearchResults
Extract the catSlug helper from TopicsBrowse.tsx into a shared utility file. Apply category accent colors to SubTopicPage (colored left border on page container, category badge on technique cards using badge--cat-{slug} classes). Apply category color badge to SearchResults cards (replace plain text topic_category span with a colored badge). All 7 categories (sound-design, mixing, synthesis, arrangement, workflow, mastering, music-theory) must render with their distinct colors.
Steps:
1. Create `frontend/src/utils/catSlug.ts` exporting the catSlug function. It converts a category name to CSS slug: `name.toLowerCase().replace(/\s+/g, '-')`.
2. Update `frontend/src/pages/TopicsBrowse.tsx` to import catSlug from the shared util instead of defining it locally.
3. Update `frontend/src/pages/SubTopicPage.tsx`:
- Import catSlug from utils
- Add a colored left border to the `.subtopic-page` container using inline style `borderLeftColor: var(--color-badge-cat-{slug}-text)` (same pattern as TopicsBrowse D020)
- Add a category badge span with class `badge badge--cat-{slug}` showing the category name near the title/subtitle area
- On each technique card, add a small category badge if desired (optional — the page-level accent may suffice)
4. Update `frontend/src/pages/SearchResults.tsx`:
- Import catSlug from utils
- In SearchResultCard, replace the plain `<span>{item.topic_category}</span>` with `<span className={`badge badge--cat-${catSlug(item.topic_category)}`}>{item.topic_category}</span>`
5. Add CSS for SubTopicPage color accent in `frontend/src/App.css` — a `.subtopic-page` left border style (4px solid, using CSS var), and any needed spacing adjustments.
6. Verify: `cd frontend && npx tsc --noEmit && npm run build`
## Inputs
- `frontend/src/pages/TopicsBrowse.tsx`
- `frontend/src/pages/SubTopicPage.tsx`
- `frontend/src/pages/SearchResults.tsx`
- `frontend/src/App.css`
## Expected Output
- `frontend/src/utils/catSlug.ts`
- `frontend/src/pages/TopicsBrowse.tsx`
- `frontend/src/pages/SubTopicPage.tsx`
- `frontend/src/pages/SearchResults.tsx`
- `frontend/src/App.css`
## Verification
cd frontend && npx tsc --noEmit && npm run build

View file

@ -0,0 +1,84 @@
---
id: T01
parent: S03
milestone: M010
provides: []
requires: []
affects: []
key_files: ["frontend/src/utils/catSlug.ts", "frontend/src/pages/TopicsBrowse.tsx", "frontend/src/pages/SubTopicPage.tsx", "frontend/src/pages/SearchResults.tsx", "frontend/src/App.css"]
key_decisions: ["Placed category badge in subtitle row rather than standalone element for clean visual hierarchy", "Created shared utils/ directory pattern for cross-page helpers"]
patterns_established: []
drill_down_paths: []
observability_surfaces: []
duration: ""
verification_result: "TypeScript type-checking (npx tsc --noEmit) and Vite production build (npm run build) both pass with zero errors. 49 modules transformed, output produced at dist/."
completed_at: 2026-03-31T06:25:57.209Z
blocker_discovered: false
---
# T01: Extracted catSlug to shared utility; added category accent border and badge to SubTopicPage, colored category badges to SearchResults cards
> Extracted catSlug to shared utility; added category accent border and badge to SubTopicPage, colored category badges to SearchResults cards
## What Happened
---
id: T01
parent: S03
milestone: M010
key_files:
- frontend/src/utils/catSlug.ts
- frontend/src/pages/TopicsBrowse.tsx
- frontend/src/pages/SubTopicPage.tsx
- frontend/src/pages/SearchResults.tsx
- frontend/src/App.css
key_decisions:
- Placed category badge in subtitle row rather than standalone element for clean visual hierarchy
- Created shared utils/ directory pattern for cross-page helpers
duration: ""
verification_result: passed
completed_at: 2026-03-31T06:25:57.210Z
blocker_discovered: false
---
# T01: Extracted catSlug to shared utility; added category accent border and badge to SubTopicPage, colored category badges to SearchResults cards
**Extracted catSlug to shared utility; added category accent border and badge to SubTopicPage, colored category badges to SearchResults cards**
## What Happened
Extracted the catSlug helper from TopicsBrowse.tsx into frontend/src/utils/catSlug.ts for cross-page reuse. Updated SubTopicPage with a 4px colored left border using category CSS variables and a colored badge in the subtitle area. Updated SearchResults to render topic_category as a colored badge instead of plain text. Added supporting CSS for border and subtitle layout.
## Verification
TypeScript type-checking (npx tsc --noEmit) and Vite production build (npm run build) both pass with zero errors. 49 modules transformed, output produced at dist/.
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 2500ms |
| 2 | `cd frontend && npm run build` | 0 | ✅ pass | 1500ms |
## Deviations
None.
## Known Issues
None.
## Files Created/Modified
- `frontend/src/utils/catSlug.ts`
- `frontend/src/pages/TopicsBrowse.tsx`
- `frontend/src/pages/SubTopicPage.tsx`
- `frontend/src/pages/SearchResults.tsx`
- `frontend/src/App.css`
## Deviations
None.
## Known Issues
None.

View file

@ -0,0 +1,42 @@
---
estimated_steps: 14
estimated_files: 8
skills_used: []
---
# T02: Add CSS page-enter fade-in transitions to all pages
Add a subtle CSS-only fade-in animation that triggers when page components mount. No libraries needed — a @keyframes animation applied to page wrapper elements.
Steps:
1. In `frontend/src/App.css`, add a `@keyframes pageEnter` animation: opacity 0→1 and translateY(8px→0) over 250ms ease-out.
2. Add a `.page-enter` class that applies this animation.
3. Apply the `.page-enter` class to the outermost wrapper div in each page component:
- `SubTopicPage.tsx` — the `.subtopic-page` div
- `SearchResults.tsx` — the `.search-results-page` div
- `TopicsBrowse.tsx` — its outermost wrapper
- `CreatorsBrowse.tsx` — its outermost wrapper
- `CreatorDetail.tsx` — its outermost wrapper
- `TechniquePage.tsx` — its outermost wrapper
- `Home.tsx` — its outermost wrapper
4. Alternative to adding className to every page: apply the animation directly to existing page container classes in CSS (e.g., `.subtopic-page, .search-results-page, .topics-browse, ... { animation: pageEnter 250ms ease-out; }`). This avoids touching every TSX file. Choose whichever approach is cleaner given the existing class names.
5. Verify: `cd frontend && npx tsc --noEmit && npm run build`
## Inputs
- `frontend/src/App.css`
- `frontend/src/pages/SubTopicPage.tsx`
- `frontend/src/pages/SearchResults.tsx`
- `frontend/src/pages/TopicsBrowse.tsx`
- `frontend/src/pages/CreatorsBrowse.tsx`
- `frontend/src/pages/CreatorDetail.tsx`
- `frontend/src/pages/TechniquePage.tsx`
- `frontend/src/pages/Home.tsx`
## Expected Output
- `frontend/src/App.css`
## Verification
cd frontend && npx tsc --noEmit && npm run build

View file

@ -2334,6 +2334,8 @@ a.app-footer__repo:hover {
max-width: 56rem;
margin: 0 auto;
padding: 1rem 0;
border-left: 4px solid transparent;
padding-left: 1.25rem;
}
.subtopic-page__title {
@ -2347,6 +2349,13 @@ a.app-footer__repo:hover {
font-size: 0.95rem;
color: var(--color-text-secondary);
margin: 0 0 2rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.subtopic-page__subtitle-sep {
color: var(--color-text-muted);
}
.subtopic-groups {

View file

@ -9,6 +9,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Link, useSearchParams, useNavigate } from "react-router-dom";
import { searchApi, type SearchResultItem } from "../api/public-client";
import { catSlug } from "../utils/catSlug";
export default function SearchResults() {
const [searchParams] = useSearchParams();
@ -167,7 +168,7 @@ function SearchResultCard({ item }: { item: SearchResultItem }) {
{item.topic_category && (
<>
<span className="queue-card__separator">·</span>
<span>{item.topic_category}</span>
<span className={`badge badge--cat-${catSlug(item.topic_category)}`}>{item.topic_category}</span>
</>
)}
{item.topic_tags.length > 0 && (

View file

@ -11,6 +11,7 @@ import {
fetchSubTopicTechniques,
type TechniqueListItem,
} from "../api/public-client";
import { catSlug } from "../utils/catSlug";
/** Convert a URL slug to a display name: replace hyphens with spaces, title-case. */
function slugToDisplayName(slug: string): string {
@ -91,9 +92,13 @@ export default function SubTopicPage() {
}
const groups = groupByCreator(techniques);
const slug = category ? catSlug(categoryDisplay) : "";
return (
<div className="subtopic-page">
<div
className="subtopic-page"
style={slug ? { borderLeftColor: `var(--color-badge-cat-${slug}-text)` } : undefined}
>
{/* Breadcrumbs */}
<nav className="breadcrumbs" aria-label="Breadcrumb">
<Link to="/topics" className="breadcrumbs__link">Topics</Link>
@ -105,7 +110,9 @@ export default function SubTopicPage() {
<h2 className="subtopic-page__title">{subtopicDisplay}</h2>
<p className="subtopic-page__subtitle">
{techniques.length} technique{techniques.length !== 1 ? "s" : ""} in {categoryDisplay}
<span className={`badge badge--cat-${slug}`}>{categoryDisplay}</span>
<span className="subtopic-page__subtitle-sep">·</span>
{techniques.length} technique{techniques.length !== 1 ? "s" : ""}
</p>
{techniques.length === 0 ? (

View file

@ -13,11 +13,7 @@ import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { fetchTopics, type TopicCategory } from "../api/public-client";
import { CATEGORY_ICON } from "../components/CategoryIcons";
/** Derive the badge CSS slug from a category name. */
function catSlug(name: string): string {
return name.toLowerCase().replace(/\s+/g, "-");
}
import { catSlug } from "../utils/catSlug";

View file

@ -0,0 +1,4 @@
/** Derive the badge CSS slug from a category name. */
export function catSlug(name: string): string {
return name.toLowerCase().replace(/\s+/g, "-");
}

View file

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx"],"version":"5.6.3"}
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts"],"version":"5.6.3"}