Date Picker

Styled date picker combining trigger, popover and calendar, built on the @foldkit/ui DatePicker submodel.

Preview
Style
src/demo/views/date-picker.ts76 lines
import { Update } from 'foldkit'
import { Calendar as FoldkitCalendar } from 'foldkit'
import { Match as M, Option } from 'effect'
import { Schema as S } from 'effect'
import { evo } from 'foldkit/struct'
import { defineMessageUnion } from 'foldkit/message'
import type { Html, HtmlBuilder } from 'foldkit/html'

import * as datePicker from '../../generated/registry/ui/date-picker'

import { DEMO_TODAY } from '../bundles'
import { defineSlice, type UpdateReturn } from '../slice'
import type { Model, Message as AppMessage } from '../assemble'

const Message = defineMessageUnion({
  GotDatePickerMessage: { message: datePicker.Message },
})

export const datePickerView = (model: Model, h: HtmlBuilder<AppMessage>): Html =>
  h.submodel({
    slotId: model.datePicker.id,
    model: model.datePicker,
    view: datePicker.view,
    viewInputs: datePicker.styledViewInputs({ maybeSelectedDate: model.maybePickedDate }, h),
    toParentMessage: (message) => Message.GotDatePickerMessage({ message }),
  })

const foldDatePickerOutMessage = M.type<datePicker.OutMessage>().pipe(
  M.withReturnType<Update.Step<State, unknown>>(),
  M.tagsExhaustive({
    SelectedDate:
      ({ date }) =>
      (model) => ({ model: evo(model, { maybePickedDate: () => Option.some(date) }) }),
    ClearedDate: () => (model) => ({ model: evo(model, { maybePickedDate: () => Option.none() }) }),
    ChangedViewMonth: () => (model) => ({ model }),
  }),
)

const foldDatePicker = Update.foldChild({
  update: datePicker.update,
  read: (model: State) => Option.some(model.datePicker),
  write: (model, next) => evo(model, { datePicker: () => next }),
  toParentMessage: (message) => Message.GotDatePickerMessage({ message }),
  foldOutMessage: foldDatePickerOutMessage,
})

const fields = {
  datePicker: datePicker.Model,
  maybePickedDate: S.Option(FoldkitCalendar.CalendarDate),
}

const stateSchema = S.Struct(fields)
type State = typeof stateSchema.Type

