Skip to content

feat: faceted search for the Store — topic, language, author, license - #6

Merged
0xLeif merged 1 commit into
mainfrom
feat/store-faceted-search
May 6, 2026
Merged

0xLeif merged 1 commit into
mainfrom
feat/store-faceted-search

Conversation

@0xLeif

@0xLeif 0xLeif commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Make it easy to discover plugins/templates by what they actually are, not just by name.

The Store page now has a left filter rail of topics, languages, authors, and licenses with live counts computed from the current result set, plus inline pivots — click a topic tag, language stat, or author name on any card to filter by it instantly.

Backend (src/github.ts, src/api.ts)

  • New BrowseFilters shape (q, language, topics[], owner, license) replacing the string-only query argument.
  • buildQualifiers() composes a GitHub search string from filters, drops the reserved base markers (fledge-plugin / fledge-template / fledge) so users can't override them, and quotes languages with whitespace.
  • computeFacets() aggregates topics / languages / owners / licenses across the result set — deduped per repo, sorted by count, NOASSERTION licenses skipped.
  • /api/github/browse accepts q, language, owner, license, and repeatable topic query params; responds with { items, total, facets, applied }.
  • per_page bumped from 30 → 50 for richer facets.

Frontend (public/)

  • Sticky left filter rail (Topic / Language / Author / License) with counts and a "Show N more" expander per group.
  • 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 pivot to that filter.
  • Search input is debounced (300 ms); Enter forces immediate run, Esc clears.
  • Result count rendered top-right.
  • Responsive: rail collapses above the grid on narrow viewports.

Tests

12 new unit tests in tests/github.test.ts covering:

  • buildQualifiers: empty, multi-topic, reserved-topic dropping, whitespace quoting, falsy filter handling.
  • computeFacets: sorting, per-repo dedupe, NOASSERTION skip, empty input.

bun test → 17 pass / 0 fail. bun run typecheck clean.

Test plan

  • bun test and bun run typecheck both pass on CI
  • Open / → Store. Confirm filter rail loads with counts.
  • Type into search — observe debounced refresh; Esc clears.
  • Click a topic facet → grid narrows; chip appears; click chip to remove.
  • Click a topic tag on a card → same behaviour.
  • Click a language stat / author name on a card → grid pivots.
  • Stack two filters (e.g. topic:swift + language:Swift).
  • "Clear all" resets everything.
  • Switch category tabs (All / Plugins / Templates) — filters persist, results refresh.
  • Mobile (<= 768px): rail stacks above results.

🤖 Generated with Claude Code

…r, license

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) <noreply@anthropic.com>
@corvid-agent

Copy link
Copy Markdown
Collaborator

👋 CorvidAgent is looking into this.

@0xLeif
0xLeif merged commit 08a55ab into main May 6, 2026
6 checks passed

@corvid-agent corvid-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid work — clean refactor with good test coverage. Approving.

What I liked:

  • buildQualifiers() / computeFacets() are well-separated and independently testable. The generic tally() helper with per-repo dedup is a nice pattern.
  • Reserved topic stripping is applied consistently (both in filter construction and facet output).
  • 12 tests cover the important edge cases: empty/falsy filters, whitespace quoting, NOASSERTION skip, intra-repo deduplication.
  • Search debounce (300ms) with Enter bypass and Esc clear is good UX.
  • setSingleFilter toggling (same value clears) gives the filter UI consistent behavior.
  • Responsive collapse of the filter rail at <= 768px is handled cleanly.

Minor observations (non-blocking):

  • quoteIfNeeded() wraps values with whitespace in double quotes but doesn't escape embedded quotes. Unlikely to matter with GitHub's language taxonomy, but worth a comment or guard if the filter surface expands later.
  • Facet counts reflect only the first 50 results (per_page=50), not the full result set. The PR description is honest about this ("computed from the current result set"), but users might find it surprising when a filter shows count=1 for something that actually appears in many repos beyond the first page. If this becomes a problem, a follow-up could fetch total_count from the search response to indicate incomplete facets.
  • The applied field in the API response is useful for debugging but isn't consumed by the frontend — could be dropped to keep the response lean, or documented as intentional.

Clean code, good tests, sensible UX — ship it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants