Pantrix UI

Elastic Date Picker

A range of time that can move with the clock, as a dashboard is filtered by: "the last 15 minutes" is still the last 15 minutes at the next refresh. Elastic UI's EuiSuperDatePicker, built again on Pantrix.

import com.pantrixui.components.date.elasticdatepicker.PantrixElasticDatePicker

Examples

A dashboard with the ranges used lately

The last 15 minutes of a dashboard. The ranges chosen gather under the quick menu's "Recently used"; Refresh keeps a load going for a second, and the button spins meanwhile. Under it, the two moments the range falls on now and the time of the last refresh.

var range by remember { mutableStateOf(LAST_QUARTER) }
var refreshed by remember { mutableStateOf<Instant?>(null) }
val report = LocalExampleEvents.current
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = {
            range = it
            refreshed = Clock.System.now()
            report("Range chosen")
        },
        recentlyUsed = rememberRecentRanges(),
        onRefresh = {
            delay(LOADING_MILLIS)
            refreshed = Clock.System.now()
            report("Refreshed")
        },
        tagKey = "dashboard"
    )
    Resolved(range)
    refreshed?.let { PantrixText(text = "Last refresh: ${moment(it)}", style = PantrixTheme.typography.footnote) }
}

@Composable
private fun Resolved(range: PantrixTimeRange) {
    val read = range.resolve()
    PantrixText(
        text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
        style = PantrixTheme.typography.footnote,
        color = PantrixTheme.colors.textSecondary
    )
}

@Composable
private fun moment(at: Instant): String {
    val strings = PantrixTheme.strings
    val day = PantrixDate.of(at)
    val time = PantrixTime.of(at)
    return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

private const val LOADING_MILLIS = 1000L

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

Update button: in words, icon alone, none

The button beside the field three ways. In words it says Update while an edit waits; the icon alone does the same in less room; with no button each edit in an end is handed over at once. Change an end and see the difference.

var button by remember { mutableStateOf(ElasticDatePickerUpdateButton.Text) }
var range by remember { mutableStateOf(LAST_QUARTER) }
val report = LocalExampleEvents.current
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixSegmentButton(
        options = listOf(
            SegmentOption(ElasticDatePickerUpdateButton.Text, "In words", tagKey = "text"),
            SegmentOption(ElasticDatePickerUpdateButton.Icon, "Icon alone", tagKey = "icon"),
            SegmentOption(ElasticDatePickerUpdateButton.None, "No button", tagKey = "none")
        ),
        selected = button,
        onSelect = { button = it },
        fullWidth = false,
        tagKey = "update_button"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = {
            range = it
            report("Range handed over")
        },
        updateButton = button,
        tagKey = "update"
    )
    Resolved(range)
}

@Composable
private fun Resolved(range: PantrixTimeRange) {
    val read = range.resolve()
    PantrixText(
        text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
        style = PantrixTheme.typography.footnote,
        color = PantrixTheme.colors.textSecondary
    )
}

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

@Composable
private fun moment(at: Instant): String {
    val strings = PantrixTheme.strings
    val day = PantrixDate.of(at)
    val time = PantrixTime.of(at)
    return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

Popover, sheet or dialog

Where the panels open is the same on every platform: with presentation = Popover a popover beside the part, with presentation = Sheet a sheet from the edge of the screen, with presentation = Dialog a dialog in the middle of the window, as wide as the panel. The default is Adaptive: a sheet in a narrow window, a popover in a wider one; switch between "Narrow" and "Wide" in the card's window selector. None has a title over it. The quick menu, the start and the end all open the way chosen.

var presentation by remember { mutableStateOf(DatePickerPresentation.Sheet) }
val today = PantrixQuickRange.commonlyUsed(PantrixTheme.strings).first().range
var range by remember { mutableStateOf(today) }
// The card's window is as tall as the example: the example is kept tall so that a sheet opened in a narrow window has room.
Column(
    modifier = Modifier.fillMaxWidth().height(480.dp),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
    PantrixSegmentButton(
        options = listOf(
            SegmentOption(DatePickerPresentation.Popover, "Popover", tagKey = "popover"),
            SegmentOption(DatePickerPresentation.Sheet, "Sheet", tagKey = "sheet"),
            SegmentOption(DatePickerPresentation.Dialog, "Dialog", tagKey = "dialog"),
            SegmentOption(DatePickerPresentation.Adaptive, "Adaptive", tagKey = "adaptive")
        ),
        selected = presentation,
        onSelect = { presentation = it },
        fullWidth = false,
        tagKey = "presentation"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        presentation = presentation,
        tagKey = "presentation"
    )
}

The quick menu's own ranges

The quick menu's ranges are the app's: here seven ranges from the last 15 minutes to the last year in place of Elastic UI's eight. Switching "Recently used" shows or hides the list; a range with a name stands in the field by it.

val ranges = remember {
    listOf(
        PantrixQuickRange("Last 15 minutes", last(15, PantrixTimeUnit.Minutes)),
        PantrixQuickRange("Last 1 hour", last(1, PantrixTimeUnit.Hours)),
        PantrixQuickRange("Last 24 hours", last(24, PantrixTimeUnit.Hours)),
        PantrixQuickRange("Last 7 days", last(7, PantrixTimeUnit.Days)),
        PantrixQuickRange("Last 30 days", last(30, PantrixTimeUnit.Days)),
        PantrixQuickRange("Last 90 days", last(90, PantrixTimeUnit.Days)),
        PantrixQuickRange("Last 1 year", last(1, PantrixTimeUnit.Years))
    )
}
var range by remember { mutableStateOf(ranges[4].range) }
var showRecent by remember { mutableStateOf(true) }
val recent = rememberRecentRanges(max = 5)
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixSwitchButton(
        checked = showRecent,
        onCheckedChange = { showRecent = it },
        label = "Recently used",
        tagKey = "show_recent"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        commonlyUsed = ranges,
        recentlyUsed = recent.takeIf { showRecent },
        tagKey = "own_ranges"
    )
}

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

Width and height

The picker fills the width it is given up to the style's maxWidth (560dp by default, Elastic UI's "restricted"). maxWidth = Dp.Infinity gives the full width; in a narrow place the button in words is its icon alone; height makes the field compressed.

var range by remember { mutableStateOf(SINCE_YESTERDAY) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)) {
    PantrixText(text = "Default (up to 560dp)", style = PantrixTheme.typography.footnote)
    PantrixElasticDatePicker(range = range, onRangeChange = { range = it }, tagKey = "restricted")
    PantrixText(text = "Full width", style = PantrixTheme.typography.footnote)
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        style = ElasticDatePickerStyle(maxWidth = Dp.Infinity),
        tagKey = "full"
    )
    PantrixText(text = "In a narrow place (320dp)", style = PantrixTheme.typography.footnote)
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        modifier = Modifier.width(NARROW_WIDTH),
        tagKey = "narrow"
    )
    PantrixText(text = "Compressed (32dp)", style = PantrixTheme.typography.footnote)
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        style = ElasticDatePickerStyle(height = COMPRESSED_HEIGHT),
        tagKey = "compressed"
    )
}

private val SINCE_YESTERDAY = PantrixTimeRange(
    PantrixTimePoint.Relative(-1, PantrixTimeUnit.Days, roundTo = PantrixTimeUnit.Days),
    PantrixTimePoint.Now
)

private val NARROW_WIDTH = 320.dp

private val COMPRESSED_HEIGHT = 32.dp

Loading and disabled

A load and a picker that takes no choice. While loading, the button beside the field spins and takes no press; a disabled picker is drawn faint and opens no panel.

var range by remember { mutableStateOf(LAST_HOUR) }
var loading by remember { mutableStateOf(true) }
var enabled by remember { mutableStateOf(true) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixSwitchButton(
        checked = loading,
        onCheckedChange = { loading = it },
        label = "Loading",
        tagKey = "loading"
    )
    PantrixSwitchButton(
        checked = enabled,
        onCheckedChange = { enabled = it },
        label = "Enabled",
        tagKey = "enabled"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        loading = loading,
        enabled = enabled,
        tagKey = "states"
    )
}

