feat: Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-…
- "frontend/src/pages/TopicsBrowse.tsx" - "frontend/src/App.css" GSD-Task: S05/T02
This commit is contained in:
parent
3f3fe065f8
commit
75332343cb
5 changed files with 261 additions and 97 deletions
|
|
@ -15,7 +15,7 @@ Steps:
|
|||
- Estimate: 15m
|
||||
- Files: config/canonical_tags.yaml (on ub01 via SSH), frontend/src/App.css
|
||||
- Verify: curl -s http://ub01:8096/api/v1/topics | python3 -c "import sys,json; d=json.load(sys.stdin); assert len(d)==7, f'Expected 7 categories, got {len(d)}'; names=[c['name'] for c in d]; assert 'Music Theory' in names, f'Music Theory not found in {names}'; print('PASS: 7 categories including Music Theory')" && cd frontend && npx grep-cli --no-error 'music-theory' src/App.css || grep -q 'music-theory' frontend/src/App.css && echo 'PASS: badge CSS present'
|
||||
- [ ] **T02: Redesign TopicsBrowse.tsx to card grid layout with updated CSS** — Rewrite `TopicsBrowse.tsx` from the current vertical accordion list to a responsive card grid layout, and replace the topics CSS section in `App.css` with card-based styles.
|
||||
- [x] **T02: Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles** — Rewrite `TopicsBrowse.tsx` from the current vertical accordion list to a responsive card grid layout, and replace the topics CSS section in `App.css` with card-based styles.
|
||||
|
||||
The card layout should:
|
||||
- Use CSS grid: 2 columns on desktop (min-width > 768px), 1 column on mobile
|
||||
|
|
|
|||
22
.gsd/milestones/M006/slices/S05/tasks/T01-VERIFY.json
Normal file
22
.gsd/milestones/M006/slices/S05/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T01",
|
||||
"unitId": "M006/S05/T01",
|
||||
"timestamp": 1774871058689,
|
||||
"passed": true,
|
||||
"discoverySource": "task-plan",
|
||||
"checks": [
|
||||
{
|
||||
"command": "cd frontend",
|
||||
"exitCode": 0,
|
||||
"durationMs": 6,
|
||||
"verdict": "pass"
|
||||
},
|
||||
{
|
||||
"command": "echo 'PASS: badge CSS present'",
|
||||
"exitCode": 0,
|
||||
"durationMs": 6,
|
||||
"verdict": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
81
.gsd/milestones/M006/slices/S05/tasks/T02-SUMMARY.md
Normal file
81
.gsd/milestones/M006/slices/S05/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
id: T02
|
||||
parent: S05
|
||||
milestone: M006
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["frontend/src/pages/TopicsBrowse.tsx", "frontend/src/App.css"]
|
||||
key_decisions: ["Used 3px colored left border + small colored dot for category visual differentiation — subtler than full colored header, maintains dark theme cohesion", "Computed totalTechniques client-side via sub_topics.reduce() — no API changes needed"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "Frontend build passes (800ms, no errors). Live CSS and JS bundles contain topic-card class references. API returns 7 categories. Browser verification confirms 2-column desktop grid, 1-column mobile, filter narrows to matching cards, expand/collapse toggles work correctly."
|
||||
completed_at: 2026-03-30T11:48:43.282Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles
|
||||
|
||||
> Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T02
|
||||
parent: S05
|
||||
milestone: M006
|
||||
key_files:
|
||||
- frontend/src/pages/TopicsBrowse.tsx
|
||||
- frontend/src/App.css
|
||||
key_decisions:
|
||||
- Used 3px colored left border + small colored dot for category visual differentiation — subtler than full colored header, maintains dark theme cohesion
|
||||
- Computed totalTechniques client-side via sub_topics.reduce() — no API changes needed
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-30T11:48:43.283Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles
|
||||
|
||||
**Rewrote TopicsBrowse.tsx from vertical accordion to responsive 2-column card grid with category color accents, summary stats, and expand/collapse toggles**
|
||||
|
||||
## What Happened
|
||||
|
||||
Replaced the single-column accordion layout with a CSS grid card layout. Each card shows a colored left border and dot (via existing badge custom properties), category name, description, sub-topic + technique count stats line, and expand/collapse toggle. Grid is 2 columns on desktop, 1 on mobile. CSS rewritten from .topics-list/.topic-category* to .topics-grid/.topic-card* classes. All existing functionality preserved (filter, expand/collapse, sub-topic navigation). Deployed to ub01 and verified in browser at both desktop and mobile viewports.
|
||||
|
||||
## Verification
|
||||
|
||||
Frontend build passes (800ms, no errors). Live CSS and JS bundles contain topic-card class references. API returns 7 categories. Browser verification confirms 2-column desktop grid, 1-column mobile, filter narrows to matching cards, expand/collapse toggles work correctly.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 800ms |
|
||||
| 2 | `curl CSS bundle | grep topic-card` | 0 | ✅ pass | 200ms |
|
||||
| 3 | `curl JS bundle | grep topic-card` | 0 | ✅ pass | 200ms |
|
||||
| 4 | `curl API /topics | assert len==7` | 0 | ✅ pass | 300ms |
|
||||
| 5 | `Browser: desktop 2-col grid + mobile 1-col + filter + toggle` | 0 | ✅ pass | 5000ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
Plan verification command used grep on HTML for topic-category classes, but SPA serves class names in JS bundles — adapted to check bundle contents instead.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/src/pages/TopicsBrowse.tsx`
|
||||
- `frontend/src/App.css`
|
||||
|
||||
|
||||
## Deviations
|
||||
Plan verification command used grep on HTML for topic-category classes, but SPA serves class names in JS bundles — adapted to check bundle contents instead.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
|
|
@ -1839,11 +1839,11 @@ body {
|
|||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════════
|
||||
TOPICS BROWSE
|
||||
TOPICS BROWSE — Card Grid Layout
|
||||
══════════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.topics-browse {
|
||||
max-width: 56rem;
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.topics-browse__title {
|
||||
|
|
@ -1869,7 +1869,7 @@ body {
|
|||
font-family: inherit;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-input);
|
||||
margin-bottom: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
|
|
@ -1879,67 +1879,92 @@ body {
|
|||
box-shadow: 0 0 0 2px var(--color-accent-focus);
|
||||
}
|
||||
|
||||
/* ── Topics hierarchy ─────────────────────────────────────────────────────── */
|
||||
/* ── Card grid ────────────────────────────────────────────────────────────── */
|
||||
|
||||
.topics-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
.topics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.topic-category {
|
||||
.topic-card {
|
||||
background: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-left: 3px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px var(--color-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topic-category__header {
|
||||
.topic-card__body {
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.topic-card__name {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.25rem;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topic-card__dot {
|
||||
display: inline-block;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topic-card__desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topic-card__stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.topic-card__stats-sep {
|
||||
color: var(--color-border);
|
||||
}
|
||||
|
||||
.topic-card__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.375rem;
|
||||
padding: 0.25rem 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.topic-category__header:hover {
|
||||
background: var(--color-bg-surface-hover);
|
||||
}
|
||||
|
||||
.topic-category__chevron {
|
||||
font-size: 0.625rem;
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0;
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.topic-category__name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
.topic-card__toggle:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.topic-category__desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.topic-category__count {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ── Sub-topics ───────────────────────────────────────────────────────────── */
|
||||
/* ── Sub-topics inside card ───────────────────────────────────────────────── */
|
||||
|
||||
.topic-subtopics {
|
||||
border-top: 1px solid var(--color-border);
|
||||
|
|
@ -1949,10 +1974,10 @@ body {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.625rem 1.25rem 0.625rem 2.75rem;
|
||||
padding: 0.5rem 1.25rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
|
|
@ -1974,7 +1999,7 @@ body {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
|
|
@ -2037,12 +2062,16 @@ body {
|
|||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.topic-category__desc {
|
||||
.topics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.topic-card__desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topic-subtopic {
|
||||
padding-left: 2rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
/**
|
||||
* Topics browse page (R008).
|
||||
*
|
||||
* Two-level hierarchy: 6 top-level categories with expandable/collapsible
|
||||
* sub-topics. Each sub-topic shows technique_count and creator_count.
|
||||
* Filter input narrows categories and sub-topics.
|
||||
* Click sub-topic → search results filtered to that topic.
|
||||
* Responsive card grid layout with 7 top-level categories.
|
||||
* Each card shows: category name, description, summary stats
|
||||
* (sub-topic count + total technique count), and an expand/collapse
|
||||
* toggle revealing sub-topics as links to search.
|
||||
*
|
||||
* Filter input narrows visible cards by category name + sub-topic names.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { fetchTopics, type TopicCategory } from "../api/public-client";
|
||||
|
||||
/** Derive the badge CSS slug from a category name. */
|
||||
function catSlug(name: string): string {
|
||||
return name.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
export default function TopicsBrowse() {
|
||||
const [categories, setCategories] = useState<TopicCategory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -62,7 +69,7 @@ export default function TopicsBrowse() {
|
|||
// Apply filter: show categories whose name or sub-topics match
|
||||
const lowerFilter = filter.toLowerCase();
|
||||
const filtered = filter
|
||||
? categories
|
||||
? (categories
|
||||
.map((cat) => {
|
||||
const catMatches = cat.name.toLowerCase().includes(lowerFilter);
|
||||
const matchingSubs = cat.sub_topics.filter((st) =>
|
||||
|
|
@ -74,7 +81,7 @@ export default function TopicsBrowse() {
|
|||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as TopicCategory[]
|
||||
.filter(Boolean) as TopicCategory[])
|
||||
: categories;
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -104,51 +111,76 @@ export default function TopicsBrowse() {
|
|||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
No topics matching "{filter}"
|
||||
No topics matching “{filter}”
|
||||
</div>
|
||||
) : (
|
||||
<div className="topics-list">
|
||||
{filtered.map((cat) => (
|
||||
<div key={cat.name} className="topic-category">
|
||||
<button
|
||||
className="topic-category__header"
|
||||
onClick={() => toggleCategory(cat.name)}
|
||||
aria-expanded={expanded.has(cat.name)}
|
||||
>
|
||||
<span className="topic-category__chevron">
|
||||
{expanded.has(cat.name) ? "▼" : "▶"}
|
||||
</span>
|
||||
<span className="topic-category__name">{cat.name}</span>
|
||||
<span className="topic-category__desc">{cat.description}</span>
|
||||
<span className="topic-category__count">
|
||||
{cat.sub_topics.length} sub-topic{cat.sub_topics.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</button>
|
||||
<div className="topics-grid">
|
||||
{filtered.map((cat) => {
|
||||
const slug = catSlug(cat.name);
|
||||
const isExpanded = expanded.has(cat.name);
|
||||
const totalTechniques = cat.sub_topics.reduce(
|
||||
(sum, st) => sum + st.technique_count,
|
||||
0,
|
||||
);
|
||||
|
||||
{expanded.has(cat.name) && (
|
||||
<div className="topic-subtopics">
|
||||
{cat.sub_topics.map((st) => (
|
||||
<Link
|
||||
key={st.name}
|
||||
to={`/search?q=${encodeURIComponent(st.name)}&scope=topics`}
|
||||
className="topic-subtopic"
|
||||
>
|
||||
<span className="topic-subtopic__name">{st.name}</span>
|
||||
<span className="topic-subtopic__counts">
|
||||
<span className="topic-subtopic__count">
|
||||
{st.technique_count} technique{st.technique_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<span className="topic-subtopic__separator">·</span>
|
||||
<span className="topic-subtopic__count">
|
||||
{st.creator_count} creator{st.creator_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
return (
|
||||
<div
|
||||
key={cat.name}
|
||||
className="topic-card"
|
||||
style={{
|
||||
borderLeftColor: `var(--color-badge-cat-${slug}-text)`,
|
||||
}}
|
||||
>
|
||||
<div className="topic-card__body">
|
||||
<h3 className="topic-card__name">
|
||||
<span
|
||||
className="topic-card__dot"
|
||||
style={{
|
||||
background: `var(--color-badge-cat-${slug}-text)`,
|
||||
}}
|
||||
/>
|
||||
{cat.name}
|
||||
</h3>
|
||||
<p className="topic-card__desc">{cat.description}</p>
|
||||
<div className="topic-card__stats">
|
||||
<span>{cat.sub_topics.length} sub-topic{cat.sub_topics.length !== 1 ? "s" : ""}</span>
|
||||
<span className="topic-card__stats-sep">·</span>
|
||||
<span>{totalTechniques} technique{totalTechniques !== 1 ? "s" : ""}</span>
|
||||
</div>
|
||||
<button
|
||||
className="topic-card__toggle"
|
||||
onClick={() => toggleCategory(cat.name)}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
{isExpanded ? "Hide sub-topics ▲" : "Show sub-topics ▼"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isExpanded && (
|
||||
<div className="topic-subtopics">
|
||||
{cat.sub_topics.map((st) => (
|
||||
<Link
|
||||
key={st.name}
|
||||
to={`/search?q=${encodeURIComponent(st.name)}&scope=topics`}
|
||||
className="topic-subtopic"
|
||||
>
|
||||
<span className="topic-subtopic__name">{st.name}</span>
|
||||
<span className="topic-subtopic__counts">
|
||||
<span className="topic-subtopic__count">
|
||||
{st.technique_count} technique{st.technique_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<span className="topic-subtopic__separator">·</span>
|
||||
<span className="topic-subtopic__count">
|
||||
{st.creator_count} creator{st.creator_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue