use maud::{html, Markup};
#[derive(Clone, Debug)]
pub struct Props {
pub id: String,
pub name: String,
pub selected: Option<(u32, u32, u32)>,
pub placeholder: String,
pub disabled: bool,
pub min_date: Option<(u32, u32, u32)>,
pub max_date: Option<(u32, u32, u32)>,
}
impl Default for Props {
fn default() -> Self {
Self {
id: "date-picker".to_string(),
name: "date".to_string(),
selected: None,
placeholder: "Pick a date".to_string(),
disabled: false,
min_date: None,
max_date: None,
}
}
}
fn format_display(year: u32, month: u32, day: u32) -> String {
let month_name = month_name(month);
format!("{} {}, {}", month_name, day, year)
}
fn format_iso(year: u32, month: u32, day: u32) -> String {
format!("{:04}-{:02}-{:02}", year, month, day)
}
fn month_name(month: u32) -> &'static str {
match month {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "June",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
_ => "January",
}
}
fn days_in_month(year: u32, month: u32) -> u32 {
match month {
1 => 31,
2 => {
if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
29
} else {
28
}
}
3 => 31,
4 => 30,
5 => 31,
6 => 30,
7 => 31,
8 => 31,
9 => 30,
10 => 31,
11 => 30,
12 => 31,
_ => 30,
}
}
fn day_of_week(year: u32, month: u32, day: u32) -> u32 {
let t: [u32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
let y = if month < 3 { year - 1 } else { year };
let m = month as usize;
((y + y / 4 - y / 100 + y / 400 + t[m - 1] + day) % 7) as u32
}
fn render_calendar_grid(
year: u32,
month: u32,
selected: Option<(u32, u32, u32)>,
min_date: Option<(u32, u32, u32)>,
max_date: Option<(u32, u32, u32)>,
) -> Markup {
let total_days = days_in_month(year, month);
let first_dow = day_of_week(year, month, 1);
let day_headers = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
html! {
div class="mui-date-picker__calendar" data-year=(year) data-month=(month) {
div class="mui-date-picker__cal-header" {
button type="button" class="mui-date-picker__nav-btn" data-action="prev-month"
aria-label="Previous month" { "\u{2039}" }
span class="mui-date-picker__cal-title" {
(month_name(month)) " " (year)
}
button type="button" class="mui-date-picker__nav-btn" data-action="next-month"
aria-label="Next month" { "\u{203a}" }
}
div class="mui-date-picker__day-headers" {
@for dh in &day_headers {
span class="mui-date-picker__day-header" { (*dh) }
}
}
div class="mui-date-picker__days" {
@for _ in 0..first_dow {
span class="mui-date-picker__day mui-date-picker__day--empty" {}
}
@for d in 1..=total_days {
@let is_selected = selected.map_or(false, |(sy, sm, sd)| sy == year && sm == month && sd == d);
@let is_disabled = {
let before_min = min_date.map_or(false, |(my, mm, md)| {
year < my || (year == my && month < mm) || (year == my && month == mm && d < md)
});
let after_max = max_date.map_or(false, |(xy, xm, xd)| {
year > xy || (year == xy && month > xm) || (year == xy && month == xm && d > xd)
});
before_min || after_max
};
@let mut cls = String::from("mui-date-picker__day");
@if is_selected {
@let _ = cls.push_str(" mui-date-picker__day--selected");
}
@if is_disabled {
@let _ = cls.push_str(" mui-date-picker__day--disabled");
}
button type="button" class=(cls)
data-day=(d) data-month=(month) data-year=(year)
disabled[is_disabled]
{
(d)
}
}
}
}
}
}
pub fn render(props: Props) -> Markup {
let display_text = match props.selected {
Some((y, m, d)) => format_display(y, m, d),
None => props.placeholder.clone(),
};
let iso_value = match props.selected {
Some((y, m, d)) => format_iso(y, m, d),
None => String::new(),
};
let has_value = props.selected.is_some();
let (cal_year, cal_month) = match props.selected {
Some((y, m, _)) => (y, m),
None => (2026, 4), };
html! {
div class="mui-date-picker" data-mui="date-picker" {
@if props.disabled {
button type="button" class="mui-date-picker__trigger mui-input"
id=(props.id)
aria-expanded="false"
aria-haspopup="dialog"
disabled
{
span class=(if has_value { "mui-date-picker__value" } else { "mui-date-picker__value mui-date-picker__value--placeholder" }) {
(display_text)
}
span class="mui-date-picker__icon" aria-hidden="true" {
(maud::PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>"#))
}
}
} @else {
button type="button" class="mui-date-picker__trigger mui-input"
id=(props.id)
aria-expanded="false"
aria-haspopup="dialog"
{
span class=(if has_value { "mui-date-picker__value" } else { "mui-date-picker__value mui-date-picker__value--placeholder" }) {
(display_text)
}
span class="mui-date-picker__icon" aria-hidden="true" {
(maud::PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>"#))
}
}
}
div class="mui-date-picker__dropdown" hidden {
(render_calendar_grid(cal_year, cal_month, props.selected, props.min_date, props.max_date))
}
input type="hidden" name=(props.name) value=(iso_value) class="mui-date-picker__hidden";
}
}
}
pub fn showcase() -> Markup {
html! {
div.mui-showcase__grid {
div {
p.mui-showcase__caption { "No selection (placeholder)" }
div.mui-showcase__row {
(render(Props {
id: "dp-1".to_string(),
name: "date-1".to_string(),
selected: None,
placeholder: "Pick a date".to_string(),
disabled: false,
min_date: None,
max_date: None,
}))
}
}
div {
p.mui-showcase__caption { "With selected date" }
div.mui-showcase__row {
(render(Props {
id: "dp-2".to_string(),
name: "date-2".to_string(),
selected: Some((2026, 4, 20)),
placeholder: "Pick a date".to_string(),
disabled: false,
min_date: None,
max_date: None,
}))
}
}
div {
p.mui-showcase__caption { "Disabled" }
div.mui-showcase__row {
(render(Props {
id: "dp-3".to_string(),
name: "date-3".to_string(),
selected: Some((2026, 4, 15)),
placeholder: "Pick a date".to_string(),
disabled: true,
min_date: None,
max_date: None,
}))
}
}
}
}
}