private val LAST_HOUR = last(1, PantrixTimeUnit.Hours)

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

The days that can be chosen

The calendar offers this month's days alone (minDate and maxDate): on an end's Absolute tab the days outside the month are faint and cannot be chosen. A relative end is not bound by them.

val today = rememberToday()
val first = PantrixDate(today.year, today.month, 1)
val last = first.plusMonths(1).plusDays(-1)
var range by remember {
    mutableStateOf(PantrixTimeRange(PantrixTimePoint.Absolute(first.at(PantrixTime(0, 0))), PantrixTimePoint.Now))
}
PantrixElasticDatePicker(
    range = range,
    onRangeChange = { range = it },
    minDate = first,
    maxDate = last,
    tagKey = "bounds"
)

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

Clock, start of the week and units

How moments are written and read: 12 or 24 hours (is24Hour), the first day of the week (firstDayOfWeek, which sets the /w rounding too), and whether a relative end is put in the unit that reads best (roundRelativeUnits: 90 minutes "~ 2 hours ago" or "90 minutes ago"). The language comes from the page's language button.

var is24Hour by remember { mutableStateOf(true) }
var mondayFirst by remember { mutableStateOf(true) }
var roundUnits by remember { mutableStateOf(true) }
var range by remember { mutableStateOf(NINETY_TO_THIRTY_AGO) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixSwitchButton(checked = is24Hour, onCheckedChange = { is24Hour = it }, label = "24 hours", tagKey = "clock")
    PantrixSwitchButton(
        checked = mondayFirst,
        onCheckedChange = { mondayFirst = it },
        label = "The week starts on Monday",
        tagKey = "monday"
    )
    PantrixSwitchButton(
        checked = roundUnits,
        onCheckedChange = { roundUnits = it },
        label = "Round the units",
        tagKey = "round"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        roundRelativeUnits = roundUnits,
        firstDayOfWeek = if (mondayFirst) 1 else SUNDAY,
        is24Hour = is24Hour,
        tagKey = "clock"
    )
}

private val NINETY_TO_THIRTY_AGO = PantrixTimeRange(
    PantrixTimePoint.Relative(-90, PantrixTimeUnit.Minutes),
    PantrixTimePoint.Relative(-30, PantrixTimeUnit.Minutes)
)

private const val SUNDAY = 7

Date math in an address

The range stands in an address as date math: from and to are written again at each choice. With an end typed into the fields and Apply pressed, the range is read from them; an end that cannot be read draws its field as wrong.

var range by remember { mutableStateOf(SINCE_YESTERDAY) }
val written = range.toDateMath()
var from by remember(written) { mutableStateOf(written.first) }
var to by remember(written) { mutableStateOf(written.second) }
val start = PantrixTimePoint.parseDateMath(from)
val end = PantrixTimePoint.parseDateMath(to)
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixElasticDatePicker(range = range, onRangeChange = { range = it }, tagKey = "url")
    PantrixText(text = "?from=${written.first}&to=${written.second}", style = PantrixTheme.typography.footnote)
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
        verticalAlignment = Alignment.Bottom
    ) {
        PantrixTextField(
            value = from,
            onValueChange = { from = it },
            modifier = Modifier.weight(1f),
            label = "from",
            error = start == null,
            showCharacterCount = false,
            tagKey = "from"
        )
        PantrixTextField(
            value = to,
            onValueChange = { to = it },
            modifier = Modifier.weight(1f),
            label = "to",
            error = end == null,
            showCharacterCount = false,
            tagKey = "to"
        )
    }
    PantrixButton(
        text = "Apply",
        onClick = { if (start != null && end != null) range = PantrixTimeRange(start, end) },
        variant = ButtonVariant.Outline,
        enabled = start != null && end != null,
        tagKey = "read"
    )
}

private val SINCE_YESTERDAY = PantrixTimeRange(
    PantrixTimePoint.Relative(-1, PantrixTimeUnit.Days, roundTo = PantrixTimeUnit.Days),
    PantrixTimePoint.Now
)

Colours of its own

