Date Time Picker
A Popover + Calendar + time columns composition for picking a date and time, or a date-time range.
Installation
npx kosmesis add date-time-pickerpnpm dlx kosmesis add date-time-pickeryarn dlx kosmesis add date-time-pickerbunx kosmesis add date-time-pickerInstall the following dependencies:
npm install @morphos/overlays @morphos/iconspnpm add @morphos/overlays @morphos/iconsyarn add @morphos/overlays @morphos/iconsbun add @morphos/overlays @morphos/iconsThis component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button, calendar, popover, time-picker.
Copy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { type Popover, PopoverTrigger } from "@morphos/overlays";import { buttonVariants } from "./button";import { Calendar, type CalendarState } from "./calendar";import { PopoverContent } from "./popover";import { TimePickerPanel, type TimePickerState } from "./time-picker";import { cn } from "@/lib/utils";export interface DateTimePickerProps { popover: Popover; calendar: CalendarState; time: TimePickerState; placeholder?: string; class?: string;}@Component()export class DateTimePicker extends StatelessComponent<DateTimePickerProps> { render() { const { popover, calendar, time, placeholder = "Pick date and time", class: cls } = this.props; return ( <> <PopoverTrigger popover={popover} class={cn(buttonVariants({ variant: "outline" }), "justify-start font-normal", cls)}> <Icon name="Calendar" size={14} /> {() => { const date = calendar.formattedDate; if (!date) return <span class="text-muted-foreground">{placeholder}</span>; return time.formattedValue ? `${date}, ${time.formattedValue}` : date; }} </PopoverTrigger> <PopoverContent popover={popover} class="flex w-auto p-0"> <Calendar state={calendar} class="border-none shadow-none" /> <TimePickerPanel state={time} class="rounded-none border-0 border-l" /> </PopoverContent> </> ); }}export interface DateTimeRangePickerProps { popover: Popover; /** Must be created with `mode: "range"`. */ calendar: CalendarState; startTime: TimePickerState; endTime: TimePickerState; placeholder?: string; class?: string;}@Component()export class DateTimeRangePicker extends StatelessComponent<DateTimeRangePickerProps> { render() { const { popover, calendar, startTime, endTime, placeholder = "Pick a date and time range", class: cls } = this.props; return ( <> <PopoverTrigger popover={popover} class={cn(buttonVariants({ variant: "outline" }), "justify-start font-normal", cls)}> <Icon name="Calendar" size={14} /> {() => { const { from, to } = calendar.range; if (!from) return <span class="text-muted-foreground">{placeholder}</span>; const start = [from.toLocaleDateString(), startTime.formattedValue].filter(Boolean).join(", "); const end = to ? [to.toLocaleDateString(), endTime.formattedValue].filter(Boolean).join(", ") : "…"; return `${start} – ${end}`; }} </PopoverTrigger> <PopoverContent popover={popover} class="flex w-auto p-0"> <Calendar state={calendar} class="border-none shadow-none" /> <div class="flex flex-col gap-1 border-l p-2"> <span class="px-1 text-xs font-medium text-muted-foreground">Start</span> <TimePickerPanel state={startTime} class="border-0 p-0" /> </div> <div class="flex flex-col gap-1 border-l p-2"> <span class="px-1 text-xs font-medium text-muted-foreground">End</span> <TimePickerPanel state={endTime} class="border-0 p-0" /> </div> </PopoverContent> </> ); }}Install the following dependencies:
npm install @morphos/overlays @morphos/icons @praxisjs/csspnpm add @morphos/overlays @morphos/icons @praxisjs/cssyarn add @morphos/overlays @morphos/icons @praxisjs/cssbun add @morphos/overlays @morphos/icons @praxisjs/cssThis component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button, calendar, popover, time-picker.
Copy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { type Popover, PopoverTrigger } from "@morphos/overlays";import { ButtonStyles } from "./button";import { Calendar, type CalendarState } from "./calendar";import { PopoverContent } from "./popover";import { TimePickerPanel, type TimePickerState } from "./time-picker";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class DateTimePickerStyles extends Stylesheet { $trigger = this.css({ justifyContent: "flex-start", fontWeight: 400 }); $placeholder = this.css({ color: t.mutedForeground }); $content = this.css({ display: "flex", width: "auto", padding: "0" }); $flush = this.css({ border: "none !important" }); $timeFlush = this.css({ border: "none !important", borderRadius: "0", borderLeft: `1px solid ${t.border} !important` }); $section = this.css({ display: "flex", flexDirection: "column", gap: "0.25rem", padding: "0.5rem", borderLeft: `1px solid ${t.border}` }); $sectionLabel = this.css({ padding: "0 0.25rem", fontSize: "0.75rem", fontWeight: 500, color: t.mutedForeground }); $sectionPanel = this.css({ border: "none !important", padding: "0" });}export interface DateTimePickerProps { popover: Popover; calendar: CalendarState; time: TimePickerState; placeholder?: string; class?: string;}@Component()export class DateTimePicker extends StatelessComponent<DateTimePickerProps> { @Styled(ButtonStyles) $btn!: ButtonStyles; @Styled(DateTimePickerStyles) $s!: DateTimePickerStyles; render() { const { popover, calendar, time, placeholder = "Pick date and time", class: cls } = this.props; return ( <> <PopoverTrigger popover={popover} class={cx(this.$btn.$root, this.$btn.$variantOutline, this.$btn.$sizeDefault, this.$s.$trigger, cls)} > <Icon name="Calendar" size={14} /> {() => { const date = calendar.formattedDate; if (!date) return <span class={this.$s.$placeholder}>{placeholder}</span>; return time.formattedValue ? `${date}, ${time.formattedValue}` : date; }} </PopoverTrigger> <PopoverContent popover={popover} class={this.$s.$content}> <Calendar state={calendar} class={this.$s.$flush} /> <TimePickerPanel state={time} class={this.$s.$timeFlush} /> </PopoverContent> </> ); }}export interface DateTimeRangePickerProps { popover: Popover; /** Must be created with `mode: "range"`. */ calendar: CalendarState; startTime: TimePickerState; endTime: TimePickerState; placeholder?: string; class?: string;}@Component()export class DateTimeRangePicker extends StatelessComponent<DateTimeRangePickerProps> { @Styled(ButtonStyles) $btn!: ButtonStyles; @Styled(DateTimePickerStyles) $s!: DateTimePickerStyles; render() { const { popover, calendar, startTime, endTime, placeholder = "Pick a date and time range", class: cls } = this.props; return ( <> <PopoverTrigger popover={popover} class={cx(this.$btn.$root, this.$btn.$variantOutline, this.$btn.$sizeDefault, this.$s.$trigger, cls)} > <Icon name="Calendar" size={14} /> {() => { const { from, to } = calendar.range; if (!from) return <span class={this.$s.$placeholder}>{placeholder}</span>; const start = [from.toLocaleDateString(), startTime.formattedValue].filter(Boolean).join(", "); const end = to ? [to.toLocaleDateString(), endTime.formattedValue].filter(Boolean).join(", ") : "…"; return `${start} – ${end}`; }} </PopoverTrigger> <PopoverContent popover={popover} class={this.$s.$content}> <Calendar state={calendar} class={this.$s.$flush} /> <div class={this.$s.$section}> <span class={this.$s.$sectionLabel}>Start</span> <TimePickerPanel state={startTime} class={this.$s.$sectionPanel} /> </div> <div class={this.$s.$section}> <span class={this.$s.$sectionLabel}>End</span> <TimePickerPanel state={endTime} class={this.$s.$sectionPanel} /> </div> </PopoverContent> </> ); }}Examples
Usage
import { Popover } from "@morphos/overlays";
import { CalendarState } from "@/components/ui/calendar";
import { DateTimePicker } from "@/components/ui/date-time-picker";
import { TimePickerState } from "@/components/ui/time-picker";
@Component()
class EventForm extends StatefulComponent {
@State() popover = new Popover();
@State() calendar = new CalendarState();
@State() time = new TimePickerState({ minuteStep: 5 });
onBeforeMount() {
this.popover.onBeforeMount();
this.calendar.onBeforeMount();
this.time.onBeforeMount();
}
render() {
return <DateTimePicker popover={this.popover} calendar={this.calendar} time={this.time} />;
}
}For a range, create the calendar with mode: "range" and use DateTimeRangePicker with a
startTime and an endTime state. Unlike Date Picker, the popover
stays open after a selection, since both a date and a time need to be chosen.
Props
DateTimePicker
| Prop | Type | Default |
|---|---|---|
popover | Popover | — required |
calendar | CalendarState | — required |
time | TimePickerState | — required |
placeholder | string | "Pick date and time" |
class | string | — |
DateTimeRangePicker
| Prop | Type | Default |
|---|---|---|
popover | Popover | — required |
calendar | CalendarState | — required, mode: "range" |
startTime | TimePickerState | — required |
endTime | TimePickerState | — required |
placeholder | string | "Pick a date and time range" |
class | string | — |