// Port of web/kit/DatePicker.tsx. Date math uses the stdlib time package. package webui import ( "fmt" "strconv" "strings" "time" "kjol/vdom" ) const datePickerWrap = "relative w-full min-w-0" const datePickerField = "relative w-full min-w-0 cursor-pointer [&_.ui-form]:m-0 [&_input]:cursor-text" const datePickerDropdown = "bg-white border border-neutral-200 rounded-default shadow-[0_4px_12px_rgba(0,0,0,0.15)] p-1 min-w-[16rem]" const datePickerIconBtn = "absolute inset-y-0 right-0 z-[1] flex items-center justify-center bg-transparent border-0 px-2 cursor-pointer text-text-muted leading-none hover:text-text-body pointer-events-auto" const datePickerClearBtn = "absolute inset-y-0 right-8 z-[1] flex items-center justify-center bg-transparent border-0 px-1.5 cursor-pointer text-text-muted leading-none hover:text-text-body pointer-events-auto" // NOTE: the TSX renders the dropdown into a with fixed coordinates from // getBoundingClientRect (tracked each frame). Portals, refs and element // measurement have no equivalent here, so the dropdown is rendered inline and // positioned with static Tailwind (absolute, below the field). const datePickerDropdownPos = "absolute left-0 top-full mt-1 z-[200]" // datePickerInputBase mirrors Forms.tsx INPUT_BASE (light, no error/success). const datePickerInputBase = "bg-white block w-full border rounded-default shadow-xs text-sm focus:outline-2 focus:outline-offset-1 disabled:bg-neutral-100 disabled:cursor-not-allowed" // datepickerInputCls reproduces Forms.tsx _inputCls for the light, no-state case. func datepickerInputCls(small bool, extra string) string { h, pad := "h-[38px]", "p-2" if small { h, pad = "h-[30px]", "p-1" } return cx(datePickerInputBase, h, pad, "border-neutral-300 focus:outline-sky-500", extra) } // datepickerParseInput parses typed/pasted text into a "YYYY-MM-DD" key, or "". // // NOTE: the TSX first tries YYYY-MM-DD, then falls back to the very lenient // `new Date(text)`. Go has no equivalent, so a fixed set of common layouts is // tried instead. func datepickerParseInput(text string) string { s := strings.TrimSpace(text) if s == "" { return "" } for _, layout := range []string{"2006-01-02", "1/2/2006", "01/02/2006", "2006/01/02", "January 2, 2006", "Jan 2, 2006"} { if t, err := time.Parse(layout, s); err == nil { return t.Format("2006-01-02") } } return "" } // datepickerFormatDisplay renders a "YYYY-MM-DD" key for display. // // NOTE: the TSX uses Date.toLocaleDateString() (locale-dependent). Go has no // locale formatter, so this approximates the common en-US "M/D/YYYY" form. func datepickerFormatDisplay(iso string) string { t, ok := calParseKey(iso) if !ok { return "" } return fmt.Sprintf("%d/%d/%d", int(t.Month()), t.Day(), t.Year()) } // datepickerInput renders the FormInput approximation: a .ui-form wrapper around // a styled . onChange commits the parsed value on the native change event. // // NOTE: the TSX FormInput tracks an editing/draft state machine across // focus/input/blur. Without persistent local signals that collapses to a single // onchange commit; the input shows the formatted external value otherwise. func datepickerInput(value, placeholder string, small bool, extra string, onChange func(string)) *vdom.VNode { mods := []vdom.Mod{ vdom.Attr("type", "text"), vdom.Attr("class", datepickerInputCls(small, extra)), vdom.Attr("placeholder", placeholder), vdom.Prop("value", value), } if onChange != nil { mods = append(mods, vdom.OnEvent(vdom.EVENT_CHANGE, func(e vdom.Event) { onChange(datepickerParseInput(e.Value())) })) } return vdom.El("div", vdom.Attr("class", "ui-form"), vdom.El("input", mods...)) } func datepickerIconButton(onClick func()) *vdom.VNode { return vdom.El("button", vdom.Attr("type", "button"), vdom.Attr("class", datePickerIconBtn), vdom.Attr("aria-label", "Open calendar"), vdom.On(vdom.EVENT_CLICK, onClick), Icon("calendar", 16, "block leading-none"), ) } // DatePickerProps configures DatePicker and DateOfBirthPicker. It is controlled: // Value is the "YYYY-MM-DD" selection, Open is the dropdown state, ViewMonth is // the calendar's visible month — each paired with a callback. // // NOTE: the TSX owns open/editing/localValue/dropdownPos in internal signals and // closes the dropdown via a document click listener. Outside-click handling, // stopPropagation and computed positioning aren't representable in the neutral // runtime, so open/selection/month are lifted to props + callbacks. type DatePickerProps struct { Value string // selected date "YYYY-MM-DD" OnChange func(value string) // new selection (or "" when cleared) Placeholder string // input placeholder Small bool // compact input height Clearable bool // show a clear (x) button when a value is set Open bool // dropdown open state (controlled) OnToggle func(open bool) // request to open/close the dropdown ViewMonth time.Time // visible month of the dropdown calendar OnNavigate func(month time.Time) // prev/next month requested in the dropdown } // DatePicker renders a text field with a calendar dropdown (picker layout). func DatePicker(p DatePickerProps) *vdom.VNode { hasValue := p.Value != "" extra := "w-full pr-9" if p.Clearable && hasValue { extra = "w-full pr-14" } fieldMods := []vdom.Mod{ vdom.Attr("class", datePickerField), vdom.On(vdom.EVENT_CLICK, func() { if p.OnToggle != nil { p.OnToggle(true) } }), datepickerInput(datepickerFormatDisplay(p.Value), pick(p.Placeholder, "Select date"), p.Small, extra, p.OnChange), } if p.Clearable && hasValue { fieldMods = append(fieldMods, vdom.El("button", vdom.Attr("type", "button"), vdom.Attr("class", datePickerClearBtn), vdom.Attr("aria-label", "Clear date"), vdom.On(vdom.EVENT_CLICK, func() { if p.OnChange != nil { p.OnChange("") } if p.OnToggle != nil { p.OnToggle(false) } }), Icon("xmark", 14, "block leading-none"), )) } fieldMods = append(fieldMods, datepickerIconButton(func() { if p.OnToggle != nil { p.OnToggle(!p.Open) } })) wrapMods := []vdom.Mod{ vdom.Attr("class", datePickerWrap), vdom.El("div", fieldMods...), } if p.Open { cal := Calendar(CalendarProps{ Selected: p.Value, ViewMonth: p.ViewMonth, Variant: CalendarVariantPicker, OnSelect: func(key string) { if p.OnChange != nil { p.OnChange(key) } if p.OnToggle != nil { p.OnToggle(false) } }, OnNavigate: p.OnNavigate, }) wrapMods = append(wrapMods, vdom.El("div", vdom.Attr("class", cx(datePickerDropdown, datePickerDropdownPos)), cal, )) } return vdom.El("div", wrapMods...) } // datepickerDOBCalendar renders the date-of-birth calendar: the picker grid with // month/year