Components
Calendar
A month-grid date picker with real date math. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add calendarpnpm dlx kosmesis add calendaryarn dlx kosmesis add calendarbunx kosmesis add calendarCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, Emit, FunctionProp, Prop, State } from "@praxisjs/decorators";import { cn } from "@/lib/utils";const WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];const MONTH_LABELS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December",];function isSameDay(a: Date, b: Date): boolean { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();}function startOfMonth(year: number, month: number): Date { return new Date(year, month, 1);}/** Builds a 6-row (42-cell) month grid, including the trailing/leading days of adjacent months. */function buildMonthGrid(year: number, month: number): Array<{ date: Date; inMonth: boolean }> { const first = startOfMonth(year, month); const gridStart = new Date(year, month, 1 - first.getDay()); return Array.from({ length: 42 }, (_, i) => { const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i); return { date, inMonth: date.getMonth() === month }; });}export interface CalendarStateProps { defaultMonth?: Date; selected?: Date; onSelect?: (date: Date) => void; disabled?: (date: Date) => boolean;}/** * Purely presentational + date math — no Morphos equivalent (Radix has no calendar primitive * either). `CalendarState` owns the visible month and selection; `Calendar` renders the grid it * computes. Both instantiated directly + rendered, the same pattern as every other compound * component here. */@Component()export class CalendarState extends StatefulComponent { @Prop() defaultMonth?: Date; @Prop() selected?: Date; // `@Prop()`'s getter auto-invokes any raw value that is itself a function, treating it as a // reactive `() => T` binding (see `selected` in the Calendar stories, which relies on exactly // that to stay controlled). `onSelect`/`disabled` are real callbacks, not value-returning // thunks — `@FunctionProp()` returns them as-is instead of calling them. @FunctionProp() onSelect?: CalendarStateProps["onSelect"]; @FunctionProp() disabled?: CalendarStateProps["disabled"]; @State() _viewYear = 0; @State() _viewMonth = 0; @State() _selected: Date | undefined = undefined; onBeforeMount() { const base = this.defaultMonth ?? this.selected ?? new Date(); this._viewYear = base.getFullYear(); this._viewMonth = base.getMonth(); this._selected = this.selected; } get monthLabel(): string { return `${MONTH_LABELS[this._viewMonth]} ${String(this._viewYear)}`; } get weekdayLabels(): string[] { return WEEKDAY_LABELS; } get grid(): Array<{ date: Date; inMonth: boolean }> { return buildMonthGrid(this._viewYear, this._viewMonth); } get selectedDate(): Date | undefined { return this.selected ?? this._selected; } isSelected(date: Date): boolean { const selected = this.selectedDate; return selected !== undefined && isSameDay(date, selected); } isToday(date: Date): boolean { return isSameDay(date, new Date()); } isDisabled(date: Date): boolean { return this.disabled?.(date) ?? false; } goToPrevMonth(): void { const prev = new Date(this._viewYear, this._viewMonth - 1, 1); this._viewYear = prev.getFullYear(); this._viewMonth = prev.getMonth(); } goToNextMonth(): void { const next = new Date(this._viewYear, this._viewMonth + 1, 1); this._viewYear = next.getFullYear(); this._viewMonth = next.getMonth(); } @Emit("onSelect") select(date: Date): Date { if (this.isDisabled(date)) return this.selectedDate ?? date; if (this.selected === undefined) this._selected = date; return date; } /** Pure state container — never mounted via JSX, only instantiated directly. */ render() { return null; }}export interface CalendarProps { state: CalendarState; class?: string;}@Component()export class Calendar extends StatelessComponent<CalendarProps> { render() { const { state, class: cls } = this.props; return ( <div class={cn("w-fit rounded-md border bg-background p-3", cls)}> <div class="mb-4 flex items-center justify-between"> <button type="button" aria-label="Previous month" class="inline-flex size-7 items-center justify-center rounded-md border hover:bg-accent hover:text-accent-foreground" onClick={() => { state.goToPrevMonth(); }} > ‹ </button> <span class="text-sm font-medium">{() => state.monthLabel}</span> <button type="button" aria-label="Next month" class="inline-flex size-7 items-center justify-center rounded-md border hover:bg-accent hover:text-accent-foreground" onClick={() => { state.goToNextMonth(); }} > › </button> </div> <div class="grid grid-cols-7 gap-1 text-center text-xs text-muted-foreground"> {state.weekdayLabels.map((label) => ( <div key={label} class="flex h-8 items-center justify-center font-normal"> {label} </div> ))} </div> <div class="grid grid-cols-7 gap-1"> {() => state.grid.map(({ date, inMonth }) => ( <button key={date.toISOString()} type="button" disabled={state.isDisabled(date)} data-selected={state.isSelected(date) ? "" : undefined} data-today={state.isToday(date) ? "" : undefined} data-outside-month={!inMonth ? "" : undefined} class={cn( "flex size-8 items-center justify-center rounded-md p-0 text-sm font-normal text-foreground", "hover:bg-accent hover:text-accent-foreground", "data-outside-month:text-muted-foreground data-outside-month:opacity-50", "data-selected:bg-primary data-selected:text-primary-foreground data-selected:hover:bg-primary data-selected:hover:text-primary-foreground", "data-today:border data-today:border-input", "disabled:pointer-events-none disabled:opacity-30", )} onClick={() => { state.select(date); }} > {date.getDate()} </button> )) } </div> </div> ); }}Install the following dependencies:
npm install @praxisjs/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Emit, FunctionProp, Prop, State } from "@praxisjs/decorators";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);const WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];const MONTH_LABELS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December",];function isSameDay(a: Date, b: Date): boolean { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();}function startOfMonth(year: number, month: number): Date { return new Date(year, month, 1);}/** Builds a 6-row (42-cell) month grid, including the trailing/leading days of adjacent months. */function buildMonthGrid(year: number, month: number): Array<{ date: Date; inMonth: boolean }> { const first = startOfMonth(year, month); const gridStart = new Date(year, month, 1 - first.getDay()); return Array.from({ length: 42 }, (_, i) => { const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i); return { date, inMonth: date.getMonth() === month }; });}export interface CalendarStateProps { defaultMonth?: Date; selected?: Date; onSelect?: (date: Date) => void; disabled?: (date: Date) => boolean;}/** * Purely presentational + date math — no Morphos equivalent (Radix has no calendar primitive * either). `CalendarState` owns the visible month and selection; `Calendar` renders the grid it * computes. Both instantiated directly + rendered, the same pattern as every other compound * component here. */@Component()export class CalendarState extends StatefulComponent { @Prop() defaultMonth?: Date; @Prop() selected?: Date; // `@Prop()`'s getter auto-invokes any raw value that is itself a function, treating it as a // reactive `() => T` binding (see `selected` in the Calendar stories, which relies on exactly // that to stay controlled). `onSelect`/`disabled` are real callbacks, not value-returning // thunks — `@FunctionProp()` returns them as-is instead of calling them. @FunctionProp() onSelect?: CalendarStateProps["onSelect"]; @FunctionProp() disabled?: CalendarStateProps["disabled"]; @State() _viewYear = 0; @State() _viewMonth = 0; @State() _selected: Date | undefined = undefined; onBeforeMount() { const base = this.defaultMonth ?? this.selected ?? new Date(); this._viewYear = base.getFullYear(); this._viewMonth = base.getMonth(); this._selected = this.selected; } get monthLabel(): string { return `${MONTH_LABELS[this._viewMonth]} ${String(this._viewYear)}`; } get weekdayLabels(): string[] { return WEEKDAY_LABELS; } get grid(): Array<{ date: Date; inMonth: boolean }> { return buildMonthGrid(this._viewYear, this._viewMonth); } get selectedDate(): Date | undefined { return this.selected ?? this._selected; } isSelected(date: Date): boolean { const selected = this.selectedDate; return selected !== undefined && isSameDay(date, selected); } isToday(date: Date): boolean { return isSameDay(date, new Date()); } isDisabled(date: Date): boolean { return this.disabled?.(date) ?? false; } goToPrevMonth(): void { const prev = new Date(this._viewYear, this._viewMonth - 1, 1); this._viewYear = prev.getFullYear(); this._viewMonth = prev.getMonth(); } goToNextMonth(): void { const next = new Date(this._viewYear, this._viewMonth + 1, 1); this._viewYear = next.getFullYear(); this._viewMonth = next.getMonth(); } @Emit("onSelect") select(date: Date): Date { if (this.isDisabled(date)) return this.selectedDate ?? date; if (this.selected === undefined) this._selected = date; return date; } /** Pure state container — never mounted via JSX, only instantiated directly. */ render() { return null; }}class CalendarStyles extends Stylesheet { $root = this.css({ width: "fit-content", borderRadius: `calc(${t.radius} - 2px)`, border: `1px solid ${t.border}`, backgroundColor: t.background, padding: "0.75rem" }); $navHeader = this.css({ marginBottom: "1rem", display: "flex", alignItems: "center", justifyContent: "space-between" }); $navButton = this.css({ display: "inline-flex", width: "1.75rem", height: "1.75rem", alignItems: "center", justifyContent: "center", borderRadius: `calc(${t.radius} - 2px)`, border: `1px solid ${t.border}`, }).hover({ backgroundColor: t.accent, color: t.accentForeground }); $monthLabel = this.css({ fontSize: "0.875rem", fontWeight: 500 }); $weekdayRow = this.css({ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: "0.25rem", textAlign: "center", fontSize: "0.75rem", color: t.mutedForeground }); $weekday = this.css({ display: "flex", height: "2rem", alignItems: "center", justifyContent: "center", fontWeight: 400 }); $grid = this.css({ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: "0.25rem" }); $day = this.css({ display: "flex", width: "2rem", height: "2rem", alignItems: "center", justifyContent: "center", borderRadius: `calc(${t.radius} - 2px)`, padding: "0", fontSize: "0.875rem", fontWeight: 400, color: t.foreground, }) .hover({ backgroundColor: t.accent, color: t.accentForeground }) .on("&[data-outside-month]", { color: t.mutedForeground, opacity: 0.5 }) .on("&[data-selected]", { backgroundColor: t.primary, color: t.primaryForeground }) .on("&[data-selected]:hover", { backgroundColor: t.primary, color: t.primaryForeground }) .on("&[data-today]", { border: `1px solid ${t.input}` }) .disabled({ pointerEvents: "none", opacity: 0.3 });}export interface CalendarProps { state: CalendarState; class?: string;}@Component()export class Calendar extends StatelessComponent<CalendarProps> { @Styled(CalendarStyles) $s!: CalendarStyles; render() { const { state, class: cls } = this.props; return ( <div class={cx(this.$s.$root, cls)}> <div class={this.$s.$navHeader}> <button type="button" aria-label="Previous month" class={this.$s.$navButton} onClick={() => { state.goToPrevMonth(); }}> ‹ </button> <span class={this.$s.$monthLabel}>{() => state.monthLabel}</span> <button type="button" aria-label="Next month" class={this.$s.$navButton} onClick={() => { state.goToNextMonth(); }}> › </button> </div> <div class={this.$s.$weekdayRow}> {state.weekdayLabels.map((label) => ( <div key={label} class={this.$s.$weekday}> {label} </div> ))} </div> <div class={this.$s.$grid}> {() => state.grid.map(({ date, inMonth }) => ( <button key={date.toISOString()} type="button" disabled={state.isDisabled(date)} data-selected={state.isSelected(date) ? "" : undefined} data-today={state.isToday(date) ? "" : undefined} data-outside-month={!inMonth ? "" : undefined} class={this.$s.$day} onClick={() => { state.select(date); }} > {date.getDate()} </button> )) } </div> </div> ); }}Examples
Usage
import { Calendar, CalendarState } from "@/components/ui/calendar";
@Component()
class BookingCalendar extends StatefulComponent {
@State() calendarState = new CalendarState({
onSelect: (date) => console.log(date),
});
render() {
return <Calendar state={this.calendarState} />;
}
}Props (CalendarState)
| Prop | Type | Default |
|---|---|---|
defaultMonth | Date | today |
selected | Date | — (controlled) |
onSelect | (date: Date) => void | — |
disabled | (date: Date) => boolean | — |
For a Popover-triggered variant, see Date Picker.