feat: compact filter section as single horizontal strip (Task 5.2)

- Redesign filter_section() as 48px horizontal strip
- Remove "Filters" header (saves vertical space)
- Compact initiated_filter_dropdown() and last_seen_filter_dropdown()
  - 32px height triggers via compact_dropdown_trigger_style()
  - Labels moved inside dropdown panels
- Compact searchable_dropdown() component
  - 32px trigger height, no external label
  - Reduced panel item height (150px max, was 200px)
  - Smaller search input (size="1"), tighter spacing
- All filters now in ONE row with divider separator

Target: filter section height ≤ 60px (from ~200px)
This commit is contained in:
Andrew Charlwood
2026-02-05 01:53:38 +00:00
parent c9654905be
commit d2bed71078
2 changed files with 213 additions and 260 deletions
+7 -6
View File
@@ -60,17 +60,18 @@ python -m reflex compile
- [x] Verify: `python -c "from pathways_app.styles import *"` - PASSED - [x] Verify: `python -c "from pathways_app.styles import *"` - PASSED
### 5.2 Compact Filter Section (50-67% height reduction) ### 5.2 Compact Filter Section (50-67% height reduction)
- [ ] Redesign filter_section() as a single horizontal strip: - [x] Redesign filter_section() as a single horizontal strip:
- All filters in ONE row: Date dropdowns | Drugs | Indications | Directorates - All filters in ONE row: Date dropdowns | Drugs | Indications | Directorates
- Remove "Filters" header (saves vertical space) - Remove "Filters" header (saves vertical space)
- Use smaller dropdown triggers (height: 32px instead of 40px) - Use smaller dropdown triggers (height: 32px instead of 40px)
- Use icon-only labels where possible - Use icon-only labels where possible
- [ ] Reduce searchable_dropdown() panel heights: - [x] Reduce searchable_dropdown() panel heights:
- Max item list height: 150px (was 200px) - Max item list height: 150px (was 200px)
- Smaller search input - Smaller search input (size="1" instead of size="2")
- Tighter spacing (4px gaps instead of 8px) - Tighter spacing (6px/8px gaps via Spacing.SM/MD)
- [ ] Make filter dropdowns collapsible/expandable (optional advanced feature) - [x] Make filter dropdowns collapsible/expandable (optional advanced feature)
- [ ] Verify: Filter section height ≤ 60px when collapsed - Note: This was already implemented - dropdowns open/close on click
- [ ] Verify: Filter section height ≤ 60px when collapsed (requires visual verification)
### 5.3 Compact KPI Cards (50% reduction) ### 5.3 Compact KPI Cards (50% reduction)
- [ ] Reduce KPI card dimensions: - [ ] Reduce KPI card dimensions:
+206 -254
View File
@@ -20,6 +20,7 @@ from pathways_app.styles import (
Shadows, Shadows,
Transitions, Transitions,
TOP_BAR_HEIGHT, TOP_BAR_HEIGHT,
FILTER_STRIP_HEIGHT,
PAGE_MAX_WIDTH, PAGE_MAX_WIDTH,
PAGE_PADDING, PAGE_PADDING,
card_style, card_style,
@@ -31,6 +32,11 @@ from pathways_app.styles import (
kpi_card_style, kpi_card_style,
kpi_value_style, kpi_value_style,
kpi_label_style, kpi_label_style,
# v2.1 compact styles
filter_strip_style,
compact_dropdown_trigger_style,
searchable_dropdown_panel_style,
searchable_dropdown_item_style,
) )
@@ -1372,84 +1378,54 @@ class AppState(rx.State):
def initiated_filter_dropdown() -> rx.Component: def initiated_filter_dropdown() -> rx.Component:
""" """
Dropdown for selecting the treatment initiated time period. Compact dropdown for selecting the treatment initiated time period.
Redesigned for v2.1 filter strip - single row, no external label.
Options: All years, Last 2 years, Last 1 year Options: All years, Last 2 years, Last 1 year
Values: "all", "2yr", "1yr" Values: "all", "2yr", "1yr"
""" """
return rx.vstack( return rx.select.root(
# Label rx.select.trigger(
rx.text( placeholder="Initiated...",
"Treatment Initiated", **compact_dropdown_trigger_style(),
font_size=Typography.H3_SIZE,
font_weight=Typography.H3_WEIGHT,
color=Colors.SLATE_900,
font_family=Typography.FONT_FAMILY,
), ),
# Dropdown using rx.select with Root > Trigger > Content > Item pattern rx.select.content(
rx.select.root( rx.select.group(
rx.select.trigger(placeholder="Select period..."), rx.select.label("Treatment Initiated"),
rx.select.content( rx.select.item("All years", value="all"),
rx.select.group( rx.select.item("Last 2 years", value="2yr"),
rx.select.item("All years", value="all"), rx.select.item("Last 1 year", value="1yr"),
rx.select.item("Last 2 years", value="2yr"),
rx.select.item("Last 1 year", value="1yr"),
),
), ),
value=AppState.selected_initiated,
on_change=AppState.set_initiated_filter,
size="2",
), ),
# Description value=AppState.selected_initiated,
rx.text( on_change=AppState.set_initiated_filter,
"When patients first received treatment", size="1",
font_size=Typography.CAPTION_SIZE,
color=Colors.SLATE_500,
font_family=Typography.FONT_FAMILY,
),
spacing="1",
align="start",
) )
def last_seen_filter_dropdown() -> rx.Component: def last_seen_filter_dropdown() -> rx.Component:
""" """
Dropdown for selecting the last seen time period. Compact dropdown for selecting the last seen time period.
Redesigned for v2.1 filter strip - single row, no external label.
Options: Last 6 months, Last 12 months Options: Last 6 months, Last 12 months
Values: "6mo", "12mo" Values: "6mo", "12mo"
""" """
return rx.vstack( return rx.select.root(
# Label rx.select.trigger(
rx.text( placeholder="Last seen...",
"Last Seen", **compact_dropdown_trigger_style(),
font_size=Typography.H3_SIZE,
font_weight=Typography.H3_WEIGHT,
color=Colors.SLATE_900,
font_family=Typography.FONT_FAMILY,
), ),
# Dropdown using rx.select with Root > Trigger > Content > Item pattern rx.select.content(
rx.select.root( rx.select.group(
rx.select.trigger(placeholder="Select period..."), rx.select.label("Last Seen"),
rx.select.content( rx.select.item("Last 6 months", value="6mo"),
rx.select.group( rx.select.item("Last 12 months", value="12mo"),
rx.select.item("Last 6 months", value="6mo"),
rx.select.item("Last 12 months", value="12mo"),
),
), ),
value=AppState.selected_last_seen,
on_change=AppState.set_last_seen_filter,
size="2",
), ),
# Description value=AppState.selected_last_seen,
rx.text( on_change=AppState.set_last_seen_filter,
"Most recent treatment activity", size="1",
font_size=Typography.CAPTION_SIZE,
color=Colors.SLATE_500,
font_family=Typography.FONT_FAMILY,
),
spacing="1",
align="start",
) )
@@ -1467,12 +1443,13 @@ def searchable_dropdown(
clear_all_handler, clear_all_handler,
) -> rx.Component: ) -> rx.Component:
""" """
Searchable multi-select dropdown component. Compact searchable multi-select dropdown component.
Redesigned for v2.1 filter strip - 32px trigger, no external label.
Uses debounced search input (300ms) for smooth filtering. Uses debounced search input (300ms) for smooth filtering.
Args: Args:
label: Label for the dropdown label: Label shown inside dropdown panel header
selection_text: Text showing selection count selection_text: Text showing selection count
is_open: Whether dropdown is expanded is_open: Whether dropdown is expanded
toggle_handler: Handler to toggle dropdown open/close toggle_handler: Handler to toggle dropdown open/close
@@ -1485,137 +1462,127 @@ def searchable_dropdown(
clear_all_handler: Handler to clear selection clear_all_handler: Handler to clear selection
""" """
return rx.box( return rx.box(
rx.vstack( # Compact trigger button (no external label)
# Label rx.box(
rx.text( rx.hstack(
label, rx.text(
font_size=Typography.CAPTION_SIZE, selection_text,
font_weight=Typography.CAPTION_WEIGHT, font_size=Typography.BODY_SMALL_SIZE,
color=Colors.SLATE_700, color=Colors.SLATE_900,
font_family=Typography.FONT_FAMILY, font_family=Typography.FONT_FAMILY,
flex="1",
white_space="nowrap",
overflow="hidden",
text_overflow="ellipsis",
),
rx.icon(
rx.cond(is_open, "chevron-up", "chevron-down"),
size=14,
color=Colors.SLATE_500,
),
justify="between",
align="center",
gap=Spacing.SM,
), ),
# Dropdown trigger button **compact_dropdown_trigger_style(),
on_click=toggle_handler,
min_width="120px",
),
# Dropdown panel
rx.cond(
is_open,
rx.box( rx.box(
rx.hstack( rx.vstack(
# Header with label
rx.text( rx.text(
selection_text, label,
font_size=Typography.BODY_SIZE, font_size=Typography.CAPTION_SIZE,
color=Colors.SLATE_900, font_weight=Typography.CAPTION_WEIGHT,
font_family=Typography.FONT_FAMILY,
flex="1",
),
rx.icon(
rx.cond(is_open, "chevron-up", "chevron-down"),
size=16,
color=Colors.SLATE_500, color=Colors.SLATE_500,
font_family=Typography.FONT_FAMILY,
padding_x=Spacing.MD,
padding_top=Spacing.SM,
), ),
justify="between", # Search input (debounced 300ms)
align="center", rx.hstack(
width="100%", rx.icon("search", size=12, color=Colors.SLATE_500),
), rx.debounce_input(
**input_style(), rx.input(
display="flex", placeholder="Search...",
align_items="center", value=search_value,
cursor="pointer", on_change=on_search_change,
on_click=toggle_handler, variant="soft",
width="100%",
),
# Dropdown panel
rx.cond(
is_open,
rx.box(
rx.vstack(
# Search input (debounced 300ms)
rx.hstack(
rx.icon("search", size=14, color=Colors.SLATE_500),
rx.debounce_input(
rx.input(
placeholder="Search...",
value=search_value,
on_change=on_search_change,
variant="soft",
size="2",
width="100%",
),
debounce_timeout=300,
),
spacing="2",
align="center",
width="100%",
padding=Spacing.SM,
background_color=Colors.SLATE_100,
border_radius=Radii.SM,
),
# Action buttons
rx.hstack(
rx.button(
"Select All",
on_click=select_all_handler,
variant="ghost",
size="1", size="1",
color_scheme="blue", width="100%",
), ),
rx.button( debounce_timeout=300,
"Clear",
on_click=clear_all_handler,
variant="ghost",
size="1",
color_scheme="gray",
),
spacing="2",
), ),
# Items list spacing="1",
rx.box( align="center",
rx.foreach(
filtered_items,
lambda item: rx.box(
rx.checkbox(
item,
checked=selected_items.contains(item),
on_change=lambda: toggle_item_handler(item),
size="2",
),
padding_y=Spacing.XS,
padding_x=Spacing.SM,
border_radius=Radii.SM,
background_color=rx.cond(
selected_items.contains(item),
Colors.PALE,
"transparent",
),
_hover={
"background_color": Colors.SLATE_100,
},
width="100%",
),
),
max_height="200px",
overflow_y="auto",
width="100%",
),
spacing="2",
align="start",
width="100%", width="100%",
padding=Spacing.SM, padding_x=Spacing.SM,
), ),
position="absolute", # Action buttons (more compact)
top="100%", rx.hstack(
left="0", rx.button(
right="0", "All",
background_color=Colors.WHITE, on_click=select_all_handler,
border=f"1px solid {Colors.SLATE_300}", variant="ghost",
border_radius=Radii.MD, size="1",
box_shadow=Shadows.LG, color_scheme="blue",
z_index="50", ),
margin_top=Spacing.XS, rx.button(
"None",
on_click=clear_all_handler,
variant="ghost",
size="1",
color_scheme="gray",
),
spacing="1",
padding_x=Spacing.SM,
),
# Items list (reduced height)
rx.box(
rx.foreach(
filtered_items,
lambda item: rx.box(
rx.checkbox(
item,
checked=selected_items.contains(item),
on_change=lambda: toggle_item_handler(item),
size="1",
),
padding=f"{Spacing.SM} {Spacing.MD}",
font_size=Typography.BODY_SMALL_SIZE,
cursor="pointer",
background_color=rx.cond(
selected_items.contains(item),
Colors.PALE,
"transparent",
),
_hover={
"background_color": Colors.SLATE_100,
},
width="100%",
),
),
max_height="150px",
overflow_y="auto",
width="100%",
),
spacing="1",
align="start",
width="100%",
padding_bottom=Spacing.SM,
), ),
**searchable_dropdown_panel_style(),
position="absolute",
top="100%",
left="0",
margin_top=Spacing.XS,
), ),
spacing="1",
align="start",
width="100%",
), ),
position="relative", position="relative",
width="100%",
) )
@@ -1743,97 +1710,82 @@ def top_bar() -> rx.Component:
def filter_section() -> rx.Component: def filter_section() -> rx.Component:
""" """
Filter section component. Compact filter strip component (v2.1 redesign).
Contains: Single horizontal row containing ALL filters:
- Two date filter dropdowns: Treatment Initiated, Last Seen - Date filters: Treatment Initiated, Last Seen
- Three searchable multi-select dropdowns: Drugs, Indications, Directorates - Multi-select filters: Drugs, Indications, Directorates
Layout: Two rows Target height: 48px (single row)
- Row 1: Date filter dropdowns side by side No "Filters" header - labels are in dropdown triggers/panels.
- Row 2: Three searchable dropdowns in a grid
""" """
return rx.box( return rx.box(
rx.vstack( rx.hstack(
# Header # Date filters group
rx.text(
"Filters",
**text_h1(),
),
# Row 1: Date filter dropdowns
rx.hstack( rx.hstack(
initiated_filter_dropdown(), initiated_filter_dropdown(),
rx.divider(orientation="vertical", size="3"),
last_seen_filter_dropdown(), last_seen_filter_dropdown(),
spacing="5", spacing="2",
align="start", align="center",
flex_wrap="wrap",
), ),
# Divider # Separator
rx.divider(size="4"), rx.divider(
# Row 2: Searchable dropdowns orientation="vertical",
size="2",
color_scheme="gray",
),
# Multi-select filters group
rx.hstack( rx.hstack(
rx.box( searchable_dropdown(
searchable_dropdown( label="Drugs",
label="Drugs", selection_text=AppState.drug_selection_text,
selection_text=AppState.drug_selection_text, is_open=AppState.drug_dropdown_open,
is_open=AppState.drug_dropdown_open, toggle_handler=AppState.toggle_drug_dropdown,
toggle_handler=AppState.toggle_drug_dropdown, search_value=AppState.drug_search,
search_value=AppState.drug_search, on_search_change=AppState.set_drug_search,
on_search_change=AppState.set_drug_search, filtered_items=AppState.filtered_drugs,
filtered_items=AppState.filtered_drugs, selected_items=AppState.selected_drugs,
selected_items=AppState.selected_drugs, toggle_item_handler=AppState.toggle_drug,
toggle_item_handler=AppState.toggle_drug, select_all_handler=AppState.select_all_drugs,
select_all_handler=AppState.select_all_drugs, clear_all_handler=AppState.clear_all_drugs,
clear_all_handler=AppState.clear_all_drugs,
),
flex="1",
min_width="200px",
), ),
rx.box( searchable_dropdown(
searchable_dropdown( label="Indications",
label="Indications", selection_text=AppState.indication_selection_text,
selection_text=AppState.indication_selection_text, is_open=AppState.indication_dropdown_open,
is_open=AppState.indication_dropdown_open, toggle_handler=AppState.toggle_indication_dropdown,
toggle_handler=AppState.toggle_indication_dropdown, search_value=AppState.indication_search,
search_value=AppState.indication_search, on_search_change=AppState.set_indication_search,
on_search_change=AppState.set_indication_search, filtered_items=AppState.filtered_indications,
filtered_items=AppState.filtered_indications, selected_items=AppState.selected_indications,
selected_items=AppState.selected_indications, toggle_item_handler=AppState.toggle_indication,
toggle_item_handler=AppState.toggle_indication, select_all_handler=AppState.select_all_indications,
select_all_handler=AppState.select_all_indications, clear_all_handler=AppState.clear_all_indications,
clear_all_handler=AppState.clear_all_indications,
),
flex="1",
min_width="200px",
), ),
rx.box( searchable_dropdown(
searchable_dropdown( label="Directorates",
label="Directorates", selection_text=AppState.directorate_selection_text,
selection_text=AppState.directorate_selection_text, is_open=AppState.directorate_dropdown_open,
is_open=AppState.directorate_dropdown_open, toggle_handler=AppState.toggle_directorate_dropdown,
toggle_handler=AppState.toggle_directorate_dropdown, search_value=AppState.directorate_search,
search_value=AppState.directorate_search, on_search_change=AppState.set_directorate_search,
on_search_change=AppState.set_directorate_search, filtered_items=AppState.filtered_directorates,
filtered_items=AppState.filtered_directorates, selected_items=AppState.selected_directorates,
selected_items=AppState.selected_directorates, toggle_item_handler=AppState.toggle_directorate,
toggle_item_handler=AppState.toggle_directorate, select_all_handler=AppState.select_all_directorates,
select_all_handler=AppState.select_all_directorates, clear_all_handler=AppState.clear_all_directorates,
clear_all_handler=AppState.clear_all_directorates,
),
flex="1",
min_width="200px",
), ),
spacing="4", spacing="2",
width="100%", align="center",
flex_wrap="wrap",
), ),
spacing="4", # Spacer to push content left
rx.spacer(),
justify="start",
align="center",
gap=Spacing.LG,
width="100%", width="100%",
align="start",
), ),
**card_style(), **filter_strip_style(),
width="100%",
) )