Packages

A shadcn/ui-inspired UI library for Gleam and Lustre

Current section

Files

Jump to
glizzy src ui date_range_picker.gleam
Raw

src/ui/date_range_picker.gleam

// MIT License
// Copyright (c) 2026 Koncreate
// See LICENSE for details
import gleam/string
import lustre/attribute.{type Attribute, attribute, class, type_}
import lustre/element.{type Element}
import lustre/element/html
import ui/css
import ui/size.{type Size, input_class}
// ============================================================================
// Types
// ============================================================================
pub type Variant {
Default
Muted
}
// ============================================================================
// Date Range Picker Component (Presentational - controlled by parent)
// ============================================================================
pub fn picker(
attributes: List(Attribute(a)),
start_attributes: List(Attribute(a)),
end_attributes: List(Attribute(a)),
) -> Element(a) {
html.div([class("relative"), ..attributes], [
html.div([class("flex items-center gap-2")], [
start_field(start_attributes),
html.span([class("text-muted-foreground")], [html.text("→")]),
end_field(end_attributes),
calendar_button([]),
]),
])
}
pub fn start_field(attributes: List(Attribute(a))) -> Element(a) {
html.input([
class(
"flex h-10 w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm placeholder:text-muted-foreground "
<> css.focus_ring()
<> " "
<> css.disabled(),
),
type_("text"),
attribute("role", "combobox"),
attribute("aria-label", "Start date"),
attribute("aria-haspopup", "dialog"),
attribute("aria-expanded", "false"),
attribute("autocomplete", "off"),
attribute("placeholder", "Start date"),
..attributes
])
}
pub fn end_field(attributes: List(Attribute(a))) -> Element(a) {
html.input([
class(
"flex h-10 w-full rounded-md border border-input bg-background px-4 py-2.5 text-sm placeholder:text-muted-foreground "
<> css.focus_ring()
<> " "
<> css.disabled(),
),
type_("text"),
attribute("role", "combobox"),
attribute("aria-label", "End date"),
attribute("aria-haspopup", "dialog"),
attribute("aria-expanded", "false"),
attribute("autocomplete", "off"),
attribute("placeholder", "End date"),
..attributes
])
}
pub fn calendar_button(attributes: List(Attribute(a))) -> Element(a) {
html.button(
[
class(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors "
<> css.focus_ring()
<> " disabled:pointer-events-none "
<> css.disabled()
<> " border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 w-10 ",
),
type_("button"),
attribute("aria-label", "Choose date range"),
attribute("data-date-range-picker-trigger", "true"),
..attributes
],
[calendar_icon()],
)
}
pub fn content(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class(
"absolute top-full left-0 z-50 mt-2 w-auto min-w-[560px] border border-input bg-background rounded-md shadow-lg p-3 ",
),
attribute("data-date-range-picker-content", "true"),
attribute("role", "dialog"),
attribute("aria-modal", "true"),
attribute("aria-label", "Select date range"),
..attributes
],
children,
)
}
pub fn calendar(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class("space-y-3"),
attribute("role", "grid"),
attribute("aria-label", "Calendar"),
attribute("aria-readonly", "true"),
..attributes
],
children,
)
}
pub fn header(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[class("flex items-center justify-between mb-2"), ..attributes],
children,
)
}
pub fn title(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class("text-base font-semibold text-foreground tracking-wide"),
attribute("role", "heading"),
attribute("aria-live", "polite"),
..attributes
],
children,
)
}
pub fn nav_button(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.button(
[
class(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors "
<> css.focus_ring()
<> " disabled:pointer-events-none "
<> css.disabled()
<> " h-7 w-7 hover:bg-accent hover:text-accent-foreground ",
),
type_("button"),
attribute("data-date-picker-nav", "true"),
..attributes
],
children,
)
}
/// Navigation button for previous month with proper aria-label.
pub fn nav_button_prev(attributes: List(Attribute(a))) -> Element(a) {
nav_button([attribute("aria-label", "Previous month"), ..attributes], [
chevron_left(),
])
}
/// Navigation button for next month with proper aria-label.
pub fn nav_button_next(attributes: List(Attribute(a))) -> Element(a) {
nav_button([attribute("aria-label", "Next month"), ..attributes], [
chevron_right(),
])
}
pub fn weekday(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class(
[
"h-8 w-9 text-[11px] p-0 font-medium uppercase tracking-wider text-muted-foreground/70",
"flex items-center justify-center",
]
|> string.join(" "),
),
attribute("role", "columnheader"),
..attributes
],
children,
)
}
pub fn day(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.button(
[
class(
[
// Base sizing - ensure consistent cell size and perfect centering
"relative h-9 w-9 p-0 font-normal text-sm rounded-md",
"transition-colors duration-150 ease-in-out",
css.focus_ring(),
"disabled:pointer-events-none " <> css.disabled(),
"hover:bg-accent hover:text-accent-foreground",
// Selected dates (range start/end) - primary color with rounded ends
"[&[data-selected='true']]:bg-primary",
"[&[data-selected='true']]:text-primary-foreground",
"[&[data-selected='true']]:hover:bg-primary",
"[&[data-selected='true']]:hover:text-primary-foreground",
"[&[data-selected='true']]:font-medium",
// Range start - rounded right only
"[&[data-range-start='true']]:rounded-r-none",
"[&[data-range-start='true']]:rounded-l-md",
"[&[data-range-start='true']]:bg-primary",
"[&[data-range-start='true']]:text-primary-foreground",
// Range end - rounded left only
"[&[data-range-end='true']]:rounded-l-none",
"[&[data-range-end='true']]:rounded-r-md",
"[&[data-range-end='true']]:bg-primary",
"[&[data-range-end='true']]:text-primary-foreground",
// In-range dates - accent color with square edges
"[&[data-in-range='true']]:bg-accent",
"[&[data-in-range='true']]:text-accent-foreground",
"[&[data-in-range='true']]:rounded-none",
// Today indicator - subtle ring
"[&[data-today='true']]:ring-1 [&[data-today='true']]:ring-primary [&[data-today='true']]:ring-offset-1",
"[&[data-today='true']]:font-semibold",
// Disabled dates
"[&[data-disabled='true']]:opacity-30",
"[&[data-disabled='true']]:pointer-events-none",
// Outside month dates
"[&[data-outside-month='true']]:text-muted-foreground/40",
]
|> string.join(" "),
),
type_("button"),
attribute("role", "gridcell"),
attribute("data-date-picker-day", "true"),
..attributes
],
children,
)
}
pub fn week(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
attribute("style", "display: contents"),
attribute("role", "row"),
..attributes
],
children,
)
}
pub fn grid(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class("grid grid-cols-7 gap-0.5"),
attribute("role", "grid"),
attribute("aria-label", "Days"),
..attributes
],
children,
)
}
pub fn weekdays(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class("grid grid-cols-7 gap-0.5 mb-1"),
attribute("role", "row"),
attribute("aria-label", "Weekdays"),
..attributes
],
children,
)
}
pub fn range_calendar(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div([class("grid grid-cols-2 gap-4"), ..attributes], children)
}
// ============================================================================
// Presets Component
// ============================================================================
pub fn presets(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div([class("flex flex-wrap gap-2 p-3 border-t"), ..attributes], children)
}
pub fn preset_button(
attributes: List(Attribute(a)),
label: String,
) -> Element(a) {
html.button(
[
class(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-xs font-medium transition-colors "
<> css.focus_ring()
<> " disabled:pointer-events-none "
<> css.disabled()
<> " border border-input bg-background hover:bg-accent hover:text-accent-foreground h-8 px-3 ",
),
type_("button"),
attribute("data-date-range-picker-preset", "true"),
..attributes
],
[html.text(label)],
)
}
// ============================================================================
// Variant & Size Helpers
// ============================================================================
pub fn variant(v: Variant) -> Attribute(a) {
case v {
Default -> class("border-input bg-background")
Muted -> class("border-input bg-muted")
}
}
pub fn size(s: Size) -> Attribute(a) {
class(input_class(s))
}
pub fn required() -> Attribute(a) {
attribute("aria-required", "true")
}
pub fn disabled() -> Attribute(a) {
attribute("aria-disabled", "true")
}
pub fn invalid() -> Attribute(a) {
attribute("aria-invalid", "true")
}
pub fn placeholder(placeholder: String) -> Attribute(a) {
attribute("placeholder", placeholder)
}
pub fn value(val: String) -> Attribute(a) {
attribute("value", val)
}
pub fn start_value(val: String) -> Attribute(a) {
attribute("data-start-value", val)
}
pub fn end_value(val: String) -> Attribute(a) {
attribute("data-end-value", val)
}
// ============================================================================
// Icons
// ============================================================================
pub fn calendar_icon() -> Element(a) {
html.i(
[class("ri ri-calendar-line h-4 w-4"), attribute("aria-hidden", "true")],
[],
)
}
pub fn chevron_left() -> Element(a) {
html.i(
[class("ri ri-arrow-left-s-line h-4 w-4"), attribute("aria-hidden", "true")],
[],
)
}
pub fn chevron_right() -> Element(a) {
html.i(
[
class("ri ri-arrow-right-s-line h-4 w-4"),
attribute("aria-hidden", "true"),
],
[],
)
}