Calendar
Basic
January 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
// Package calendar holds the site's example gsx components for ui/calendar.
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// DefaultMonth is the fixed month Basic renders when no month is given —
// 2026-01, deliberately never time.Now(): jstest/specs/calendar.spec.ts
// asserts exact grid contents (data-date values, the "Go and JS agree on …"
// diffs), and a test like that cannot depend on the day it runs.
var DefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// Basic renders a single-mode calendar pinned to DefaultMonth by default. A
// zero month falls back to DefaultMonth, so the zero-arg registration
// (site/examples/calendar.go's static Node) and jstest/harness's own
// per-request re-render (its Query hook, driven by ?month=) share one
// component instead of two.
component Basic(month time.Time) {
{{
if month.IsZero() {
month = DefaultMonth
}
}}
<ui.Calendar mode="single" month={month} weekStartsOn={time.Sunday} showOutsideDays={true} captionLayout="label"/>
}
Bounded
January 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// Bounded pins fromYear/toYear to the SAME single year (2026) as the
// displayed month — the narrowest possible navigation range. Task 4 review
// finding 1: calendar.js's own nav bounds were never recomputed after a
// client-side navigation, so with the wide ~110-year default range nothing
// in the browser suite ever reached a bound and the gap went unnoticed.
// This example exists purely to make that bound reachable in a handful of
// clicks (jstest/specs/calendar.spec.ts's own bounded-navigation tests).
// captionLayout="dropdown" additionally exercises the year <select> at the
// same narrow bound — Task 4 review's other observation, that
// syncDropdowns could otherwise write a year with no matching <option>.
component Bounded() {
<ui.Calendar
mode="single"
month={time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)}
weekStartsOn={time.Sunday}
showOutsideDays={true}
captionLayout="dropdown"
fromYear={2026}
toYear={2026}
/>
}
Loaded
January 2026
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// LoadedDefaultMonth mirrors Basic's own DefaultMonth (2026-01, never
// time.Now()) — same reason: a test asserting exact grid contents can't
// depend on the day it runs.
var LoadedDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// Loaded exists solely to make calendar.js's ported dayDisabled, single-mode
// daySelected, ariaLabel and outside computations LIVE in the browser suite
// — Task 4 review's Important finding 2: basic.gsx has no selection, no
// disabled rules and captionLayout="label", so every one of those code
// paths in ui/calendar.js was zero-coverage; a bug in any of them (e.g. `<`
// vs `<=` against disabledBefore) left all seven original browser tests
// green. weekStartsOn=Monday is deliberate too (Task 4 review's Minor
// finding 4): basic.gsx's own Sunday week start makes the JS twin's
// `(x - weekStartsOn + 7) % 7` wrap-around term dead code, since `x - 0` is
// already non-negative.
//
// disabledBefore is 2026-01-05 (a Monday), deliberately NOT the same weekday
// as disabledWeekdays' own Saturday: an earlier draft used 2026-01-03 (also
// a Saturday), which meant a `<=` vs `<` bug at the before-bound was
// invisible — that date was already disabled by the independent weekday
// rule, so the OR-combination masked the regression. Every boundary date
// here (disabledBefore, disabledAfter, the disabledDates entry) is chosen to
// fall on a distinct, non-Saturday weekday, so each rule's own boundary is
// independently observable in a diff instead of being covered by another
// rule's coincidence.
component Loaded(month time.Time) {
{{
if month.IsZero() {
month = LoadedDefaultMonth
}
selected := []time.Time{time.Date(2026, 1, 8, 0, 0, 0, 0, time.UTC)}
disabledDates := []time.Time{time.Date(2026, 1, 20, 0, 0, 0, 0, time.UTC)}
disabledWeekdays := []time.Weekday{time.Saturday}
}}
<ui.Calendar
mode="single"
month={month}
selected={selected}
weekStartsOn={time.Monday}
showOutsideDays={true}
captionLayout="label"
disabledBefore={time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)}
disabledAfter={time.Date(2026, 1, 28, 0, 0, 0, 0, time.UTC)}
disabledDates={disabledDates}
disabledWeekdays={disabledWeekdays}
/>
}
// LoadedRangeDefaultMonth: same fixed-month discipline as LoadedDefaultMonth.
var LoadedRangeDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// LoadedRange exists to exercise calendar.js's range-flag computation
// (rangeStart/rangeMiddle/rangeEnd and the cell's own cellSelected, which is
// true across the WHOLE range, not just the two ends) — Task 4 review's
// Important finding 2 again: basic.gsx and Loaded above are both
// single-mode, so mode === "range" was never reached client-side.
// weekStartsOn=Monday for the same reason as Loaded.
component LoadedRange(month time.Time) {
{{
if month.IsZero() {
month = LoadedRangeDefaultMonth
}
}}
<ui.Calendar
mode="range"
month={month}
from={time.Date(2026, 1, 9, 0, 0, 0, 0, time.UTC)}
to={time.Date(2026, 1, 14, 0, 0, 0, 0, time.UTC)}
weekStartsOn={time.Monday}
showOutsideDays={true}
captionLayout="label"
/>
}
Loaded range
January 2026
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// LoadedDefaultMonth mirrors Basic's own DefaultMonth (2026-01, never
// time.Now()) — same reason: a test asserting exact grid contents can't
// depend on the day it runs.
var LoadedDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// Loaded exists solely to make calendar.js's ported dayDisabled, single-mode
// daySelected, ariaLabel and outside computations LIVE in the browser suite
// — Task 4 review's Important finding 2: basic.gsx has no selection, no
// disabled rules and captionLayout="label", so every one of those code
// paths in ui/calendar.js was zero-coverage; a bug in any of them (e.g. `<`
// vs `<=` against disabledBefore) left all seven original browser tests
// green. weekStartsOn=Monday is deliberate too (Task 4 review's Minor
// finding 4): basic.gsx's own Sunday week start makes the JS twin's
// `(x - weekStartsOn + 7) % 7` wrap-around term dead code, since `x - 0` is
// already non-negative.
//
// disabledBefore is 2026-01-05 (a Monday), deliberately NOT the same weekday
// as disabledWeekdays' own Saturday: an earlier draft used 2026-01-03 (also
// a Saturday), which meant a `<=` vs `<` bug at the before-bound was
// invisible — that date was already disabled by the independent weekday
// rule, so the OR-combination masked the regression. Every boundary date
// here (disabledBefore, disabledAfter, the disabledDates entry) is chosen to
// fall on a distinct, non-Saturday weekday, so each rule's own boundary is
// independently observable in a diff instead of being covered by another
// rule's coincidence.
component Loaded(month time.Time) {
{{
if month.IsZero() {
month = LoadedDefaultMonth
}
selected := []time.Time{time.Date(2026, 1, 8, 0, 0, 0, 0, time.UTC)}
disabledDates := []time.Time{time.Date(2026, 1, 20, 0, 0, 0, 0, time.UTC)}
disabledWeekdays := []time.Weekday{time.Saturday}
}}
<ui.Calendar
mode="single"
month={month}
selected={selected}
weekStartsOn={time.Monday}
showOutsideDays={true}
captionLayout="label"
disabledBefore={time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)}
disabledAfter={time.Date(2026, 1, 28, 0, 0, 0, 0, time.UTC)}
disabledDates={disabledDates}
disabledWeekdays={disabledWeekdays}
/>
}
// LoadedRangeDefaultMonth: same fixed-month discipline as LoadedDefaultMonth.
var LoadedRangeDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// LoadedRange exists to exercise calendar.js's range-flag computation
// (rangeStart/rangeMiddle/rangeEnd and the cell's own cellSelected, which is
// true across the WHOLE range, not just the two ends) — Task 4 review's
// Important finding 2 again: basic.gsx and Loaded above are both
// single-mode, so mode === "range" was never reached client-side.
// weekStartsOn=Monday for the same reason as Loaded.
component LoadedRange(month time.Time) {
{{
if month.IsZero() {
month = LoadedRangeDefaultMonth
}
}}
<ui.Calendar
mode="range"
month={month}
from={time.Date(2026, 1, 9, 0, 0, 0, 0, time.UTC)}
to={time.Date(2026, 1, 14, 0, 0, 0, 0, time.UTC)}
weekStartsOn={time.Monday}
showOutsideDays={true}
captionLayout="label"
/>
}
Hidden outside days
January 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// HiddenOutsideDefaultMonth mirrors Basic's own DefaultMonth (2026-01, never
// time.Now()) — same reason: a test asserting exact grid contents can't
// depend on the day it runs.
var HiddenOutsideDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// HiddenOutside is the only example that passes showOutsideDays={false}, and
// it exists for exactly that reason. The final review found the parameter
// declared and read nowhere: every cell rendered regardless of its value, so
// passing false had no observable effect at all — and no example ever passed
// false, which is why it went unnoticed for seven tasks.
//
// The upstream semantics this exercises are NOT "drop the cell"
// (react-day-picker 9.14.0, helpers/createGetModifiers.js): showOutsideDays
// false sets modifiers.hidden, the <td> stays for layout with
// classNames.hidden = "invisible", and no DayButton renders inside it. gsxui
// keeps the button element (calendar.js may never create or destroy one) and
// blanks it instead — empty text, tabindex="-1", aria-hidden="true", native
// disabled — so a hidden day is out of the tab order entirely, unlike a
// merely DISABLED day, which stays in it.
//
// A ?month= Query hook (site/examples/calendar.go) makes this example
// reachable at an arbitrary month, so jstest/specs/calendar.spec.ts can run
// the same client-navigate-then-diff-against-the-server agreement test the
// other examples do — the one thing that keeps calendar.js's own `hidden`
// computation honest against calendar.gsx's.
component HiddenOutside(month time.Time) {
{{
if month.IsZero() {
month = HiddenOutsideDefaultMonth
}
}}
<ui.Calendar mode="single" month={month} weekStartsOn={time.Sunday} showOutsideDays={false} captionLayout="label"/>
}
Range
January 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// RangeDefaultMonth mirrors Basic's own DefaultMonth (2026-01, never
// time.Now()) — same reason: Task 5's browser tests assert exact grid
// contents and click-driven selection, which can't depend on the day the
// suite runs.
var RangeDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// Range renders a range-mode calendar with no initial selection — Task 5's
// selection tests build the range entirely client-side: the two-click
// machine (first click sets from, second sets to, swapping if it precedes
// from, starting over once both are set) and the hover preview that fills in
// data-range-middle while from is set and to is not.
//
// name="stay" (Task 5 review, Minor 1 verification): before this, no range
// example carried a `name`, so ui/calendar.js's own range branch of
// syncHiddenInputs — the one that has to tell its two hidden inputs apart
// by the "to" input's own data-gsxui-calendar-hidden-to marker rather than
// a name-suffix guess — had zero live-browser coverage at all.
component Range(month time.Time) {
{{
if month.IsZero() {
month = RangeDefaultMonth
}
}}
<ui.Calendar
mode="range"
month={month}
name="stay"
weekStartsOn={time.Sunday}
showOutsideDays={true}
captionLayout="label"
/>
}
Multiple
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// MultipleDefaultMonth mirrors Basic's own DefaultMonth (2026-01, never
// time.Now()) — same reason as Range's own RangeDefaultMonth.
var MultipleDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// Multiple renders a multiple-mode calendar with no initial selection.
// The form and name make the repeated-value bridge observable as ordinary
// FormData: every selected date is submitted under the same "dates" key.
component Multiple(month time.Time) {
{{
if month.IsZero() {
month = MultipleDefaultMonth
}
}}
<form>
<ui.Calendar
mode="multiple"
month={month}
name="dates"
weekStartsOn={time.Sunday}
showOutsideDays={true}
captionLayout="label"
/>
</form>
}
Form
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// FormDefaultMonth mirrors Basic's own DefaultMonth — pinned so Task 5's
// form-reset browser test gets a stable grid regardless of the day the
// suite runs.
var FormDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// Form is real corpus coverage for Calendar's hidden-input bridge and for a
// mixed form whose reset event is intentionally handled by two modules.
// Calendar and Combobox restore disjoint descendants of the same form; the
// selector invariant records that overlap explicitly and this composition
// proves both restorations survive one native reset.
component Form() {
<form class="flex max-w-xs flex-col gap-4">
<ui.Calendar
mode="single"
month={FormDefaultMonth}
name="date"
weekStartsOn={time.Sunday}
showOutsideDays={true}
captionLayout="label"
/>
<div class="flex flex-col gap-2">
<ui.Label for="calendar-form-framework">Framework</ui.Label>
<ui.Combobox name="framework" value="">
<ui.ComboboxInput id="calendar-form-framework" placeholder="Search framework..." showTrigger/>
<ui.ComboboxContent>
<ui.ComboboxList>
<ui.ComboboxEmpty>No framework found.</ui.ComboboxEmpty>
<ui.ComboboxItem value="next.js" selected={false}>Next.js</ui.ComboboxItem>
<ui.ComboboxItem value="sveltekit" selected={false}>SvelteKit</ui.ComboboxItem>
</ui.ComboboxList>
</ui.ComboboxContent>
</ui.Combobox>
</div>
<div class="flex gap-2">
<ui.Button type="submit">Continue</ui.Button>
<ui.Button type="reset" variant="outline">Reset</ui.Button>
</div>
</form>
}
Dropdown
January 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// DropdownDefaultMonth mirrors Basic's own DefaultMonth (2026-01, never
// time.Now()) — same reason as every other fixture in this package: a
// component the browser invariants sweep asserts against can't depend on
// the day the suite runs.
var DropdownDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// Dropdown renders captionLayout="dropdown" with a realistic, wide
// fromYear/toYear span — a birthdate-picker-shaped range, not Bounded's own
// deliberately narrow single-year one. Bounded.gsx exists to make the
// navigation-bound edge reachable in a handful of clicks; this example
// exists to show the month/year <select> pair the way a caller actually
// uses captionLayout="dropdown" day to day, with fromYear/toYear supplied
// explicitly rather than left to calendarNavBounds's own current−100/
// current+10 default (ui/calendar.gsx).
//
// No Query hook: like Bounded and Form, this example carries no ?month=
// browser test of its own — the invariants sweep exercises it as-is.
component Dropdown() {
<ui.Calendar
mode="single"
month={DropdownDefaultMonth}
weekStartsOn={time.Sunday}
showOutsideDays={true}
captionLayout="dropdown"
fromYear={1980}
toYear={2030}
/>
}
Date Picker
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
"github.com/gsxhq/gsxui/ui/icon"
)
// DatePickerDefaultMonth mirrors Basic's own DefaultMonth (2026-01, never
// time.Now()) — same reason as every other fixture in this package.
var DatePickerDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// DatePicker is the composition shadcn documents as "Date Picker"
// (new-york-v4's own date-picker-demo.tsx): NOT its own component — a
// ui.Popover whose trigger is a ui.Button (showing "Pick a date" until a
// day is chosen, then the ISO date) and whose content is a ui.Calendar in
// single mode. This is the whole point of the example: gsxui has no
// DatePicker export, because Popover + Calendar already IS one, and
// composing them at the call site is strictly cheaper than threading a
// thirteenth Popover-specific parameter (open state, trigger label)
// through ui.Calendar itself. data-gsxui-slot-popover-trigger goes straight on
// ui.Button, the same direct-attribute idiom popover/basic.gsx's own
// "Open popover" trigger already uses, rather than wrapping it in a
// separate ui.PopoverTrigger.
//
// The inline <script> is carousel/api.gsx's own established idiom: a
// document-level listener for a component's own CustomEvent, scoped by id
// so it never reacts to some OTHER example's calendar on the same
// /components/calendar page (every example on that page renders at once —
// site/pages/component.gsx — so a bare "did a calendar change" guard would
// also fire for Range's, Multiple's, and every other calendar example's own
// clicks). ui.Calendar emits gsxui:change with { mode: "single", selected }
// on every click (docs/superpowers/specs/2026-07-25-calendar-design.md
// §4); the listener writes selected[0] (or "Pick a date" once more, if the
// click cleared the selection — commitSingle in ui/calendar.js clears on a
// re-click of the already-selected day) onto the trigger's own label, and
// clears the muted "no date yet" styling to match.
component DatePicker() {
<div>
<ui.Popover>
<ui.Button
variant="outline"
data-gsxui-slot-popover-trigger
aria-expanded="false"
class="w-[240px] justify-start text-left font-normal text-muted-foreground"
>
<icon.Calendar/>
<span data-datepicker-label>Pick a date</span>
</ui.Button>
<ui.PopoverContent class="w-auto p-0">
<ui.Calendar
id="datepicker-calendar"
mode="single"
month={DatePickerDefaultMonth}
weekStartsOn={time.Sunday}
showOutsideDays={true}
captionLayout="label"
/>
</ui.PopoverContent>
</ui.Popover>
<script>
document.addEventListener("gsxui:change", (e) => {
if (e.target.id !== "datepicker-calendar") return;
const button = e.target.closest("[data-gsxui-slot-popover]")?.querySelector("[data-gsxui-slot-popover-trigger]");
const label = button?.querySelector("[data-datepicker-label]");
if (!label) return;
const picked = e.detail.selected[0];
label.textContent = picked ?? "Pick a date";
button.classList.toggle("text-muted-foreground", !picked);
});
</script>
</div>
}
RTL
January 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
package calendar
import (
"time"
"github.com/gsxhq/gsxui/ui"
)
// RtlDefaultMonth mirrors Basic's own DefaultMonth (2026-01, never
// time.Now()).
var RtlDefaultMonth = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
// Rtl renders the same single-mode grid as Basic, wrapped in dir="rtl":
// the week runs right-to-left and the caption's prev/next icons flip via
// rtl:rotate-180, with no RTL-specific props of its own.
component Rtl() {
<div dir="rtl">
<ui.Calendar
mode="single"
month={RtlDefaultMonth}
weekStartsOn={time.Sunday}
showOutsideDays={true}
captionLayout="label"
/>
</div>
}