From adc86446f102eb8be1cf1bbbd60c6493ad2641a4 Mon Sep 17 00:00:00 2001 From: jlightner Date: Tue, 31 Mar 2026 08:35:07 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Created=20shared=20TagList=20component?= =?UTF-8?q?=20with=20max-4=20overflow,=20applied=20acr=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/components/TagList.tsx" - "frontend/src/pages/Home.tsx" - "frontend/src/pages/SearchResults.tsx" - "frontend/src/pages/SubTopicPage.tsx" - "frontend/src/pages/CreatorDetail.tsx" - "frontend/src/pages/TopicsBrowse.tsx" - "frontend/src/App.css" GSD-Task: S02/T03 --- .gsd/milestones/M011/slices/S02/S02-PLAN.md | 2 +- .../M011/slices/S02/tasks/T02-VERIFY.json | 30 +++++++ .../M011/slices/S02/tasks/T03-SUMMARY.md | 87 +++++++++++++++++++ frontend/src/App.css | 22 +++++ frontend/src/components/TagList.tsx | 33 +++++++ frontend/src/pages/CreatorDetail.tsx | 7 +- frontend/src/pages/Home.tsx | 13 +-- frontend/src/pages/SearchResults.tsx | 7 +- frontend/src/pages/SubTopicPage.tsx | 5 +- frontend/src/pages/TopicsBrowse.tsx | 13 +++ frontend/tsconfig.app.tsbuildinfo | 2 +- 11 files changed, 200 insertions(+), 21 deletions(-) create mode 100644 .gsd/milestones/M011/slices/S02/tasks/T02-VERIFY.json create mode 100644 .gsd/milestones/M011/slices/S02/tasks/T03-SUMMARY.md create mode 100644 frontend/src/components/TagList.tsx diff --git a/.gsd/milestones/M011/slices/S02/S02-PLAN.md b/.gsd/milestones/M011/slices/S02/S02-PLAN.md index 7ba660b..55e2865 100644 --- a/.gsd/milestones/M011/slices/S02/S02-PLAN.md +++ b/.gsd/milestones/M011/slices/S02/S02-PLAN.md @@ -57,7 +57,7 @@ - Estimate: 20m - Files: frontend/src/pages/CreatorDetail.tsx, frontend/src/App.css - Verify: cd frontend && npx tsc --noEmit && npm run build -- [ ] **T03: TagList component, tag overflow limit, and empty subtopic handling** — Create a shared TagList component for tag overflow (R027), apply it across all 5 tag-rendering sites, and add empty subtopic handling in TopicsBrowse (R028). +- [x] **T03: Created shared TagList component with max-4 overflow, applied across all 5 tag sites, and added empty subtopic Coming soon badge** — Create a shared TagList component for tag overflow (R027), apply it across all 5 tag-rendering sites, and add empty subtopic handling in TopicsBrowse (R028). ## Steps diff --git a/.gsd/milestones/M011/slices/S02/tasks/T02-VERIFY.json b/.gsd/milestones/M011/slices/S02/tasks/T02-VERIFY.json new file mode 100644 index 0000000..2716151 --- /dev/null +++ b/.gsd/milestones/M011/slices/S02/tasks/T02-VERIFY.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M011/S02/T02", + "timestamp": 1774945929488, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd frontend", + "exitCode": 0, + "durationMs": 7, + "verdict": "pass" + }, + { + "command": "npx tsc --noEmit", + "exitCode": 1, + "durationMs": 762, + "verdict": "fail" + }, + { + "command": "npm run build", + "exitCode": 254, + "durationMs": 89, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M011/slices/S02/tasks/T03-SUMMARY.md b/.gsd/milestones/M011/slices/S02/tasks/T03-SUMMARY.md new file mode 100644 index 0000000..2b18064 --- /dev/null +++ b/.gsd/milestones/M011/slices/S02/tasks/T03-SUMMARY.md @@ -0,0 +1,87 @@ +--- +id: T03 +parent: S02 +milestone: M011 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/components/TagList.tsx", "frontend/src/pages/Home.tsx", "frontend/src/pages/SearchResults.tsx", "frontend/src/pages/SubTopicPage.tsx", "frontend/src/pages/CreatorDetail.tsx", "frontend/src/pages/TopicsBrowse.tsx", "frontend/src/App.css"] +key_decisions: ["Added pillClass prop to TagList so Home.tsx can pass pill--tag while other pages use bare pill class"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "TypeScript compilation (tsc --noEmit) passed with zero errors. Production build (npm run build) succeeded — 51 modules transformed, built in 827ms." +completed_at: 2026-03-31T08:34:55.792Z +blocker_discovered: false +--- + +# T03: Created shared TagList component with max-4 overflow, applied across all 5 tag sites, and added empty subtopic Coming soon badge + +> Created shared TagList component with max-4 overflow, applied across all 5 tag sites, and added empty subtopic Coming soon badge + +## What Happened +--- +id: T03 +parent: S02 +milestone: M011 +key_files: + - frontend/src/components/TagList.tsx + - frontend/src/pages/Home.tsx + - frontend/src/pages/SearchResults.tsx + - frontend/src/pages/SubTopicPage.tsx + - frontend/src/pages/CreatorDetail.tsx + - frontend/src/pages/TopicsBrowse.tsx + - frontend/src/App.css +key_decisions: + - Added pillClass prop to TagList so Home.tsx can pass pill--tag while other pages use bare pill class +duration: "" +verification_result: passed +completed_at: 2026-03-31T08:34:55.792Z +blocker_discovered: false +--- + +# T03: Created shared TagList component with max-4 overflow, applied across all 5 tag sites, and added empty subtopic Coming soon badge + +**Created shared TagList component with max-4 overflow, applied across all 5 tag sites, and added empty subtopic Coming soon badge** + +## What Happened + +Created TagList component with configurable max and pillClass props. Replaced all 5 inline topic_tags.map() calls across Home, SearchResults, SubTopicPage, CreatorDetail with the shared component. Added empty subtopic handling in TopicsBrowse — technique_count === 0 renders a non-clickable span with Coming soon pill instead of a Link. Added pill--overflow, pill--coming-soon, and topic-subtopic--empty CSS. + +## Verification + +TypeScript compilation (tsc --noEmit) passed with zero errors. Production build (npm run build) succeeded — 51 modules transformed, built in 827ms. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 3600ms | +| 2 | `cd frontend && npm run build` | 0 | ✅ pass | 2900ms | + + +## Deviations + +Added pillClass prop to TagList (not in plan) to preserve existing pill--tag class used in Home.tsx. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/components/TagList.tsx` +- `frontend/src/pages/Home.tsx` +- `frontend/src/pages/SearchResults.tsx` +- `frontend/src/pages/SubTopicPage.tsx` +- `frontend/src/pages/CreatorDetail.tsx` +- `frontend/src/pages/TopicsBrowse.tsx` +- `frontend/src/App.css` + + +## Deviations +Added pillClass prop to TagList (not in plan) to preserve existing pill--tag class used in Home.tsx. + +## Known Issues +None. diff --git a/frontend/src/App.css b/frontend/src/App.css index c8bceb0..3ca4eb4 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1456,6 +1456,19 @@ a.app-footer__repo:hover { color: var(--color-pill-plugin-text); } +.pill--overflow { + background: var(--color-surface-2); + color: var(--color-text-secondary); + font-style: italic; +} + +.pill--coming-soon { + font-size: 0.65rem; + background: var(--color-surface-2); + color: var(--color-text-secondary); + font-style: italic; +} + .pill-list { display: flex; flex-wrap: wrap; @@ -2353,6 +2366,15 @@ a.app-footer__repo:hover { background: var(--color-bg-surface-hover); } +.topic-subtopic--empty { + opacity: 0.5; + cursor: default; +} + +.topic-subtopic--empty:hover { + background: transparent; +} + .topic-subtopic + .topic-subtopic { border-top: 1px solid var(--color-bg-surface-hover); } diff --git a/frontend/src/components/TagList.tsx b/frontend/src/components/TagList.tsx new file mode 100644 index 0000000..b83121b --- /dev/null +++ b/frontend/src/components/TagList.tsx @@ -0,0 +1,33 @@ +/** + * Shared tag list with overflow indicator. + * + * Renders up to `max` tag pills (default 4), plus a "+N more" pill + * when the list exceeds the limit. Used across cards and detail pages + * to keep tag displays compact and consistent (R027). + */ + +interface TagListProps { + tags: string[]; + max?: number; + /** Extra CSS class added to each pill (e.g. "pill--tag") */ + pillClass?: string; +} + +export default function TagList({ tags, max = 4, pillClass }: TagListProps) { + const visible = tags.slice(0, max); + const overflow = tags.length - max; + const cls = pillClass ? `pill ${pillClass}` : "pill"; + + return ( + <> + {visible.map((tag) => ( + + {tag} + + ))} + {overflow > 0 && ( + +{overflow} more + )} + + ); +} diff --git a/frontend/src/pages/CreatorDetail.tsx b/frontend/src/pages/CreatorDetail.tsx index 98acd53..ced4bca 100644 --- a/frontend/src/pages/CreatorDetail.tsx +++ b/frontend/src/pages/CreatorDetail.tsx @@ -15,6 +15,7 @@ import { } from "../api/public-client"; import CreatorAvatar from "../components/CreatorAvatar"; import { catSlug } from "../utils/catSlug"; +import TagList from "../components/TagList"; export default function CreatorDetail() { const { slug } = useParams<{ slug: string }>(); @@ -156,11 +157,7 @@ export default function CreatorDetail() { {t.topic_tags && t.topic_tags.length > 0 && ( - {t.topic_tags.map((tag) => ( - - {tag} - - ))} + )} diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 0f6e642..319f3a7 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -7,6 +7,7 @@ import { IconTopics, IconCreators } from "../components/CategoryIcons"; import SearchAutocomplete from "../components/SearchAutocomplete"; +import TagList from "../components/TagList"; import { useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { @@ -198,9 +199,9 @@ export default function Home() { {featured.topic_category && ( {featured.topic_category} )} - {featured.topic_tags && featured.topic_tags.length > 0 && featured.topic_tags.map(tag => ( - {tag} - ))} + {featured.topic_tags && featured.topic_tags.length > 0 && ( + + )} {featured.key_moment_count > 0 && ( {featured.key_moment_count} moment{featured.key_moment_count !== 1 ? "s" : ""} @@ -244,9 +245,9 @@ export default function Home() { {t.topic_category} - {t.topic_tags && t.topic_tags.length > 0 && t.topic_tags.map(tag => ( - {tag} - ))} + {t.topic_tags && t.topic_tags.length > 0 && ( + + )} {t.summary && ( {t.summary.length > 150 diff --git a/frontend/src/pages/SearchResults.tsx b/frontend/src/pages/SearchResults.tsx index 85ff179..27777d1 100644 --- a/frontend/src/pages/SearchResults.tsx +++ b/frontend/src/pages/SearchResults.tsx @@ -11,6 +11,7 @@ import { Link, useSearchParams, useNavigate } from "react-router-dom"; import { searchApi, type SearchResultItem } from "../api/public-client"; import { catSlug } from "../utils/catSlug"; import SearchAutocomplete from "../components/SearchAutocomplete"; +import TagList from "../components/TagList"; export default function SearchResults() { const [searchParams] = useSearchParams(); @@ -142,11 +143,7 @@ function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; stag )} {item.topic_tags.length > 0 && ( - {item.topic_tags.map((tag) => ( - - {tag} - - ))} + )} diff --git a/frontend/src/pages/SubTopicPage.tsx b/frontend/src/pages/SubTopicPage.tsx index c4667b9..81f4cae 100644 --- a/frontend/src/pages/SubTopicPage.tsx +++ b/frontend/src/pages/SubTopicPage.tsx @@ -12,6 +12,7 @@ import { type TechniqueListItem, } from "../api/public-client"; import { catSlug } from "../utils/catSlug"; +import TagList from "../components/TagList"; /** Convert a URL slug to a display name: replace hyphens with spaces, title-case. */ function slugToDisplayName(slug: string): string { @@ -146,9 +147,7 @@ export default function SubTopicPage() { {t.title} {t.topic_tags && t.topic_tags.length > 0 && ( - {t.topic_tags.map((tag) => ( - {tag} - ))} + )} {t.summary && ( diff --git a/frontend/src/pages/TopicsBrowse.tsx b/frontend/src/pages/TopicsBrowse.tsx index 97a5ca1..8543df8 100644 --- a/frontend/src/pages/TopicsBrowse.tsx +++ b/frontend/src/pages/TopicsBrowse.tsx @@ -155,6 +155,19 @@ export default function TopicsBrowse() {
{cat.sub_topics.map((st) => { const stSlug = st.name.toLowerCase().replace(/\s+/g, "-"); + if (st.technique_count === 0) { + return ( + + {st.name} + + Coming soon + + + ); + } return (