Filter & Date Primitives
Loquent ships three UI primitives that power date filtering and collapsible filter sections across the app: Calendar, Collapsible, and TimeRangeSelector. These work together with the TimeRange enum to provide consistent date-range filtering in contacts, calls, dashboard, and task views.
Calendar
Section titled “Calendar”A standalone month grid with day-of-week headers, prev/next navigation, selected-date highlighting, and today indicator.
File: src/ui/calendar_ui.rs
#[component]pub fn Calendar( selected_date: Option<NaiveDate>, on_select: EventHandler<NaiveDate>, display_month: NaiveDate, on_month_change: EventHandler<NaiveDate>,) -> Element| Prop | Type | Purpose |
|---|---|---|
selected_date | Option<NaiveDate> | Highlighted day cell, if any |
on_select | EventHandler<NaiveDate> | Fires when a day is clicked |
display_month | NaiveDate | Any date within the month to render |
on_month_change | EventHandler<NaiveDate> | Fires when the user navigates to a different month |
The grid renders 42 cells (6 weeks × 7 days). Days outside the displayed month render with muted opacity. The selected date gets bg-primary styling, and today gets a subtle border.
Usage:
use crate::ui::Calendar;
let mut month = use_signal(|| Local::now().date_naive());let mut selected = use_signal(|| Option::<NaiveDate>::None);
rsx! { Calendar { selected_date: selected(), on_select: move |d| selected.set(Some(d)), display_month: month(), on_month_change: move |m| month.set(m), }}Calendar is also used internally by DateTimePicker for its calendar view.
Collapsible
Section titled “Collapsible”A generic expandable section primitive with a clickable header and animated body. Used by the contact filter popover to organize filter groups (tags, status, date range, custom fields).
File: src/ui/collapsible_ui.rs
#[derive(Props, PartialEq, Clone)]pub struct CollapsibleProps { pub open: bool, pub on_toggle: EventHandler<()>, pub header: Element, pub children: Element, pub class: String, // optional, outer wrapper pub header_class: String, // optional, button element pub content_class: String, // optional, body container}The body uses CSS grid-rows animation: grid-rows-[1fr] when open, grid-rows-[0fr] when collapsed. This gives a smooth height transition without JavaScript measurement.
Usage:
use crate::ui::Collapsible;
let mut open = use_signal(|| true);
rsx! { Collapsible { open: open(), on_toggle: move |_| open.toggle(), header: rsx! { div { class: "flex items-center justify-between px-2 py-1.5", span { class: "text-sm font-medium", "Status & Tags" } span { class: "text-xs text-muted-foreground", "(2 active)" } } }, // Filter controls go here p { "Tag checkboxes, status toggles, etc." } }}The contact filter popover uses one Collapsible per filter section. Each header shows the section name plus an active-filter count badge, and a per-section clear button.
TimeRangeSelector
Section titled “TimeRangeSelector”A popover-style picker that combines preset time ranges with a custom date-range input. Click the trigger button to open a floating panel listing all presets (Today, Last 7 Days, etc.) with a checkmark on the active selection, plus inline date inputs for custom ranges.
File: src/shared/components/time_range_selector_component.rs
#[component]pub fn TimeRangeSelector( value: TimeRange, on_change: EventHandler<TimeRange>, #[props(default = "right")] align: &'static str, #[props(default = true)] limit_to_past: bool,) -> Element| Prop | Type | Default | Purpose |
|---|---|---|---|
value | TimeRange | — | Current selection |
on_change | EventHandler<TimeRange> | — | Fires when the user picks a preset or completes a custom range |
align | &'static str | "right" | Anchor edge of the popover relative to the trigger ("left" or "right") |
limit_to_past | bool | true | When true, restricts calendar to today and earlier. Set to false for forward-looking filters (due dates, scheduled events) |
Behavior:
- Clicking a preset calls
on_changeimmediately and closes the popover. - When
limit_to_pastisfalse, future presets (Tomorrow, Next 7 Days, Next 30 Days) appear below a separator after the standard presets. - The custom calendar’s
max_dateis set to today whenlimit_to_pastistrue, or removed entirely whenfalse— allowing future date selection. - In the “Custom Range” section, both start and end inputs must have values before
on_changefires withTimeRange::Custom { start, end }. - The trigger button shows the current label (e.g., “Last 7 Days” or “Jan 15 – Feb 23”).
- An outside-click backdrop closes the popover without changing the value.
Usage (past-only, default):
use crate::shared::TimeRangeSelector;use crate::shared::TimeRange;
let mut range = use_signal(|| TimeRange::Last30Days);
rsx! { TimeRangeSelector { value: range(), on_change: move |r| range.set(r), }}Usage (future dates allowed):
use crate::shared::TimeRangeSelector;use crate::shared::TimeRange;
let mut range = use_signal(|| TimeRange::Next7Days);
rsx! { TimeRangeSelector { value: range(), on_change: move |r| range.set(r), limit_to_past: false, }}The component uses fixed positioning with z-[1000] to escape overflow containers like scrollable sidebars.
TimeRange Type
Section titled “TimeRange Type”The TimeRange enum represents all selectable date-range options.
File: src/shared/types/time_range_type.rs
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]#[serde(rename_all = "snake_case")]pub enum TimeRange { Today, Yesterday, Tomorrow, Last7Days, Last30Days, Last90Days, Next7Days, Next30Days, AllTime, Custom { start: String, end: String },}Variants
Section titled “Variants”| Variant | Serde value | Label | Date range |
|---|---|---|---|
Today | "today" | ”Today” | [today, today] |
Yesterday | "yesterday" | ”Yesterday” | [today-1, today-1] |
Tomorrow | "tomorrow" | ”Tomorrow” | [today+1, today+1] |
Last7Days | "last_7_days" | ”Last 7 Days” | [today-6, today] |
Last30Days | "last_30_days" | ”Last 30 Days” | [today-29, today] |
Last90Days | "last_90_days" | ”Last 90 Days” | [today-89, today] |
Next7Days | "next_7_days" | ”Next 7 Days” | [today, today+6] |
Next30Days | "next_30_days" | ”Next 30 Days” | [today, today+29] |
AllTime | "all_time" | ”All Time” | None (no bounds) |
Custom | object | ”Jan 15 – Feb 23” | parsed from start/end ISO strings |
Key Methods
Section titled “Key Methods”| Method | Returns | Description |
|---|---|---|
label() | String | Human-readable label: “Last 7 Days”, “Jan 15 – Feb 23” |
subtitle() | String | KPI card subtitle: “last 7 days”, “next 30 days” |
all() | &[TimeRange] | Past-facing presets in display order (excludes Custom and future variants) |
future_presets() | &[TimeRange] | Future-facing presets: [Tomorrow, Next7Days, Next30Days] |
to_date_range() | Option<(NaiveDate, NaiveDate)> | Resolves to concrete start/end dates (inclusive). Returns None for AllTime |
to_value() | String | Serde string form for presets, "custom" for Custom |
from_value(s) | Option<Self> | Deserialize from serde string form |
is_custom() | bool | true for the Custom variant |
from_str_or(s, default) | Self | Parse with fallback for AI tool parameters |
Server-Side Conversion
Section titled “Server-Side Conversion”On the server, time_range_start() and time_range_end() convert a TimeRange to NaiveDateTime bounds for database queries:
// Lower bound — None for AllTimepub fn time_range_start(range: &TimeRange) -> Option<NaiveDateTime>
// Upper bound — None unless the range has a definite endpub fn time_range_end(range: &TimeRange) -> Option<NaiveDateTime>For future variants, both functions return appropriate bounds:
Tomorrow: start = tomorrow midnight, end = day-after-tomorrow midnightNext7Days: start = today midnight, end = 7 days from now midnightNext30Days: start = today midnight, end = 30 days from now midnight
For Custom ranges, start/end parse from YYYY-MM-DD strings. The end bound is exclusive (start of the next day).
Where These Are Used
Section titled “Where These Are Used”| Component | Used In | limit_to_past |
|---|---|---|
Calendar | DateTimePicker (task due dates) | — |
Collapsible | Contact filter popover, settings panels | — |
TimeRangeSelector | Dashboard, calls view | true (default) |
TimeRangeSelector | Custom date field filters (contacts) | false |
TimeRange | Contact filters, dashboard KPIs, call listing, report queries | — |
Custom Date Field Filters
Section titled “Custom Date Field Filters”Contact custom fields of type “date” can store future dates (e.g., renewal dates, appointment dates). The custom field filter component passes limit_to_past: false to TimeRangeSelector, which enables future preset options and removes the calendar’s max-date restriction.
File: src/mods/contact/components/filter_bar/custom_field_filter_component.rs
TimeRangeSelector { value: current_range, on_change: move |r| { /* update filter state */ }, limit_to_past: false,}Existing date filters (dashboard, calls, last-contacted) continue to use the default limit_to_past: true — no regression.