diff --git a/.specs/issue-14-explore-filters.md b/.specs/issue-14-explore-filters.md new file mode 100644 index 0000000..2073dcf --- /dev/null +++ b/.specs/issue-14-explore-filters.md @@ -0,0 +1,131 @@ +# Spec: Explore Filters — search, ranges, method, star type, all in the URL +Issue: #14 +Status: approved + +## Problem Statement +ROADMAP item 3 says "Improve Advanced Filters", but there is little to improve: the explore +page's only controls are a search box and a single-select discovery-method dropdown that live +as component-local state inside `PlanetTable` — they apply only to the table view (grid view is +unfiltered), vanish on reload, and can't be shared. A visitor who narrows ~6,000 planets down +to "temperate, roughly Earth-sized, around a red dwarf" has no way to do it, and no way to send +that view to anyone. This is a build, not an enhancement (#14). + +## Goals +- Filter the explore list by name/host search, radius, mass, orbital period, discovery method(s), and star type. +- Filter and sort state lives in the URL: shareable, bookmarkable, reload-safe. +- One module owns filter semantics; grid and table views always show the same filtered set. +- Controls are keyboard- and screen-reader-accessible, with a live results count. + +## Out of Scope +- localStorage persistence — the URL is the single share primitive (per #14 discussion). +- The grid/table `view` toggle stays component-local and out of the URL — it's a presentation preference, not part of the selection; a shared URL reproduces *what* is shown, not *how*. +- Server-side filtering or API query params (see Technical Approach). +- Comparison tool, saved searches, 3D visualisation — downstream, part of the #36 direction discussion. +- Fixing #6 (keyboard access to table rows/sort headers) — separate task issue; should land before this epic's UI tasks. + +## Acceptance Criteria +- [ ] Search box filters by planet name or host star (case-insensitive substring, current semantics), reflected in `?q=`, and applies to **both** grid and table views. +- [ ] Radius, mass, and orbital period each have a min/max range control on a log scale, reflected in the URL; a planet lacking that measurement is excluded **only while that filter is active**, and the UI says so (e.g. "planets without a measured mass are hidden"). +- [ ] Discovery method is a multi-select whose options are the distinct values present in the data, reflected as `?method=` (comma-separated). +- [ ] Star type filter offers O/B/A/F/G/K/M classes with plain-language labels ("M — red dwarf"), reflected as `?star=`. +- [ ] Table sort key/direction is reflected as `?sort=` (e.g. `?sort=pl_rade.asc`); the default (`disc_year.desc`) is omitted from the URL; a shared URL reproduces both the filtered set and its ordering, and Back/Forward keeps the header ▲/▼ indicators in sync with the actual order. +- [ ] "Clear all" resets every filter and the URL; the results count ("N of M planets", M computed from the fetched list — never a hardcoded total) updates in an `aria-live=polite` region. +- [ ] Malformed or unknown URL params are silently ignored — the page never shows an error state because of a bad query string. +- [ ] Changing any filter clamps/resets pagination; no empty page-37-of-2 states. +- [ ] `lib/planetFilters.ts` is pure and unit-tested: URL codec round-trips, null-value semantics, and teff→class banding (band edges exactly as tabulated in Technical Approach, including boundary values and the sub-2,300 K/unclassified case). +- [ ] `applyFilters` over the full list is memoised in `ExploreClient` (`useMemo` on planets + filter state) — `PlanetTable` already memoises today, and the hoist must not silently drop that. +- [ ] `/api/planets` projection includes `pl_orbper` and `st_teff`; `route.test.ts` updated accordingly. +- [ ] All range/select controls are native HTML inputs or have full keyboard operation with visible focus; labels associated via `