The field's own colours: its ground, edge, words and icons, the open end's edge, and the colours of an edit that waits and of ends out of order. Each colour not given is the theme's. Change an end and look at the waiting colour.

var range by remember { mutableStateOf(LAST_QUARTER) }
val colors = PantrixTheme.colors
PantrixElasticDatePicker(
    range = range,
    onRangeChange = { range = it },
    colors = ElasticDatePickerColors(
        container = colors.neutral.container,
        border = colors.neutral.color,
        active = colors.warning.color,
        pendingContainer = colors.info.container,
        pendingContent = colors.info.onContainer
    ),
    tagKey = "colors"
)

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

Auto refresh

The dashboard refreshes itself every ten seconds: the field ends in how often, written short, and a press on it opens the quick menu. The menu's "Refresh every" part turns it on and off and changes how often; nothing shorter than five seconds is taken (minAutoRefresh). onRefresh runs at each turn, and the next turn waits for it to end.

var range by remember { mutableStateOf(LAST_QUARTER) }
var refresh by remember { mutableStateOf(PantrixAutoRefresh(every = 10, paused = false)) }
var count by remember { mutableStateOf(0) }
val report = LocalExampleEvents.current
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        onRefresh = {
            count++
            report("Refreshed")
        },
        tagKey = "auto_refresh",
        autoRefresh = refresh,
        onAutoRefreshChange = {
            refresh = it
            report(if (it.paused) "Auto refresh stopped" else "Auto refresh changed")
        },
        minAutoRefresh = 5.seconds
    )
    PantrixText(text = "Refreshes: $count", style = PantrixTheme.typography.footnote)
}

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

Auto refresh only

Auto refresh alone, for a page with no time data (layout = AutoRefreshOnly): the field says "Auto refresh" and how often or off, and a press opens the menu that sets it alone. How often is given in minutes here (unit = AutoRefreshUnit.Minutes): once a minute.

var refresh by remember { mutableStateOf(PantrixAutoRefresh(every = 1, unit = AutoRefreshUnit.Minutes)) }
var count by remember { mutableStateOf(0) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixElasticDatePicker(
        range = LAST_QUARTER,
        onRangeChange = { count++ },
        tagKey = "auto_refresh_only",
        autoRefresh = refresh,
        onAutoRefreshChange = { refresh = it },
        layout = ElasticDatePickerLayout.AutoRefreshOnly
    )
    PantrixText(text = "Refreshes: $count", style = PantrixTheme.typography.footnote)
}

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

Quick select only

For a page that shows the range somewhere else, the quick menu's button alone with the button beside it (layout = QuickSelectOnly). The range chosen is written under it.

var range by remember { mutableStateOf(LAST_QUARTER) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        tagKey = "quick_select_only",
        layout = ElasticDatePickerLayout.QuickSelectOnly
    )
    Resolved(range)
}

@Composable
private fun Resolved(range: PantrixTimeRange) {
    val read = range.resolve()
    PantrixText(
        text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
        style = PantrixTheme.typography.footnote,
        color = PantrixTheme.colors.textSecondary
    )
}

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

