diff --git a/.gsd/milestones/M006/slices/S05/S05-PLAN.md b/.gsd/milestones/M006/slices/S05/S05-PLAN.md index 6d42df7..dc016e9 100644 --- a/.gsd/milestones/M006/slices/S05/S05-PLAN.md +++ b/.gsd/milestones/M006/slices/S05/S05-PLAN.md @@ -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 diff --git a/.gsd/milestones/M006/slices/S05/tasks/T01-VERIFY.json b/.gsd/milestones/M006/slices/S05/tasks/T01-VERIFY.json new file mode 100644 index 0000000..aadd43f --- /dev/null +++ b/.gsd/milestones/M006/slices/S05/tasks/T01-VERIFY.json @@ -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" + } + ] +} diff --git a/.gsd/milestones/M006/slices/S05/tasks/T02-SUMMARY.md b/.gsd/milestones/M006/slices/S05/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..150eba3 --- /dev/null +++ b/.gsd/milestones/M006/slices/S05/tasks/T02-SUMMARY.md @@ -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. diff --git a/frontend/src/App.css b/frontend/src/App.css index da9c9d2..520b1cc 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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; } } diff --git a/frontend/src/pages/TopicsBrowse.tsx b/frontend/src/pages/TopicsBrowse.tsx index a6addb0..501755a 100644 --- a/frontend/src/pages/TopicsBrowse.tsx +++ b/frontend/src/pages/TopicsBrowse.tsx @@ -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([]); 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 ? (
- No topics matching "{filter}" + No topics matching “{filter}”
) : ( -
- {filtered.map((cat) => ( -
- +
+ {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) && ( -
- {cat.sub_topics.map((st) => ( - - {st.name} - - - {st.technique_count} technique{st.technique_count !== 1 ? "s" : ""} - - · - - {st.creator_count} creator{st.creator_count !== 1 ? "s" : ""} - - - - ))} + return ( +
+
+

+ + {cat.name} +

+

{cat.description}

+
+ {cat.sub_topics.length} sub-topic{cat.sub_topics.length !== 1 ? "s" : ""} + · + {totalTechniques} technique{totalTechniques !== 1 ? "s" : ""} +
+
- )} -
- ))} + + {isExpanded && ( +
+ {cat.sub_topics.map((st) => ( + + {st.name} + + + {st.technique_count} technique{st.technique_count !== 1 ? "s" : ""} + + · + + {st.creator_count} creator{st.creator_count !== 1 ? "s" : ""} + + + + ))} +
+ )} +
+ ); + })}
)}