Skip to content

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.

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
PropTypePurpose
selected_dateOption<NaiveDate>Highlighted day cell, if any
on_selectEventHandler<NaiveDate>Fires when a day is clicked
display_monthNaiveDateAny date within the month to render
on_month_changeEventHandler<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.

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.

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
PropTypeDefaultPurpose
valueTimeRangeCurrent selection
on_changeEventHandler<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_pastbooltrueWhen true, restricts calendar to today and earlier. Set to false for forward-looking filters (due dates, scheduled events)

Behavior:

  1. Clicking a preset calls on_change immediately and closes the popover.
  2. When limit_to_past is false, future presets (Tomorrow, Next 7 Days, Next 30 Days) appear below a separator after the standard presets.
  3. The custom calendar’s max_date is set to today when limit_to_past is true, or removed entirely when false — allowing future date selection.
  4. In the “Custom Range” section, both start and end inputs must have values before on_change fires with TimeRange::Custom { start, end }.
  5. The trigger button shows the current label (e.g., “Last 7 Days” or “Jan 15 – Feb 23”).
  6. 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.

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 },
}
VariantSerde valueLabelDate 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)
Customobject”Jan 15 – Feb 23”parsed from start/end ISO strings
MethodReturnsDescription
label()StringHuman-readable label: “Last 7 Days”, “Jan 15 – Feb 23”
subtitle()StringKPI 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()StringSerde string form for presets, "custom" for Custom
from_value(s)Option<Self>Deserialize from serde string form
is_custom()booltrue for the Custom variant
from_str_or(s, default)SelfParse with fallback for AI tool parameters

On the server, time_range_start() and time_range_end() convert a TimeRange to NaiveDateTime bounds for database queries:

// Lower bound — None for AllTime
pub fn time_range_start(range: &TimeRange) -> Option<NaiveDateTime>
// Upper bound — None unless the range has a definite end
pub fn time_range_end(range: &TimeRange) -> Option<NaiveDateTime>

For future variants, both functions return appropriate bounds:

  • Tomorrow: start = tomorrow midnight, end = day-after-tomorrow midnight
  • Next7Days: start = today midnight, end = 7 days from now midnight
  • Next30Days: 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).

ComponentUsed Inlimit_to_past
CalendarDateTimePicker (task due dates)
CollapsibleContact filter popover, settings panels
TimeRangeSelectorDashboard, calls viewtrue (default)
TimeRangeSelectorCustom date field filters (contacts)false
TimeRangeContact filters, dashboard KPIs, call listing, report queries

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.