@Composable
private fun moment(at: Instant): String {
    val strings = PantrixTheme.strings
    val day = PantrixDate.of(at)
    val time = PantrixTime.of(at)
    return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

Time window buttons

Elastic UI's time window buttons beside the field: ‹ and › move the range by its own length, zoom out widens it by zoomFactor of its length and zoom in narrows it by as much; zoom in is there only when it is asked for. The range they hand over is of moments.

var range by remember { mutableStateOf(LAST_HOUR) }
var zoomIn by remember { mutableStateOf(false) }
var factor by remember { mutableStateOf(HALF) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixSwitchButton(checked = zoomIn, onCheckedChange = { zoomIn = it }, label = "Zoom in", tagKey = "zoom_in")
    PantrixSegmentButton(
        options = listOf(
            SegmentOption(HALF, "Half (0.5)", tagKey = "half"),
            SegmentOption(QUARTER, "A quarter (0.25)", tagKey = "quarter")
        ),
        selected = factor,
        onSelect = { factor = it },
        fullWidth = false,
        tagKey = "zoom_factor"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        tagKey = "time_window",
        timeWindowButtons = ElasticTimeWindowButtons(zoomIn = zoomIn, zoomFactor = factor)
    )
    Resolved(range)
}

@Composable
private fun Resolved(range: PantrixTimeRange) {
    val read = range.resolve()
    PantrixText(
        text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
        style = PantrixTheme.typography.footnote,
        color = PantrixTheme.colors.textSecondary
    )
}

private val LAST_HOUR = last(1, PantrixTimeUnit.Hours)

private const val HALF = 0.5

private const val QUARTER = 0.25

@Composable
private fun moment(at: Instant): String {
    val strings = PantrixTheme.strings
    val day = PantrixDate.of(at)
    val time = PantrixTime.of(at)
    return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

The quick menu's own panels and order

A panel of the app's own in the quick menu (customQuickPanels), after the ranges used lately by default. "My own order" builds the menu with quickMenu: the saved ranges first, then the quick select, and no ranges offered by name. The menu's parts are in ElasticQuickMenuScope: quickMenu is called in that scope.

var range by remember { mutableStateOf(LAST_QUARTER) }
var own by remember { mutableStateOf(false) }
val saved = remember {
    listOf(
        "Night shift" to PantrixTimeRange(
            PantrixTimePoint.Relative(-1, PantrixTimeUnit.Days, roundTo = PantrixTimeUnit.Days),
            PantrixTimePoint.Relative(-16, PantrixTimeUnit.Hours)
        ),
        "Since the last deploy" to last(3, PantrixTimeUnit.Hours)
    )
}
val panel = ElasticQuickPanel("Saved ranges") {
    Column {
        saved.forEachIndexed { index, (name, kept) ->
            PantrixLink(text = name, onClick = { range = kept }, tagKey = "saved_$index")
        }
    }
}
val ownMenu: @Composable ElasticQuickMenuScope.() -> Unit = {
    CustomPanels()
    QuickSelect()
}
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixSegmentButton(
        options = listOf(
            SegmentOption(false, "Elastic UI's order", tagKey = "elastic"),
            SegmentOption(true, "My own order", tagKey = "own")
        ),
        selected = own,
        onSelect = { own = it },
        fullWidth = false,
        tagKey = "order"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        tagKey = "quick_panels",
        customQuickPanels = listOf(panel),
        quickMenu = if (own) ownMenu else null
    )
    Resolved(range)
}

@Composable
private fun Resolved(range: PantrixTimeRange) {
    val read = range.resolve()
    PantrixText(
        text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
        style = PantrixTheme.typography.footnote,
        color = PantrixTheme.colors.textSecondary
    )
}

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

@Composable
private fun moment(at: Instant): String {
    val strings = PantrixTheme.strings
    val day = PantrixDate.of(at)
    val time = PantrixTime.of(at)
    return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}

Time zone

The moments are read and written in the chosen time zone's difference (utcOffset); the ends' panels name the zone with its difference from UTC (timeZoneName). The name only names, and changes nothing.

var range by remember { mutableStateOf(SINCE_YESTERDAY) }
var zone by remember { mutableStateOf(ZONES.first()) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixSegmentButton(
        options = ZONES.mapIndexed { index, it -> SegmentOption(it, it.first, tagKey = "zone_$index") },
        selected = zone,
        onSelect = { zone = it },
        fullWidth = false,
        tagKey = "zone"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        tagKey = "time_zone",
        timeZoneName = zone.first,
        utcOffset = { zone.second }
    )
}

private val SINCE_YESTERDAY = PantrixTimeRange(
    PantrixTimePoint.Relative(-1, PantrixTimeUnit.Days, roundTo = PantrixTimeUnit.Days),
    PantrixTimePoint.Now
)

private val ZONES = listOf("Europe/Istanbul" to 3.hours, "Asia/Tokyo" to 9.hours, "UTC" to Duration.ZERO)

Disabled, with its own words

A picker that takes no choice can show its own words in the place of its range (disabledText), as Elastic UI's isDisabled.display does: here "All time".

var words by remember { mutableStateOf(true) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixSwitchButton(checked = words, onCheckedChange = { words = it }, label = "Its own words", tagKey = "words")
    PantrixElasticDatePicker(
        range = LAST_QUARTER,
        onRangeChange = {},
        enabled = false,
        tagKey = "disabled_text",
        disabledText = if (words) "All time" else null
    )
}

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

The KQL pattern

Elastic UI's KQL pattern: the picker beside a search field. While the search field has the focus the picker shrinks to the quick menu's button (layout = QuickSelectOnly) and the room is the query's; when the focus leaves, the range comes back.

var range by remember { mutableStateOf(LAST_QUARTER) }
var query by remember { mutableStateOf("") }
var searching by remember { mutableStateOf(false) }
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    verticalAlignment = Alignment.CenterVertically
) {
    PantrixTextField(
        value = query,
        onValueChange = { query = it },
        modifier = Modifier.weight(1f).onFocusChanged { searching = it.hasFocus },
        placeholder = "Search with KQL",
        showCharacterCount = false,
        contentDescription = "Search query",
        tagKey = "query"
    )
    PantrixElasticDatePicker(
        range = range,
        onRangeChange = { range = it },
        modifier = if (searching) Modifier else Modifier.weight(PICKER_SHARE),
        updateButton = ElasticDatePickerUpdateButton.Icon,
        tagKey = "query_range",
        layout = if (searching) ElasticDatePickerLayout.QuickSelectOnly else ElasticDatePickerLayout.Full
    )
}

