From ab02d5aec812d108c6ebf284d1ffa14c290c2a6c Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Tue, 5 May 2026 19:17:30 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20faceted=20search=20for=20the=20Store=20?= =?UTF-8?q?=E2=80=94=20filter=20by=20topic,=20language,=20author,=20licens?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search the entire fledge ecosystem by what plugins actually are, not just by name. The Store page now exposes a left filter rail backed by counts computed from the result set, plus inline pivot from any card. Backend - Replace string-only `query` with a `BrowseFilters` shape (q, language, topics[], owner, license). - New `buildQualifiers()` composes a GitHub search string from filters, drops reserved markers (fledge-plugin / fledge-template / fledge) so users can't override the base topic, and quotes language values that contain whitespace. - New `computeFacets()` aggregates topics / languages / owners / licenses across the result set, deduped per repo, sorted by count. - `/api/github/browse` accepts q, language, owner, license, and repeatable topic params; returns `{ items, total, facets, applied }`. - `per_page` bumped from 30 to 50 for richer facets. Frontend - New filter rail (sticky on desktop, stacked on mobile) for Topic / Language / Author / License, each row showing live counts and a "Show N more" expander. - Removable active-filter chips above the grid; "Clear all" wipes filters in one click. - Topic tags on cards are buttons (toggle that topic). Language and author on each card are pivot links to filter by that value. - Search box is debounced (300 ms), Enter forces immediate, Esc clears. - Result count rendered top-right. Tests - 12 new unit tests in `tests/github.test.ts` covering qualifier composition (empty, multi-topic, reserved-topic dropping, whitespace quoting) and facet aggregation (sorting, per-repo dedupe, NOASSERTION skip, empty input). Co-Authored-By: Claude Opus 4.7 (1M context) --- public/app.js | 254 ++++++++++++++++++++++++++++++++++++++++--- public/index.html | 22 +++- public/style.css | 250 +++++++++++++++++++++++++++++++++++++++++- src/api.ts | 37 ++++++- src/github.ts | 115 +++++++++++++++++--- tests/github.test.ts | 153 ++++++++++++++++++++++++++ 6 files changed, 789 insertions(+), 42 deletions(-) create mode 100644 tests/github.test.ts diff --git a/public/app.js b/public/app.js index a1917d2..ce41f11 100644 --- a/public/app.js +++ b/public/app.js @@ -278,7 +278,7 @@ function statusBadge(status) { function renderStoreCard(repo) { const owner = escape(repo.owner?.login || ""); const isOfficial = (repo.owner?.login || "").toLowerCase() === "corvidlabs"; - const topics = (repo.topics || []).slice(0, 5); + const topics = (repo.topics || []).filter((t) => t !== "fledge-plugin" && t !== "fledge-template" && t !== "fledge").slice(0, 5); const installed = isInstalled(repo.full_name); const fullEsc = escape(repo.full_name || ""); const installedPlugin = installedBySource.get((repo.full_name || "").toLowerCase()); @@ -290,6 +290,19 @@ function renderStoreCard(repo) { : ` `; + const lang = repo.language || ""; + const topicTagsHtml = topics.map((t) => { + const active = storeFilters.topics.has(t.toLowerCase()); + return ``; + }).join(""); + + const langStat = lang + ? `` + : ""; + const ownerLink = repo.owner?.login + ? `` + : owner; + return `
@@ -299,7 +312,7 @@ function renderStoreCard(repo) { ${escape(repo.name)}
- ${owner} + ${ownerLink} ${isOfficial ? 'official' : ""} ${installed ? 'installed' : ""}
@@ -308,12 +321,12 @@ function renderStoreCard(repo) {
${escape(repo.description || "No description")}
${typeBadge(repo.topics)} - ${topics.filter((t) => t !== "fledge-plugin" && t !== "fledge-template").map((t) => `${escape(t)}`).join("")} + ${topicTagsHtml}