export const slice = defineSlice({
  fields,
  init: {
    datePicker: datePicker.init({
      id: 'date-picker-demo',
      today: DEMO_TODAY,
      minDate: FoldkitCalendar.subtractYears(DEMO_TODAY, 1),
      maxDate: FoldkitCalendar.addYears(DEMO_TODAY, 1),
    }),
    maybePickedDate: Option.none(),
  },
  messages: [Message.GotDatePickerMessage],
  handlers: (model: State) => ({
    GotDatePickerMessage: (payload: typeof Message.GotDatePickerMessage.Type): UpdateReturn =>
      foldDatePicker(model, payload.message),
  }),
  samples: [],
  // Date selection flows through the submodel's out-messages; the public
  // @foldkit/ui namespace exports no child-message constructors, so there
  // are no top-level samples to feed update().
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/date-picker

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/date-picker.ts105 lines
/** Stateful submodel — import the whole module as a namespace and wire its
 *  Model/Message/init/update into your app:
 *  `import * as DatePicker from '@/components/ui/date-picker'`
 */
import { Option } from 'effect'
import { DatePicker as FoldkitDatePicker } from '@foldkit/ui'
import type { AnchorConfig } from '@foldkit/ui/anchor'
import type { CalendarDate } from 'foldkit/calendar'
import type { HtmlBuilder } from 'foldkit/html'

import { icon } from '@/lib/icons'
import { ChevronDown } from 'lucide'
import { cn } from '@/lib/utils'
import { childAttributes } from 'foldkit/html'
import { calendarToView } from '@/ui/calendar'

// Re-export the @foldkit/ui DatePicker submodel surface.

export const init = (config: InitConfig): Model =>
  FoldkitDatePicker.init({ isAnimated: true, ...config })
export const update = FoldkitDatePicker.update
export const view = FoldkitDatePicker.view
export const Model = FoldkitDatePicker.Model
export type Model = typeof Model.Type
export const Message = FoldkitDatePicker.Message
export type Message = typeof Message.Type
export const OutMessage = FoldkitDatePicker.OutMessage
export type OutMessage = typeof OutMessage.Type
export const triggerId = FoldkitDatePicker.triggerId

export type InitConfig = FoldkitDatePicker.InitConfig
export type ViewInputs = FoldkitDatePicker.ViewInputs

export const datePickerTriggerClass =
  "flex h-10 min-w-48 items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-sm font-medium text-foreground shadow-sm outline-none transition-[color,box-shadow] hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"

export const datePickerPanelClass =
  'z-50 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[placement=bottom]:slide-in-from-top-2 data-[placement=left]:slide-in-from-right-2 data-[placement=right]:slide-in-from-left-2 data-[placement=top]:slide-in-from-bottom-2'

export const datePickerPanelAnimatedClass =
  'z-50 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[placement=bottom]:slide-in-from-top-2 data-[placement=left]:slide-in-from-right-2 data-[placement=right]:slide-in-from-left-2 data-[placement=top]:slide-in-from-bottom-2 data-[enter]:animate-in data-[enter]:fade-in-0 data-[enter]:zoom-in-95 data-[leave]:animate-out data-[leave]:fade-out-0 data-[leave]:zoom-out-95'

export const datePickerBackdropClass = 'fixed inset-0 z-0'

export const datePickerWrapperClass = 'relative inline-block'

export const datePickerPlaceholderClass = 'text-muted-foreground'

export const DATE_PICKER_ANCHOR: AnchorConfig = {
  placement: 'bottom-start',
  gap: 4,
  padding: 8,
}

const formatTriggerLabel = (date: Readonly<{ year: number; month: number; day: number }>): string =>
  `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`

export type StyledViewInputs = Readonly<{
  maybeSelectedDate: Option.Option<CalendarDate>
  anchor?: AnchorConfig
  isDisabled?: boolean
  isAnimated?: boolean
  name?: string
  className?: string
  triggerClass?: string
  panelClass?: string
  backdropClass?: string
  wrapperClass?: string
}>

/** Build styled `DatePicker.ViewInputs`. Pass your view's `h`. The trigger
 *  face shows the ISO date or a placeholder; the popover panel renders the
 *  styled calendar grid from the calendar item. */
export const styledViewInputs = <M>(
  viewInputs: StyledViewInputs,
  h: HtmlBuilder<M>,
): ViewInputs => ({
  anchor: viewInputs.anchor ?? DATE_PICKER_ANCHOR,
  maybeSelectedDate: viewInputs.maybeSelectedDate,
  isDisabled: viewInputs.isDisabled,
  name: viewInputs.name,
  className: cn(datePickerWrapperClass, viewInputs.wrapperClass),
  attributes: childAttributes([h.DataAttribute('slot', 'date-picker')]),
  triggerClassName: cn(datePickerTriggerClass, viewInputs.triggerClass),
  triggerAttributes: childAttributes([h.DataAttribute('slot', 'date-picker-trigger')]),
  panelClassName: cn(
    viewInputs.isAnimated !== false ? datePickerPanelAnimatedClass : datePickerPanelClass,
    viewInputs.panelClass,
  ),
  panelAttributes: childAttributes([h.DataAttribute('slot', 'date-picker-content')]),
  backdropClassName: cn(datePickerBackdropClass, viewInputs.backdropClass),
  triggerContent: (maybeDate) =>
    h.div(
      [h.Class('flex w-full items-center justify-between gap-4')],
      [
        Option.match(maybeDate, {
          onNone: () => h.span([h.Class(datePickerPlaceholderClass)], ['Pick a date']),
          onSome: (date) => h.span([], [formatTriggerLabel(date)]),
        }),
        icon(h, ChevronDown),
      ],
    ),
  toCalendarView: calendarToView(h),
})