private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)

private const val PICKER_SHARE = 1.4f

private fun last(
    amount: Int,
    unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)

API

range

A range of time that can move with the clock, as a dashboard is filtered by: "the last 15 minutes" is still the last 15 minutes at the next refresh. Elastic UI's EuiSuperDatePicker, built again on Pantrix.

ParameterTypeDefaultWhat it is
rangePantrixTimeRangethe range on show. Its ends can be relative to now.
onRangeChange(PantrixTimeRange) -> Unithanded the range the reader chose from the quick menu or updated after an edit, or the same range on a refresh without onRefresh.
modifierModifierModifierthe picker's modifier. The picker fills the width it is given, up to the style's maxWidth.
commonlyUsedList<PantrixQuickRange>PantrixQuickRange.commonlyUsed(PantrixTheme.strings)the ranges the quick menu offers by name: by default Elastic UI's eight, in the theme's language. A range on show that is one of these is called by its name.
recentlyUsedPantrixRecentRanges?nullthe ranges the reader chose lately, which the quick menu lists and each range the reader applies is recorded in; null for none. rememberRecentRanges() keeps them across a configuration change.
updateButtonElasticDatePickerUpdateButtonElasticDatePickerUpdateButton.Textthe button beside the field: its words and icon, its icon alone, or none, every edit then handed over at once.
presentationDatePickerPresentationDatePickerPresentation.Adaptivewhat the panels open in: by default a sheet in a narrow window and a popover beside the part pressed in a wider one, chosen as a panel opens; or always a popover, a sheet or a dialog. The same on every platform.
loadingBooleanfalsewhether what the range filters loads: the button beside the field spins and takes no press.
enabledBooleantruewhether the picker takes a choice at all. One that takes none is drawn faint and opens nothing.
minDatePantrixDate?nullthe first day an end's calendar offers, or null for none so early.
maxDatePantrixDate?nullthe last day it offers, or null for none so late.
onRefresh(suspend () -> Unit)?nullwhat Refresh does where no edit waits, the button spinning while it runs; null to hand the same range to onRangeChange.
roundRelativeUnitsBooleantruewhether an end that is an amount from now is put in the unit that reads best, "~ 2 hours ago" for 90 minutes, or keeps its own.
firstDayOfWeekInt?nullthe day the calendar's weeks start on and a week is rounded to, as ISO 8601 numbers the days: 1 is Monday. Null is the language's, PantrixLocale.firstDayOfWeek.
is24HourBooleantruewhether a moment is written and chosen by a clock of twenty-four hours, 22:00, or of twelve, 10:00 PM.
styleElasticDatePickerStyle?nullthis call's own style of the field: over every enclosing one and under the density.
colorsElasticDatePickerColors?nullthe field's own colours: the ones it sets, with the theme's for the rest.
tagKeyString?nullthe picker's automation key: it is ElasticDatePicker_<tagKey>, its parts .Button_quick_menu, .Button_name, .Button_start, .Button_end, .Button_auto_refresh, the time window buttons .Button_window_previous, .Button_window_zoom_in, .Button_window_zoom_out and .Button_window_next, and the button beside the field Button_<tagKey>_update.
autoRefreshPantrixAutoRefresh?nullhow often the range is read again by itself, or null for never. While it is not paused the field ends in how often, written short, and Refresh is done at each turn: onRefresh, waited for before the next turn is counted, or the same range handed to onRangeChange. An onRefresh that fails pauses it, and says so through onAutoRefreshChange.
onAutoRefreshChange((PantrixAutoRefresh) -> Unit)?nullhanded the refresh by itself as the reader sets it in the quick menu, where it has a part of its own; null for no such part.
minAutoRefreshDurationDuration.ZEROthe shortest refresh by itself the reader can set: a shorter one draws its amount as wrong.
layoutElasticDatePickerLayoutElasticDatePickerLayout.Fullwhat the field shows: the range, the quick menu's button alone, or the refresh by itself alone.
timeWindowButtonsElasticTimeWindowButtons?nullElastic UI's buttons beside the field that move the range by its length and zoom it, or null for none.
customQuickPanelsList<ElasticQuickPanel>emptyList()panels of the caller's own in the quick menu, after the ranges used lately.
quickMenu(@Composable ElasticQuickMenuScope.() -> Unit)?nullthe quick menu's parts in an order of the caller's own, some left out or panels of its own put in among them; null for Elastic UI's order.
timeZoneNameString?nullthe time zone the moments are in, named in the ends' panels with its difference from UTC: "Time zone: Europe/Istanbul (UTC+03:00)". It names, and changes nothing; null for no such line.
utcOffset((Instant) -> Duration)?nullthe difference from UTC the moments are read and written in at a moment; null for the screen's clock's (LocalPantrixClock), the device's unless a test or a preview gives another.
disabledTextString?nullwhat a picker that takes no choice shows in the place of its range; null for the range.

