From f90dc2dc5e4c7a80781e2daa744fbc682d5d18df Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Sun, 8 Mar 2026 20:07:56 -0700 Subject: [PATCH] feat(quotesdb): collapsible filter panel on browse page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a toggle button ("Filters ▼ / ▲") above the quote list on the browse page. Filter controls are hidden by default and expand when clicked. Each filter (Author, Tag, Date range) is on its own labelled row with consistent styling. Existing API query logic is unchanged. Co-Authored-By: Claude Sonnet 4.6 --- quotesdb/src/bin/ui/pages/browse.rs | 117 ++++++++++++++++++---------- quotesdb/src/bin/ui/style.css | 72 +++++++++++++++-- 2 files changed, 144 insertions(+), 45 deletions(-) diff --git a/quotesdb/src/bin/ui/pages/browse.rs b/quotesdb/src/bin/ui/pages/browse.rs index 21d4472..429cec3 100644 --- a/quotesdb/src/bin/ui/pages/browse.rs +++ b/quotesdb/src/bin/ui/pages/browse.rs @@ -14,6 +14,9 @@ use yew::prelude::*; /// Displays a paginated list of quotes. Supports filtering by author name, /// tag, and an optional date range (year granularity). Fetches from the API /// whenever page, author, tag, or date state changes. +/// +/// Filter controls are hidden behind a collapsible "Filters" toggle button. +/// The panel is collapsed by default when the page loads. #[function_component(BrowsePage)] pub fn browse_page() -> Html { let page = use_state(|| 1u32); @@ -25,6 +28,8 @@ pub fn browse_page() -> Html { let quotes: UseStateHandle> = use_state(Vec::new); let error: UseStateHandle> = use_state(|| None); let loading = use_state(|| true); + // Filter panel is collapsed by default. + let filters_open = use_state(|| false); // Fetch quotes whenever page, author, tag, or date bounds change { @@ -143,51 +148,85 @@ pub fn browse_page() -> Html { }) }; + let on_toggle_filters = { + let filters_open = filters_open.clone(); + Callback::from(move |_: MouseEvent| { + filters_open.set(!*filters_open); + }) + }; + + let toggle_label = if *filters_open { + "Filters \u{25b2}" + } else { + "Filters \u{25bc}" + }; + html! {

{ "Browse Quotes" }

-
- - -
- - -
-
- - -
+
+
+ if *filters_open { +
+
+ + +
+
+ + +
+
+ +
+ { "after" } + + { "before" } + +
+
+
+ } + if *loading {

{ "Loading..." }

} else if let Some(err) = (*error).clone() { diff --git a/quotesdb/src/bin/ui/style.css b/quotesdb/src/bin/ui/style.css index ee61e6d..d036afb 100644 --- a/quotesdb/src/bin/ui/style.css +++ b/quotesdb/src/bin/ui/style.css @@ -419,15 +419,69 @@ code { margin-bottom: 1.5rem; } +/* Row containing the toggle button, sits above the collapsible panel */ +.page-browse__filter-toggle-row { + margin-bottom: 0.75rem; +} + +/* Toggle button — inherits .btn base styles */ +.page-browse__filter-toggle { + font-size: 0.9rem; +} + +/* Collapsible filter panel — shown only when open */ .page-browse__filters { display: flex; - gap: 1rem; - margin-bottom: 2rem; - flex-wrap: wrap; + flex-direction: column; + gap: 0.75rem; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; } +/* A single filter row: label + input(s) on one line */ +.page-browse__filter-row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +/* Fixed-width label so all inputs line up vertically */ +.page-browse__filter-label { + flex-shrink: 0; + width: 4.5rem; + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text); + text-align: right; +} + +/* Text / number inputs inside the filter panel */ .page-browse__filter-input { - max-width: 240px; + flex: 1; + max-width: 260px; +} + +/* Narrower year inputs for the date range row */ +.page-browse__filter-input--year { + max-width: 120px; +} + +/* Inner group for the date range row: "after [input] before [input]" */ +.page-browse__filter-date-group { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* Small inline text labels ("after" / "before") */ +.page-browse__filter-date-label { + font-size: 0.875rem; + color: var(--color-text-muted); + white-space: nowrap; } .page-browse__loading, @@ -611,15 +665,21 @@ code { align-items: center; } - .page-browse__filters { - flex-direction: column; + .page-browse__filter-row { + flex-wrap: wrap; } .page-browse__filter-input, + .page-browse__filter-input--year, .page-author__filter-input { max-width: 100%; } + .page-browse__filter-date-group { + flex-direction: column; + align-items: flex-start; + } + .nav { padding: 0.75rem 1rem; }