ElasticDatePickerStyle

PantrixElasticDatePicker's field: how tall it is, its corners, the room inside it, the gap between it and the button beside it, and how wide it gets.

ParameterTypeDefaultWhat it is
heightDpDp.UnspecifiedHow tall the field is.
shapeShapeSpecShapeSpec.UnspecifiedThe field's corners.
paddingDpDp.UnspecifiedThe room between the field's edge and what is in it, at either end.
gapDpDp.UnspecifiedBetween the field and the button beside it, and between the parts in the field.
maxWidthDpDp.UnspecifiedThe widest the field and its button get; Dp.Infinity for as wide as they are given.

ElasticDatePickerColors

What a PantrixElasticDatePicker's field is painted with, in place of the theme's colours: a colour that is set is used, and one that is left out stays the theme's. What the field opens is painted with the colours of a popover, a sheet, a calendar and tabs.

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe field's ground. The theme's surface, as a text field's.
borderColorColor.UnspecifiedThe field's edge, and the line between the quick menu's button and the range. The theme's border.
contentColorColor.UnspecifiedThe words of the range and of its ends. The theme's textPrimary.
iconColorColor.UnspecifiedThe field's icons: the quick menu's calendar and the arrow between the ends. The theme's textSecondary.
activeColorColor.UnspecifiedThe edge drawn round the end whose panel is open. The primary role's color.
pendingContainerColorColor.UnspecifiedThe field's ground while an edit waits for Update. The success role's container.
pendingContentColorColor.UnspecifiedThe words, the arrow and the mark on it. The success role's onContainer.
invalidContainerColorColor.UnspecifiedThe field's ground while its end comes before its start. The danger role's container.
invalidContentColorColor.UnspecifiedThe words, the arrow and the mark on it. The danger role's onContainer.
disabledColorColor.UnspecifiedThe words and icons of a field that takes no choice. The theme's textDisabled.

Accessibility