foldkit 0.78.0 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -170,6 +170,7 @@ This is what makes Foldkit unusually AI-friendly. The same property that makes t
170
170
  - **[Stopwatch](https://foldkit.dev/example-apps/stopwatch)** — Timer with start/stop/reset
171
171
  - **[Crash View](https://foldkit.dev/example-apps/crash-view)** — Custom crash fallback UI with crash reporting
172
172
  - **[Form](https://foldkit.dev/example-apps/form)** — Form validation with async email checking
173
+ - **[Job Application](https://foldkit.dev/example-apps/job-application)** — Multi-step form with cross-field validation, file uploads, and per-step error indicators
173
174
  - **[Weather](https://foldkit.dev/example-apps/weather)** — HTTP requests with async state handling
174
175
  - **[Routing](https://foldkit.dev/example-apps/routing)** — URL routing with parser combinators
175
176
  - **[Query Sync](https://foldkit.dev/example-apps/query-sync)** — URL query parameter sync with filtering and sorting
@@ -3,9 +3,15 @@ import * as Calendar from '../../calendar/index.js';
3
3
  import type { CalendarDate } from '../../calendar/index.js';
4
4
  import * as Command from '../../command/index.js';
5
5
  import { type Attribute, type Html } from '../../html/index.js';
6
+ /** Which grid the calendar is currently displaying. `Days` is the standard
7
+ * 6×7 day grid; `Months` is a 3×4 month-name grid for fast month jumps;
8
+ * `Years` is a 3×4 year grid paged in 12-year windows for fast year jumps. */
9
+ export declare const ViewMode: S.Literal<["Days", "Months", "Years"]>;
10
+ export type ViewMode = typeof ViewMode.Type;
6
11
  /** Schema for the calendar component's state. Tracks the visible month/year,
7
- * the keyboard-focused and user-selected dates, and the configuration that
8
- * governs navigation (locale, min/max, disabled days). */
12
+ * the keyboard-focused and user-selected dates, the active view mode, and
13
+ * the configuration that governs navigation (locale, min/max, disabled
14
+ * days). */
9
15
  export declare const Model: S.Struct<{
10
16
  id: typeof S.String;
11
17
  today: S.filter<S.Struct<{
@@ -15,6 +21,7 @@ export declare const Model: S.Struct<{
15
21
  }>>;
16
22
  viewYear: typeof S.Int;
17
23
  viewMonth: S.filter<typeof S.Int>;
24
+ viewMode: S.Literal<["Days", "Months", "Years"]>;
18
25
  maybeFocusedDate: S.OptionFromSelf<S.filter<S.Struct<{
19
26
  year: typeof S.Int;
20
27
  month: S.filter<typeof S.Int>;
@@ -65,18 +72,30 @@ export declare const PressedKeyOnGrid: import("../../schema/index.js").CallableT
65
72
  key: typeof S.String;
66
73
  isShift: typeof S.Boolean;
67
74
  }>;
68
- /** Sent when the user clicks the previous-month navigation button. */
75
+ /** Sent when the user clicks the previous-month navigation button in Days
76
+ * mode. (The Years mode prev/next-page buttons dispatch `PagedYears`.) */
69
77
  export declare const ClickedPreviousMonthButton: import("../../schema/index.js").CallableTaggedStruct<"ClickedPreviousMonthButton", {}>;
70
- /** Sent when the user clicks the next-month navigation button. */
78
+ /** Sent when the user clicks the next-month navigation button in Days
79
+ * mode. (The Years mode prev/next-page buttons dispatch `PagedYears`.) */
71
80
  export declare const ClickedNextMonthButton: import("../../schema/index.js").CallableTaggedStruct<"ClickedNextMonthButton", {}>;
72
- /** Sent when the user picks a month from the month dropdown. */
73
- export declare const SelectedMonthFromDropdown: import("../../schema/index.js").CallableTaggedStruct<"SelectedMonthFromDropdown", {
81
+ /** Sent when the user clicks the calendar heading. Zooms out one mode
82
+ * level: Days → Months, Months → Years. Terminal in Years mode. */
83
+ export declare const ClickedHeading: import("../../schema/index.js").CallableTaggedStruct<"ClickedHeading", {}>;
84
+ /** Sent when the user picks a month from the months grid. Jumps the view
85
+ * to that month and returns the calendar to Days mode. */
86
+ export declare const SelectedMonth: import("../../schema/index.js").CallableTaggedStruct<"SelectedMonth", {
74
87
  month: typeof S.Int;
75
88
  }>;
76
- /** Sent when the user picks a year from the year dropdown. */
77
- export declare const SelectedYearFromDropdown: import("../../schema/index.js").CallableTaggedStruct<"SelectedYearFromDropdown", {
89
+ /** Sent when the user picks a year from the years grid. Jumps the view to
90
+ * that year and transitions the calendar to Months mode for further drilling. */
91
+ export declare const SelectedYear: import("../../schema/index.js").CallableTaggedStruct<"SelectedYear", {
78
92
  year: typeof S.Int;
79
93
  }>;
94
+ /** Sent when the user pages the years grid forward or backward by one
95
+ * window. Direction is `1` for next, `-1` for previous. */
96
+ export declare const PagedYears: import("../../schema/index.js").CallableTaggedStruct<"PagedYears", {
97
+ direction: S.Literal<[1, -1]>;
98
+ }>;
80
99
  /** Sent when the grid container receives DOM focus. */
81
100
  export declare const FocusedGrid: import("../../schema/index.js").CallableTaggedStruct<"FocusedGrid", {}>;
82
101
  /** Sent when the grid container loses DOM focus. */
@@ -101,10 +120,12 @@ export declare const Message: S.Union<[import("../../schema/index.js").CallableT
101
120
  }>, import("../../schema/index.js").CallableTaggedStruct<"PressedKeyOnGrid", {
102
121
  key: typeof S.String;
103
122
  isShift: typeof S.Boolean;
104
- }>, import("../../schema/index.js").CallableTaggedStruct<"ClickedPreviousMonthButton", {}>, import("../../schema/index.js").CallableTaggedStruct<"ClickedNextMonthButton", {}>, import("../../schema/index.js").CallableTaggedStruct<"SelectedMonthFromDropdown", {
123
+ }>, import("../../schema/index.js").CallableTaggedStruct<"ClickedPreviousMonthButton", {}>, import("../../schema/index.js").CallableTaggedStruct<"ClickedNextMonthButton", {}>, import("../../schema/index.js").CallableTaggedStruct<"ClickedHeading", {}>, import("../../schema/index.js").CallableTaggedStruct<"SelectedMonth", {
105
124
  month: typeof S.Int;
106
- }>, import("../../schema/index.js").CallableTaggedStruct<"SelectedYearFromDropdown", {
125
+ }>, import("../../schema/index.js").CallableTaggedStruct<"SelectedYear", {
107
126
  year: typeof S.Int;
127
+ }>, import("../../schema/index.js").CallableTaggedStruct<"PagedYears", {
128
+ direction: S.Literal<[1, -1]>;
108
129
  }>, import("../../schema/index.js").CallableTaggedStruct<"FocusedGrid", {}>, import("../../schema/index.js").CallableTaggedStruct<"BlurredGrid", {}>, import("../../schema/index.js").CallableTaggedStruct<"RefreshedToday", {
109
130
  today: S.filter<S.Struct<{
110
131
  year: typeof S.Int;
@@ -183,6 +204,17 @@ export declare const setDisabledDates: (model: Model, disabledDates: ReadonlyArr
183
204
  /** Sets the days of the week that are disabled (e.g. weekends). Pass an
184
205
  * empty array to clear. Does NOT reconcile the current selection. */
185
206
  export declare const setDisabledDaysOfWeek: (model: Model, disabledDaysOfWeek: ReadonlyArray<Calendar.DayOfWeek>) => Model;
207
+ /** Returns the calendar to Days mode regardless of current depth. Useful for
208
+ * standalone (non-popovered) consumers that want to wire their own back-out
209
+ * gesture. Popovered consumers like `Ui.DatePicker` don't need this — Escape
210
+ * closes the popover, and the calendar resets to Days on next open.
211
+ *
212
+ * Reconciles `maybeFocusedDate` to a date inside the visible (`viewYear`,
213
+ * `viewMonth`) — Months/Years navigation can leave the cursor on a date
214
+ * outside the days grid (paged-away year, etc.), which would otherwise
215
+ * cause `aria-activedescendant` to point at a non-rendered cell and the
216
+ * next ArrowLeft to jump to the cursor's stale year. */
217
+ export declare const dropToDays: (model: Model) => Model;
186
218
  /** Processes a calendar message and returns the next model, commands, and
187
219
  * optional OutMessage. */
188
220
  export declare const update: (model: Model, message: Message) => UpdateReturn;
@@ -198,39 +230,93 @@ export type DayCell<ParentMessage> = Readonly<{
198
230
  isInViewMonth: boolean;
199
231
  isDisabled: boolean;
200
232
  }>;
201
- /** A column header for the grid's first row (day-of-week labels). */
233
+ /** A column header for the day grid's first row (day-of-week labels). */
202
234
  export type ColumnHeader<ParentMessage> = Readonly<{
203
235
  name: string;
204
236
  attributes: ReadonlyArray<Attribute<ParentMessage>>;
205
237
  }>;
206
- /** A single week row in the calendar grid, carrying its own row attributes
207
- * (role, aria-rowindex) alongside its 7 day cells. */
238
+ /** A single week row in the day grid, carrying its own row attributes (role,
239
+ * aria-rowindex) alongside its 7 day cells. */
208
240
  export type Week<ParentMessage> = Readonly<{
209
241
  attributes: ReadonlyArray<Attribute<ParentMessage>>;
210
242
  cells: ReadonlyArray<DayCell<ParentMessage>>;
211
243
  }>;
212
- /** Attribute groups and derived data the calendar component provides to the
213
- * consumer's `toView` callback. */
214
- export type CalendarAttributes<ParentMessage> = Readonly<{
244
+ /** Information about a single month cell in the rendered months grid.
245
+ * `label` is the locale-aware full month name (e.g. "September"); `shortLabel`
246
+ * is the locale-aware abbreviation (e.g. "Sep"). Render whichever fits the
247
+ * cell — never substring `label` to abbreviate, since that's not safe across
248
+ * locales. */
249
+ export type MonthCell<ParentMessage> = Readonly<{
250
+ month: number;
251
+ label: string;
252
+ shortLabel: string;
253
+ cellAttributes: ReadonlyArray<Attribute<ParentMessage>>;
254
+ buttonAttributes: ReadonlyArray<Attribute<ParentMessage>>;
255
+ isSelected: boolean;
256
+ isFocused: boolean;
257
+ isCurrentMonth: boolean;
258
+ isDisabled: boolean;
259
+ }>;
260
+ /** Information about a single year cell in the rendered years grid. */
261
+ export type YearCell<ParentMessage> = Readonly<{
262
+ year: number;
263
+ label: string;
264
+ cellAttributes: ReadonlyArray<Attribute<ParentMessage>>;
265
+ buttonAttributes: ReadonlyArray<Attribute<ParentMessage>>;
266
+ isSelected: boolean;
267
+ isFocused: boolean;
268
+ isCurrentYear: boolean;
269
+ isDisabled: boolean;
270
+ }>;
271
+ /** Attributes provided to the consumer when rendering the day grid. */
272
+ export type DaysModeAttributes<ParentMessage> = Readonly<{
273
+ _tag: 'Days';
215
274
  root: ReadonlyArray<Attribute<ParentMessage>>;
216
275
  previousMonthButton: ReadonlyArray<Attribute<ParentMessage>>;
217
276
  nextMonthButton: ReadonlyArray<Attribute<ParentMessage>>;
277
+ headingButton: ReadonlyArray<Attribute<ParentMessage>>;
218
278
  heading: Readonly<{
219
279
  id: string;
220
280
  text: string;
221
281
  }>;
222
- monthSelect: ReadonlyArray<Attribute<ParentMessage>>;
223
- monthOptions: ReadonlyArray<Readonly<{
224
- value: number;
225
- label: string;
226
- }>>;
227
- yearSelect: ReadonlyArray<Attribute<ParentMessage>>;
228
- yearOptions: ReadonlyArray<number>;
229
282
  grid: ReadonlyArray<Attribute<ParentMessage>>;
230
283
  headerRow: ReadonlyArray<Attribute<ParentMessage>>;
231
284
  columnHeaders: ReadonlyArray<ColumnHeader<ParentMessage>>;
232
285
  weeks: ReadonlyArray<Week<ParentMessage>>;
233
286
  }>;
287
+ /** Attributes provided to the consumer when rendering the months grid. The
288
+ * 12 cells are pre-built in calendar (locale-ordered) — the consumer arranges
289
+ * them in whatever grid layout they prefer (3×4 is the typical choice). */
290
+ export type MonthsModeAttributes<ParentMessage> = Readonly<{
291
+ _tag: 'Months';
292
+ root: ReadonlyArray<Attribute<ParentMessage>>;
293
+ headingButton: ReadonlyArray<Attribute<ParentMessage>>;
294
+ heading: Readonly<{
295
+ id: string;
296
+ text: string;
297
+ }>;
298
+ grid: ReadonlyArray<Attribute<ParentMessage>>;
299
+ cells: ReadonlyArray<MonthCell<ParentMessage>>;
300
+ }>;
301
+ /** Attributes provided to the consumer when rendering the years grid. The
302
+ * 12 cells span one paged window; prev/next buttons page by 12 years. */
303
+ export type YearsModeAttributes<ParentMessage> = Readonly<{
304
+ _tag: 'Years';
305
+ root: ReadonlyArray<Attribute<ParentMessage>>;
306
+ previousPageButton: ReadonlyArray<Attribute<ParentMessage>>;
307
+ nextPageButton: ReadonlyArray<Attribute<ParentMessage>>;
308
+ heading: Readonly<{
309
+ id: string;
310
+ text: string;
311
+ }>;
312
+ grid: ReadonlyArray<Attribute<ParentMessage>>;
313
+ cells: ReadonlyArray<YearCell<ParentMessage>>;
314
+ }>;
315
+ /** Discriminated union of attribute groups and derived data the calendar
316
+ * component provides to the consumer's `toView` callback. The variant
317
+ * matches `model.viewMode` — pattern-match on `_tag` with `M.tagsExhaustive`
318
+ * to render each mode. */
319
+ export type CalendarAttributes<ParentMessage> = DaysModeAttributes<ParentMessage> | MonthsModeAttributes<ParentMessage> | YearsModeAttributes<ParentMessage>;
234
320
  /** Configuration for rendering a calendar with `view`. */
235
321
  export type ViewConfig<ParentMessage> = Readonly<{
236
322
  model: Model;
@@ -245,12 +331,15 @@ export type ViewConfig<ParentMessage> = Readonly<{
245
331
  onSelectedDate?: (date: CalendarDate) => ParentMessage;
246
332
  previousMonthLabel?: string;
247
333
  nextMonthLabel?: string;
248
- monthSelectLabel?: string;
249
- yearSelectLabel?: string;
334
+ previousYearsPageLabel?: string;
335
+ nextYearsPageLabel?: string;
336
+ daysHeadingButtonLabel?: string;
337
+ monthsHeadingButtonLabel?: string;
250
338
  }>;
251
- /** Renders an accessible calendar grid. Builds ARIA attribute groups (grid,
252
- * row, gridcell, column header) plus the derived month grid, then delegates
253
- * layout to the consumer's `toView` callback. */
339
+ /** Renders an accessible calendar. Builds mode-specific ARIA attribute
340
+ * groups and derived cell data, then delegates layout to the consumer's
341
+ * `toView` callback. The variant of `CalendarAttributes` passed to `toView`
342
+ * matches `model.viewMode`. */
254
343
  export declare const view: <ParentMessage>(config: ViewConfig<ParentMessage>) => Html;
255
344
  /** Creates a memoized calendar view. Static config is captured in a closure;
256
345
  * only `model` and `toParentMessage` are compared per render via `createLazy`. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/calendar/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/calendar/index.ts"],"names":[],"mappings":"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"}
@@ -7,14 +7,20 @@ import { m } from '../../message/index.js';
7
7
  import { evo } from '../../struct/index.js';
8
8
  import * as Task from '../../task/index.js';
9
9
  // MODEL
10
+ /** Which grid the calendar is currently displaying. `Days` is the standard
11
+ * 6×7 day grid; `Months` is a 3×4 month-name grid for fast month jumps;
12
+ * `Years` is a 3×4 year grid paged in 12-year windows for fast year jumps. */
13
+ export const ViewMode = S.Literal('Days', 'Months', 'Years');
10
14
  /** Schema for the calendar component's state. Tracks the visible month/year,
11
- * the keyboard-focused and user-selected dates, and the configuration that
12
- * governs navigation (locale, min/max, disabled days). */
15
+ * the keyboard-focused and user-selected dates, the active view mode, and
16
+ * the configuration that governs navigation (locale, min/max, disabled
17
+ * days). */
13
18
  export const Model = S.Struct({
14
19
  id: S.String,
15
20
  today: Calendar.CalendarDate,
16
21
  viewYear: S.Int,
17
22
  viewMonth: S.Int.pipe(S.between(1, 12)),
23
+ viewMode: ViewMode,
18
24
  maybeFocusedDate: S.OptionFromSelf(Calendar.CalendarDate),
19
25
  maybeSelectedDate: S.OptionFromSelf(Calendar.CalendarDate),
20
26
  isGridFocused: S.Boolean,
@@ -33,18 +39,24 @@ export const PressedKeyOnGrid = m('PressedKeyOnGrid', {
33
39
  key: S.String,
34
40
  isShift: S.Boolean,
35
41
  });
36
- /** Sent when the user clicks the previous-month navigation button. */
42
+ /** Sent when the user clicks the previous-month navigation button in Days
43
+ * mode. (The Years mode prev/next-page buttons dispatch `PagedYears`.) */
37
44
  export const ClickedPreviousMonthButton = m('ClickedPreviousMonthButton');
38
- /** Sent when the user clicks the next-month navigation button. */
45
+ /** Sent when the user clicks the next-month navigation button in Days
46
+ * mode. (The Years mode prev/next-page buttons dispatch `PagedYears`.) */
39
47
  export const ClickedNextMonthButton = m('ClickedNextMonthButton');
40
- /** Sent when the user picks a month from the month dropdown. */
41
- export const SelectedMonthFromDropdown = m('SelectedMonthFromDropdown', {
42
- month: S.Int,
43
- });
44
- /** Sent when the user picks a year from the year dropdown. */
45
- export const SelectedYearFromDropdown = m('SelectedYearFromDropdown', {
46
- year: S.Int,
47
- });
48
+ /** Sent when the user clicks the calendar heading. Zooms out one mode
49
+ * level: Days → Months, Months → Years. Terminal in Years mode. */
50
+ export const ClickedHeading = m('ClickedHeading');
51
+ /** Sent when the user picks a month from the months grid. Jumps the view
52
+ * to that month and returns the calendar to Days mode. */
53
+ export const SelectedMonth = m('SelectedMonth', { month: S.Int });
54
+ /** Sent when the user picks a year from the years grid. Jumps the view to
55
+ * that year and transitions the calendar to Months mode for further drilling. */
56
+ export const SelectedYear = m('SelectedYear', { year: S.Int });
57
+ /** Sent when the user pages the years grid forward or backward by one
58
+ * window. Direction is `1` for next, `-1` for previous. */
59
+ export const PagedYears = m('PagedYears', { direction: S.Literal(1, -1) });
48
60
  /** Sent when the grid container receives DOM focus. */
49
61
  export const FocusedGrid = m('FocusedGrid');
50
62
  /** Sent when the grid container loses DOM focus. */
@@ -56,7 +68,7 @@ export const RefreshedToday = m('RefreshedToday', {
56
68
  /** Sent when a FocusGrid command completes. */
57
69
  export const CompletedFocusGrid = m('CompletedFocusGrid');
58
70
  /** Union of all messages the calendar component can produce. */
59
- export const Message = S.Union(ClickedDay, PressedKeyOnGrid, ClickedPreviousMonthButton, ClickedNextMonthButton, SelectedMonthFromDropdown, SelectedYearFromDropdown, FocusedGrid, BlurredGrid, RefreshedToday, CompletedFocusGrid);
71
+ export const Message = S.Union(ClickedDay, PressedKeyOnGrid, ClickedPreviousMonthButton, ClickedNextMonthButton, ClickedHeading, SelectedMonth, SelectedYear, PagedYears, FocusedGrid, BlurredGrid, RefreshedToday, CompletedFocusGrid);
60
72
  // OUT MESSAGE
61
73
  /** Emitted when the visible month changes due to navigation. Consumers of an
62
74
  * inline calendar may use this to load month-scoped data (holidays, events).
@@ -83,6 +95,7 @@ export const init = (config) => {
83
95
  today: config.today,
84
96
  viewYear: initialFocus.year,
85
97
  viewMonth: initialFocus.month,
98
+ viewMode: 'Days',
86
99
  maybeFocusedDate: Option.some(initialFocus),
87
100
  maybeSelectedDate: maybeInitialSelectedDate,
88
101
  isGridFocused: false,
@@ -126,25 +139,34 @@ export const setDisabledDates = (model, disabledDates) => evo(model, { disabledD
126
139
  /** Sets the days of the week that are disabled (e.g. weekends). Pass an
127
140
  * empty array to clear. Does NOT reconcile the current selection. */
128
141
  export const setDisabledDaysOfWeek = (model, disabledDaysOfWeek) => evo(model, { disabledDaysOfWeek: () => disabledDaysOfWeek });
142
+ /** Returns the calendar to Days mode regardless of current depth. Useful for
143
+ * standalone (non-popovered) consumers that want to wire their own back-out
144
+ * gesture. Popovered consumers like `Ui.DatePicker` don't need this — Escape
145
+ * closes the popover, and the calendar resets to Days on next open.
146
+ *
147
+ * Reconciles `maybeFocusedDate` to a date inside the visible (`viewYear`,
148
+ * `viewMonth`) — Months/Years navigation can leave the cursor on a date
149
+ * outside the days grid (paged-away year, etc.), which would otherwise
150
+ * cause `aria-activedescendant` to point at a non-rendered cell and the
151
+ * next ArrowLeft to jump to the cursor's stale year. */
152
+ export const dropToDays = (model) => {
153
+ const focusedDay = Option.match(model.maybeFocusedDate, {
154
+ onNone: () => 1,
155
+ onSome: date => Math.min(date.day, Calendar.daysInMonth(model.viewYear, model.viewMonth)),
156
+ });
157
+ return evo(model, {
158
+ viewMode: () => 'Days',
159
+ maybeFocusedDate: () => Option.some(Calendar.make(model.viewYear, model.viewMonth, focusedDay)),
160
+ });
161
+ };
129
162
  const DAY_SKIP_CAP = 31;
130
163
  const MONTH_SKIP_CAP = 12;
131
- const isDateDisabled = (model, date) => {
132
- const belowMin = Option.exists(model.maybeMinDate, min => Calendar.isBefore(date, min));
133
- if (belowMin) {
134
- return true;
135
- }
136
- const aboveMax = Option.exists(model.maybeMaxDate, max => Calendar.isAfter(date, max));
137
- if (aboveMax) {
138
- return true;
139
- }
140
- if (model.disabledDaysOfWeek.includes(Calendar.dayOfWeek(date))) {
141
- return true;
142
- }
143
- if (model.disabledDates.some(Calendar.isEqual(date))) {
144
- return true;
145
- }
146
- return false;
147
- };
164
+ /** Number of years per Years-mode page. A 3×4 grid renders one window. */
165
+ const YEARS_PAGE_SIZE = 12;
166
+ const isDateDisabled = (model, date) => Option.exists(model.maybeMinDate, min => Calendar.isBefore(date, min)) ||
167
+ Option.exists(model.maybeMaxDate, max => Calendar.isAfter(date, max)) ||
168
+ model.disabledDaysOfWeek.includes(Calendar.dayOfWeek(date)) ||
169
+ model.disabledDates.some(Calendar.isEqual(date));
148
170
  /** Walks from `start` in `direction`, returning the first non-disabled date
149
171
  * within `cap` steps. Falls back to `start` if every candidate is disabled. */
150
172
  const skipDisabled = (model, start, direction, cap) => pipe(cap, Array.makeBy(step => Calendar.addDays(start, step * direction)), Array.findFirst(date => !isDateDisabled(model, date)), Option.getOrElse(() => start));
@@ -241,14 +263,52 @@ const applyViewMonthChange = (model, year, month, direction) => {
241
263
  });
242
264
  return [nextModel, [], Option.some(ChangedViewMonth({ year, month }))];
243
265
  };
244
- /** Direction the user moved when jumping to a new view month via dropdown.
245
- * Used by `skipDisabled` so a forward jump skips forward through disabled
246
- * dates and a backward jump skips backward. */
247
- const dropdownDirection = (model, year, month) => {
266
+ /** Direction the user moved when jumping to a new view year/month via grid
267
+ * selection. Used by `skipDisabled` so a forward jump skips forward through
268
+ * disabled dates and a backward jump skips backward. */
269
+ const jumpDirection = (model, year, month) => {
248
270
  const next = Calendar.make(year, month, 1);
249
271
  const current = Calendar.make(model.viewYear, model.viewMonth, 1);
250
272
  return Calendar.isAfter(next, current) ? 1 : -1;
251
273
  };
274
+ /** Maps a keyboard key to a months-grid focus shift (in months). Months
275
+ * mode supports horizontal (±1), vertical (±row width), and PageUp/Down
276
+ * (±12) navigation. */
277
+ const resolveMonthsKey = (key) => M.value(key).pipe(M.withReturnType(), M.when('ArrowLeft', () => -1), M.when('ArrowRight', () => 1), M.when('ArrowUp', () => -MONTHS_GRID_COLUMNS), M.when('ArrowDown', () => MONTHS_GRID_COLUMNS), M.when('PageUp', () => -MONTHS_IN_YEAR), M.when('PageDown', () => MONTHS_IN_YEAR), M.option);
278
+ /** Maps a keyboard key to a years-grid focus shift (in years). Years mode
279
+ * supports horizontal (±1), vertical (±row width), and PageUp/Down (±12 =
280
+ * one window) navigation. */
281
+ const resolveYearsKey = (key) => M.value(key).pipe(M.withReturnType(), M.when('ArrowLeft', () => -1), M.when('ArrowRight', () => 1), M.when('ArrowUp', () => -YEARS_GRID_COLUMNS), M.when('ArrowDown', () => YEARS_GRID_COLUMNS), M.when('PageUp', () => -YEARS_PAGE_SIZE), M.when('PageDown', () => YEARS_PAGE_SIZE), M.option);
282
+ /** Applies a months-grid focus shift, updating `maybeFocusedDate` and
283
+ * `viewYear` to reflect the new focused date. `viewMonth` is preserved —
284
+ * Months mode keyboard navigation moves the cursor without committing. */
285
+ const applyMonthsFocusShift = (model, monthShift) => {
286
+ const focused = currentOrFallbackFocus(model);
287
+ const nextFocus = Calendar.addMonths(focused, monthShift);
288
+ return [
289
+ evo(model, {
290
+ maybeFocusedDate: () => Option.some(nextFocus),
291
+ viewYear: () => nextFocus.year,
292
+ }),
293
+ [],
294
+ Option.none(),
295
+ ];
296
+ };
297
+ /** Applies a years-grid focus shift, updating only `maybeFocusedDate`.
298
+ * `viewYear` is preserved so the "selected" highlight (`year === viewYear`)
299
+ * stays on the calendar's centered year while the cursor moves freely. The
300
+ * visible 12-year page is derived from the cursor in the view layer. */
301
+ const applyYearsFocusShift = (model, yearShift) => {
302
+ const focused = currentOrFallbackFocus(model);
303
+ const nextFocus = Calendar.addYears(focused, yearShift);
304
+ return [
305
+ evo(model, {
306
+ maybeFocusedDate: () => Option.some(nextFocus),
307
+ }),
308
+ [],
309
+ Option.none(),
310
+ ];
311
+ };
252
312
  /** Processes a calendar message and returns the next model, commands, and
253
313
  * optional OutMessage. */
254
314
  export const update = (model, message) => M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
@@ -256,26 +316,38 @@ export const update = (model, message) => M.value(message).pipe(withUpdateReturn
256
316
  if (isDateDisabled(model, date)) {
257
317
  return [model, [], Option.none()];
258
318
  }
259
- const [nextModel, maybeOutMessage] = commitSelection(model, date);
260
- return [nextModel, [], maybeOutMessage];
319
+ else {
320
+ const [nextModel, maybeOutMessage] = commitSelection(model, date);
321
+ return [nextModel, [], maybeOutMessage];
322
+ }
261
323
  },
262
- PressedKeyOnGrid: ({ key, isShift }) => {
324
+ PressedKeyOnGrid: ({ key, isShift }) => M.value(model.viewMode).pipe(withUpdateReturn, M.when('Days', () => {
263
325
  const focused = currentOrFallbackFocus(model);
264
326
  if (isCommitKey(key)) {
265
327
  if (isDateDisabled(model, focused)) {
266
328
  return [model, [], Option.none()];
267
329
  }
268
- const [nextModel, maybeOutMessage] = commitSelection(model, focused);
269
- return [nextModel, [], maybeOutMessage];
270
- }
271
- return Option.match(resolveNavigationKey(key, isShift, focused, model.locale.firstDayOfWeek), {
272
- onNone: () => [model, [], Option.none()],
273
- onSome: ([candidate, direction, cap]) => {
274
- const [nextModel, maybeOutMessage] = applyFocusMove(model, candidate, direction, cap);
330
+ else {
331
+ const [nextModel, maybeOutMessage] = commitSelection(model, focused);
275
332
  return [nextModel, [], maybeOutMessage];
276
- },
277
- });
278
- },
333
+ }
334
+ }
335
+ else {
336
+ return Option.match(resolveNavigationKey(key, isShift, focused, model.locale.firstDayOfWeek), {
337
+ onNone: () => [model, [], Option.none()],
338
+ onSome: ([candidate, direction, cap]) => {
339
+ const [nextModel, maybeOutMessage] = applyFocusMove(model, candidate, direction, cap);
340
+ return [nextModel, [], maybeOutMessage];
341
+ },
342
+ });
343
+ }
344
+ }), M.when('Months', () => Option.match(resolveMonthsKey(key), {
345
+ onNone: () => [model, [], Option.none()],
346
+ onSome: shift => applyMonthsFocusShift(model, shift),
347
+ })), M.when('Years', () => Option.match(resolveYearsKey(key), {
348
+ onNone: () => [model, [], Option.none()],
349
+ onSome: shift => applyYearsFocusShift(model, shift),
350
+ })), M.exhaustive),
279
351
  ClickedPreviousMonthButton: () => {
280
352
  const next = Calendar.subtractMonths(Calendar.make(model.viewYear, model.viewMonth, 1), 1);
281
353
  return applyViewMonthChange(model, next.year, next.month, -1);
@@ -284,8 +356,42 @@ export const update = (model, message) => M.value(message).pipe(withUpdateReturn
284
356
  const next = Calendar.addMonths(Calendar.make(model.viewYear, model.viewMonth, 1), 1);
285
357
  return applyViewMonthChange(model, next.year, next.month, 1);
286
358
  },
287
- SelectedMonthFromDropdown: ({ month }) => applyViewMonthChange(model, model.viewYear, month, dropdownDirection(model, model.viewYear, month)),
288
- SelectedYearFromDropdown: ({ year }) => applyViewMonthChange(model, year, model.viewMonth, dropdownDirection(model, year, model.viewMonth)),
359
+ ClickedHeading: () => M.value(model.viewMode).pipe(withUpdateReturn, M.when('Days', () => [
360
+ evo(model, { viewMode: () => 'Months' }),
361
+ [focusGrid(model.id)],
362
+ Option.none(),
363
+ ]), M.when('Months', () => [
364
+ evo(model, { viewMode: () => 'Years' }),
365
+ [focusGrid(model.id)],
366
+ Option.none(),
367
+ ]), M.when('Years', () => [model, [], Option.none()]), M.exhaustive),
368
+ SelectedMonth: ({ month }) => {
369
+ if (isMonthDisabled(model, model.viewYear, month)) {
370
+ return [model, [], Option.none()];
371
+ }
372
+ else {
373
+ const [nextModel, commands, maybeOutMessage] = applyViewMonthChange(model, model.viewYear, month, jumpDirection(model, model.viewYear, month));
374
+ return [
375
+ evo(nextModel, { viewMode: () => 'Days' }),
376
+ [...commands, focusGrid(model.id)],
377
+ maybeOutMessage,
378
+ ];
379
+ }
380
+ },
381
+ SelectedYear: ({ year }) => {
382
+ if (isYearDisabled(model, year)) {
383
+ return [model, [], Option.none()];
384
+ }
385
+ else {
386
+ const [nextModel, commands, maybeOutMessage] = applyViewMonthChange(model, year, model.viewMonth, jumpDirection(model, year, model.viewMonth));
387
+ return [
388
+ evo(nextModel, { viewMode: () => 'Months' }),
389
+ [...commands, focusGrid(model.id)],
390
+ maybeOutMessage,
391
+ ];
392
+ }
393
+ },
394
+ PagedYears: ({ direction }) => applyYearsFocusShift(model, direction * YEARS_PAGE_SIZE),
289
395
  FocusedGrid: () => [
290
396
  evo(model, { isGridFocused: () => true }),
291
397
  [],
@@ -305,7 +411,9 @@ export const update = (model, message) => M.value(message).pipe(withUpdateReturn
305
411
  }));
306
412
  // VIEW
307
413
  const headingId = (modelId) => `${modelId}-heading`;
308
- const cellId = (modelId, date) => `${modelId}-cell-${date.year}-${date.month}-${date.day}`;
414
+ const dayCellId = (modelId, date) => `${modelId}-cell-${date.year}-${date.month}-${date.day}`;
415
+ const monthCellId = (modelId, month) => `${modelId}-cell-month-${month}`;
416
+ const yearCellId = (modelId, year) => `${modelId}-cell-year-${year}`;
309
417
  const DAY_NAMES_SUNDAY_FIRST = [
310
418
  'Sunday',
311
419
  'Monday',
@@ -342,24 +450,6 @@ const buildGrid = (viewYear, viewMonth, firstDayOfWeek) => {
342
450
  const weeks = Array.makeBy(WEEKS_IN_GRID, weekIndex => Array.makeBy(DAYS_IN_WEEK, dayIndex => Calendar.addDays(gridStart, weekIndex * DAYS_IN_WEEK + dayIndex)));
343
451
  return { gridStart, weeks };
344
452
  };
345
- const YEAR_RANGE_LOOKAHEAD = 10;
346
- const YEAR_RANGE_LOOKBEHIND = 100;
347
- /** Computes the inclusive year range the year dropdown should expose,
348
- * derived from min/max constraints if set, else a sensible default window
349
- * around today. */
350
- const resolveYearRange = (model) => {
351
- const defaultStart = model.today.year - YEAR_RANGE_LOOKBEHIND;
352
- const defaultEnd = model.today.year + YEAR_RANGE_LOOKAHEAD;
353
- const start = Option.match(model.maybeMinDate, {
354
- onNone: () => defaultStart,
355
- onSome: min => min.year,
356
- });
357
- const end = Option.match(model.maybeMaxDate, {
358
- onNone: () => defaultEnd,
359
- onSome: max => max.year,
360
- });
361
- return [start, end];
362
- };
363
453
  const NAV_KEYS = new Set([
364
454
  'ArrowLeft',
365
455
  'ArrowRight',
@@ -372,52 +462,55 @@ const NAV_KEYS = new Set([
372
462
  'Enter',
373
463
  ' ',
374
464
  ]);
375
- /** Renders an accessible calendar grid. Builds ARIA attribute groups (grid,
376
- * row, gridcell, column header) plus the derived month grid, then delegates
377
- * layout to the consumer's `toView` callback. */
378
- export const view = (config) => {
379
- const { AriaActiveDescendant, AriaColcount, AriaColindex, AriaDisabled, AriaLabel, AriaRowcount, AriaRowindex, AriaSelected, DataAttribute, Id, OnBlur, OnChange, OnClick, OnFocus, OnKeyDownPreventDefault, Role, Tabindex, Type, } = html();
380
- const { model, toParentMessage, toView, onSelectedDate } = config;
465
+ const MONTHS_GRID_COLUMNS = 3;
466
+ const YEARS_GRID_COLUMNS = 3;
467
+ const MONTHS_IN_YEAR = 12;
468
+ /** Returns the start year of the 12-year window the years grid renders. */
469
+ const yearsPageStart = (viewYear) => Math.floor(viewYear / YEARS_PAGE_SIZE) * YEARS_PAGE_SIZE;
470
+ /** A month range is fully disabled when its last day is below the minimum
471
+ * or its first day is above the maximum. */
472
+ const isMonthDisabled = (model, year, month) => {
473
+ const monthStart = Calendar.make(year, month, 1);
474
+ const monthEnd = Calendar.make(year, month, Calendar.daysInMonth(year, month));
475
+ return (Option.exists(model.maybeMinDate, min => Calendar.isBefore(monthEnd, min)) ||
476
+ Option.exists(model.maybeMaxDate, max => Calendar.isAfter(monthStart, max)));
477
+ };
478
+ /** A year is fully disabled when it falls entirely below the minimum date's
479
+ * year or entirely above the maximum date's year. */
480
+ const isYearDisabled = (model, year) => Option.exists(model.maybeMinDate, min => year < min.year) ||
481
+ Option.exists(model.maybeMaxDate, max => year > max.year);
482
+ const buildDaysAttributes = (config) => {
483
+ const { AriaActiveDescendant, AriaColcount, AriaColindex, AriaDisabled, AriaLabel, AriaRowcount, AriaRowindex, AriaSelected, DataAttribute, Id, Key, OnBlur, OnClick, OnFocus, OnKeyDownPreventDefault, Role, Tabindex, Type, } = html();
484
+ const { model, toParentMessage, onSelectedDate } = config;
381
485
  const { id, viewYear, viewMonth, maybeFocusedDate, maybeSelectedDate, today, locale, isGridFocused, } = model;
382
- /** Returns the parent message to dispatch when the user commits a date. In
383
- * controlled mode (when `onSelectedDate` is provided), dispatches the
384
- * callback directly. In uncontrolled mode, routes through the internal
385
- * `ClickedDay` message so the calendar's own update manages selection. */
386
486
  const dispatchSelectedDate = (date) => onSelectedDate !== undefined
387
487
  ? onSelectedDate(date)
388
488
  : toParentMessage(ClickedDay({ date }));
389
489
  const previousMonthLabel = config.previousMonthLabel ?? 'Previous month';
390
490
  const nextMonthLabel = config.nextMonthLabel ?? 'Next month';
391
- const monthSelectLabel = config.monthSelectLabel ?? 'Select month';
392
- const yearSelectLabel = config.yearSelectLabel ?? 'Select year';
491
+ const headingButtonLabel = config.daysHeadingButtonLabel ?? 'Switch to month picker';
393
492
  const headingText = `${locale.monthNames[viewMonth - 1]} ${viewYear}`;
394
- const monthOptions = locale.monthNames.map((label, index) => ({
395
- value: index + 1,
396
- label,
397
- }));
398
- const [yearRangeStart, yearRangeEnd] = resolveYearRange(model);
399
- const yearOptions = Array.makeBy(Math.max(0, yearRangeEnd - yearRangeStart + 1), index => yearRangeStart + index);
400
493
  const rotatedDayNames = rotateDayNames(DAY_NAMES_SUNDAY_FIRST, locale.firstDayOfWeek);
401
494
  const rotatedShortDayNames = rotateDayNames(locale.shortDayNames, locale.firstDayOfWeek);
402
495
  const { gridStart, weeks: weeksDates } = buildGrid(viewYear, viewMonth, locale.firstDayOfWeek);
403
- const rootAttributes = [Id(id)];
404
- const previousMonthButtonAttributes = [
496
+ const rootAttributes = [
497
+ Id(id),
498
+ Key('Days'),
499
+ ];
500
+ const previousMonthButton = [
405
501
  Type('button'),
406
502
  AriaLabel(previousMonthLabel),
407
503
  OnClick(toParentMessage(ClickedPreviousMonthButton())),
408
504
  ];
409
- const nextMonthButtonAttributes = [
505
+ const nextMonthButton = [
410
506
  Type('button'),
411
507
  AriaLabel(nextMonthLabel),
412
508
  OnClick(toParentMessage(ClickedNextMonthButton())),
413
509
  ];
414
- const monthSelectAttributes = [
415
- AriaLabel(monthSelectLabel),
416
- OnChange(value => toParentMessage(SelectedMonthFromDropdown({ month: Number(value) }))),
417
- ];
418
- const yearSelectAttributes = [
419
- AriaLabel(yearSelectLabel),
420
- OnChange(value => toParentMessage(SelectedYearFromDropdown({ year: Number(value) }))),
510
+ const headingButton = [
511
+ Type('button'),
512
+ AriaLabel(headingButtonLabel),
513
+ OnClick(toParentMessage(ClickedHeading())),
421
514
  ];
422
515
  const handleKeyDown = (key, modifiers) => {
423
516
  if (!NAV_KEYS.has(key)) {
@@ -428,7 +521,7 @@ export const view = (config) => {
428
521
  }
429
522
  return Option.some(toParentMessage(PressedKeyOnGrid({ key, isShift: modifiers.shiftKey })));
430
523
  };
431
- const activeDescendantAttributes = pipe(maybeFocusedDate, Option.map(date => AriaActiveDescendant(cellId(id, date))), Option.toArray);
524
+ const activeDescendantAttributes = pipe(maybeFocusedDate, Option.map(date => AriaActiveDescendant(dayCellId(id, date))), Option.toArray);
432
525
  const gridAttributes = [
433
526
  Id(gridId(id)),
434
527
  Role('grid'),
@@ -470,7 +563,7 @@ export const view = (config) => {
470
563
  OptionExt.when(isDisabled, DataAttribute('disabled', '')),
471
564
  ]);
472
565
  const cellAttributes = [
473
- Id(cellId(id, date)),
566
+ Id(dayCellId(id, date)),
474
567
  Role('gridcell'),
475
568
  AriaSelected(isSelected),
476
569
  AriaColindex(columnIndex + 1),
@@ -506,21 +599,216 @@ export const view = (config) => {
506
599
  cells: weekDates.map(buildDayCell),
507
600
  };
508
601
  });
509
- return toView({
602
+ return {
603
+ _tag: 'Days',
510
604
  root: rootAttributes,
511
- previousMonthButton: previousMonthButtonAttributes,
512
- nextMonthButton: nextMonthButtonAttributes,
605
+ previousMonthButton,
606
+ nextMonthButton,
607
+ headingButton,
513
608
  heading: { id: headingId(id), text: headingText },
514
- monthSelect: monthSelectAttributes,
515
- monthOptions,
516
- yearSelect: yearSelectAttributes,
517
- yearOptions,
518
609
  grid: gridAttributes,
519
610
  headerRow: headerRowAttributes,
520
611
  columnHeaders,
521
612
  weeks,
613
+ };
614
+ };
615
+ const buildMonthsAttributes = (config) => {
616
+ const { AriaActiveDescendant, AriaDisabled, AriaLabel, AriaSelected, DataAttribute, Id, Key, OnBlur, OnClick, OnFocus, OnKeyDownPreventDefault, Role, Tabindex, Type, } = html();
617
+ const { model, toParentMessage } = config;
618
+ const { id, viewYear, viewMonth, maybeFocusedDate, today, locale, isGridFocused, } = model;
619
+ const headingButtonLabel = config.monthsHeadingButtonLabel ?? 'Switch to year picker';
620
+ const headingText = `${viewYear}`;
621
+ const rootAttributes = [
622
+ Id(id),
623
+ Key('Months'),
624
+ ];
625
+ const headingButton = [
626
+ Type('button'),
627
+ AriaLabel(headingButtonLabel),
628
+ OnClick(toParentMessage(ClickedHeading())),
629
+ ];
630
+ const focusedMonth = Option.match(maybeFocusedDate, {
631
+ onNone: () => viewMonth,
632
+ onSome: date => (date.year === viewYear ? date.month : viewMonth),
522
633
  });
634
+ const handleKeyDown = (key, modifiers) => {
635
+ if (!NAV_KEYS.has(key)) {
636
+ return Option.none();
637
+ }
638
+ else if (isCommitKey(key)) {
639
+ return OptionExt.when(!isMonthDisabled(model, viewYear, focusedMonth), toParentMessage(SelectedMonth({ month: focusedMonth })));
640
+ }
641
+ else {
642
+ return Option.some(toParentMessage(PressedKeyOnGrid({ key, isShift: modifiers.shiftKey })));
643
+ }
644
+ };
645
+ const activeDescendantAttributes = [
646
+ AriaActiveDescendant(monthCellId(id, focusedMonth)),
647
+ ];
648
+ const gridAttributes = [
649
+ Id(gridId(id)),
650
+ Role('grid'),
651
+ AriaLabel(`Month picker, ${headingText}`),
652
+ Tabindex(0),
653
+ OnFocus(toParentMessage(FocusedGrid())),
654
+ OnBlur(toParentMessage(BlurredGrid())),
655
+ OnKeyDownPreventDefault(handleKeyDown),
656
+ ...activeDescendantAttributes,
657
+ ];
658
+ const buildMonthCell = (month) => {
659
+ const label = locale.monthNames[month - 1] ?? String(month);
660
+ const shortLabel = locale.shortMonthNames[month - 1] ?? label;
661
+ const isSelected = month === viewMonth;
662
+ const isFocused = month === focusedMonth;
663
+ const isCurrentMonth = today.year === viewYear && today.month === month;
664
+ const isDisabled = isMonthDisabled(model, viewYear, month);
665
+ const stateDataAttributes = Array.getSomes([
666
+ OptionExt.when(isCurrentMonth, DataAttribute('today', '')),
667
+ OptionExt.when(isSelected, DataAttribute('selected', '')),
668
+ OptionExt.when(isFocused && isGridFocused, DataAttribute('focused', '')),
669
+ OptionExt.when(isDisabled, DataAttribute('disabled', '')),
670
+ ]);
671
+ const cellAttributes = [
672
+ Id(monthCellId(id, month)),
673
+ Role('gridcell'),
674
+ AriaSelected(isSelected),
675
+ ...stateDataAttributes,
676
+ ];
677
+ const buttonAttributes = [
678
+ Type('button'),
679
+ Tabindex(-1),
680
+ AriaLabel(`${label} ${viewYear}`),
681
+ AriaDisabled(isDisabled),
682
+ ...(isDisabled
683
+ ? []
684
+ : [OnClick(toParentMessage(SelectedMonth({ month })))]),
685
+ ];
686
+ return {
687
+ month,
688
+ label,
689
+ shortLabel,
690
+ cellAttributes,
691
+ buttonAttributes,
692
+ isSelected,
693
+ isFocused: isFocused && isGridFocused,
694
+ isCurrentMonth,
695
+ isDisabled,
696
+ };
697
+ };
698
+ const cells = Array.makeBy(MONTHS_IN_YEAR, monthIndex => buildMonthCell(monthIndex + 1));
699
+ return {
700
+ _tag: 'Months',
701
+ root: rootAttributes,
702
+ headingButton,
703
+ heading: { id: headingId(id), text: headingText },
704
+ grid: gridAttributes,
705
+ cells,
706
+ };
707
+ };
708
+ const buildYearsAttributes = (config) => {
709
+ const { AriaActiveDescendant, AriaDisabled, AriaLabel, AriaSelected, DataAttribute, Id, Key, OnBlur, OnClick, OnFocus, OnKeyDownPreventDefault, Role, Tabindex, Type, } = html();
710
+ const { model, toParentMessage } = config;
711
+ const { id, viewYear, maybeFocusedDate, today, isGridFocused } = model;
712
+ const previousYearsPageLabel = config.previousYearsPageLabel ?? 'Previous 12 years';
713
+ const nextYearsPageLabel = config.nextYearsPageLabel ?? 'Next 12 years';
714
+ const cursorYear = Option.match(maybeFocusedDate, {
715
+ onNone: () => viewYear,
716
+ onSome: date => date.year,
717
+ });
718
+ const pageStart = yearsPageStart(cursorYear);
719
+ const pageEnd = pageStart + YEARS_PAGE_SIZE - 1;
720
+ const headingText = `${pageStart}–${pageEnd}`;
721
+ const rootAttributes = [
722
+ Id(id),
723
+ Key('Years'),
724
+ ];
725
+ const previousPageButton = [
726
+ Type('button'),
727
+ AriaLabel(previousYearsPageLabel),
728
+ OnClick(toParentMessage(PagedYears({ direction: -1 }))),
729
+ ];
730
+ const nextPageButton = [
731
+ Type('button'),
732
+ AriaLabel(nextYearsPageLabel),
733
+ OnClick(toParentMessage(PagedYears({ direction: 1 }))),
734
+ ];
735
+ const focusedYear = cursorYear;
736
+ const handleKeyDown = (key, modifiers) => {
737
+ if (!NAV_KEYS.has(key)) {
738
+ return Option.none();
739
+ }
740
+ else if (isCommitKey(key)) {
741
+ return OptionExt.when(!isYearDisabled(model, focusedYear), toParentMessage(SelectedYear({ year: focusedYear })));
742
+ }
743
+ else {
744
+ return Option.some(toParentMessage(PressedKeyOnGrid({ key, isShift: modifiers.shiftKey })));
745
+ }
746
+ };
747
+ const activeDescendantAttributes = [
748
+ AriaActiveDescendant(yearCellId(id, focusedYear)),
749
+ ];
750
+ const gridAttributes = [
751
+ Id(gridId(id)),
752
+ Role('grid'),
753
+ AriaLabel(`Year picker, ${headingText}`),
754
+ Tabindex(0),
755
+ OnFocus(toParentMessage(FocusedGrid())),
756
+ OnBlur(toParentMessage(BlurredGrid())),
757
+ OnKeyDownPreventDefault(handleKeyDown),
758
+ ...activeDescendantAttributes,
759
+ ];
760
+ const buildYearCell = (year) => {
761
+ const label = String(year);
762
+ const isSelected = year === viewYear;
763
+ const isFocused = year === focusedYear;
764
+ const isCurrentYear = today.year === year;
765
+ const isDisabled = isYearDisabled(model, year);
766
+ const stateDataAttributes = Array.getSomes([
767
+ OptionExt.when(isCurrentYear, DataAttribute('today', '')),
768
+ OptionExt.when(isSelected, DataAttribute('selected', '')),
769
+ OptionExt.when(isFocused && isGridFocused, DataAttribute('focused', '')),
770
+ OptionExt.when(isDisabled, DataAttribute('disabled', '')),
771
+ ]);
772
+ const cellAttributes = [
773
+ Id(yearCellId(id, year)),
774
+ Role('gridcell'),
775
+ AriaSelected(isSelected),
776
+ ...stateDataAttributes,
777
+ ];
778
+ const buttonAttributes = [
779
+ Type('button'),
780
+ Tabindex(-1),
781
+ AriaLabel(label),
782
+ AriaDisabled(isDisabled),
783
+ ...(isDisabled ? [] : [OnClick(toParentMessage(SelectedYear({ year })))]),
784
+ ];
785
+ return {
786
+ year,
787
+ label,
788
+ cellAttributes,
789
+ buttonAttributes,
790
+ isSelected,
791
+ isFocused: isFocused && isGridFocused,
792
+ isCurrentYear,
793
+ isDisabled,
794
+ };
795
+ };
796
+ const cells = Array.makeBy(YEARS_PAGE_SIZE, offset => buildYearCell(pageStart + offset));
797
+ return {
798
+ _tag: 'Years',
799
+ root: rootAttributes,
800
+ previousPageButton,
801
+ nextPageButton,
802
+ heading: { id: headingId(id), text: headingText },
803
+ grid: gridAttributes,
804
+ cells,
805
+ };
523
806
  };
807
+ /** Renders an accessible calendar. Builds mode-specific ARIA attribute
808
+ * groups and derived cell data, then delegates layout to the consumer's
809
+ * `toView` callback. The variant of `CalendarAttributes` passed to `toView`
810
+ * matches `model.viewMode`. */
811
+ export const view = (config) => config.toView(M.value(config.model.viewMode).pipe(M.withReturnType(), M.when('Days', () => buildDaysAttributes(config)), M.when('Months', () => buildMonthsAttributes(config)), M.when('Years', () => buildYearsAttributes(config)), M.exhaustive));
524
812
  /** Creates a memoized calendar view. Static config is captured in a closure;
525
813
  * only `model` and `toParentMessage` are compared per render via `createLazy`. */
526
814
  export const lazy = (staticConfig) => {
@@ -1,3 +1,3 @@
1
- export { init, update, view, lazy, focusGrid, selectDate, setMinDate, setMaxDate, setDisabledDates, setDisabledDaysOfWeek, Model, Message, OutMessage, ChangedViewMonth, ClickedDay, PressedKeyOnGrid, ClickedPreviousMonthButton, ClickedNextMonthButton, SelectedMonthFromDropdown, SelectedYearFromDropdown, FocusedGrid, BlurredGrid, RefreshedToday, CompletedFocusGrid, FocusGrid, } from './index.js';
2
- export type { InitConfig, ViewConfig, CalendarAttributes, DayCell, ColumnHeader, Week, } from './index.js';
1
+ export { init, update, view, lazy, focusGrid, selectDate, setMinDate, setMaxDate, setDisabledDates, setDisabledDaysOfWeek, dropToDays, Model, ViewMode, Message, OutMessage, ChangedViewMonth, ClickedDay, PressedKeyOnGrid, ClickedPreviousMonthButton, ClickedNextMonthButton, ClickedHeading, SelectedMonth, SelectedYear, PagedYears, FocusedGrid, BlurredGrid, RefreshedToday, CompletedFocusGrid, FocusGrid, } from './index.js';
2
+ export type { InitConfig, ViewConfig, CalendarAttributes, DaysModeAttributes, MonthsModeAttributes, YearsModeAttributes, DayCell, ColumnHeader, Week, MonthCell, YearCell, } from './index.js';
3
3
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/calendar/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,EACL,OAAO,EACP,UAAU,EACV,gBAAgB,EAChB,UAAU,EACV,gBAAgB,EAChB,0BAA0B,EAC1B,sBAAsB,EACtB,yBAAyB,EACzB,wBAAwB,EACxB,WAAW,EACX,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,OAAO,EACP,YAAY,EACZ,IAAI,GACL,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/calendar/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,qBAAqB,EACrB,UAAU,EACV,KAAK,EACL,QAAQ,EACR,OAAO,EACP,UAAU,EACV,gBAAgB,EAChB,UAAU,EACV,gBAAgB,EAChB,0BAA0B,EAC1B,sBAAsB,EACtB,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,OAAO,EACP,YAAY,EACZ,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- export { init, update, view, lazy, focusGrid, selectDate, setMinDate, setMaxDate, setDisabledDates, setDisabledDaysOfWeek, Model, Message, OutMessage, ChangedViewMonth, ClickedDay, PressedKeyOnGrid, ClickedPreviousMonthButton, ClickedNextMonthButton, SelectedMonthFromDropdown, SelectedYearFromDropdown, FocusedGrid, BlurredGrid, RefreshedToday, CompletedFocusGrid, FocusGrid, } from './index.js';
1
+ export { init, update, view, lazy, focusGrid, selectDate, setMinDate, setMaxDate, setDisabledDates, setDisabledDaysOfWeek, dropToDays, Model, ViewMode, Message, OutMessage, ChangedViewMonth, ClickedDay, PressedKeyOnGrid, ClickedPreviousMonthButton, ClickedNextMonthButton, ClickedHeading, SelectedMonth, SelectedYear, PagedYears, FocusedGrid, BlurredGrid, RefreshedToday, CompletedFocusGrid, FocusGrid, } from './index.js';
@@ -24,6 +24,7 @@ export declare const Model: S.Struct<{
24
24
  }>>;
25
25
  viewYear: typeof S.Int;
26
26
  viewMonth: S.filter<typeof S.Int>;
27
+ viewMode: S.Literal<["Days", "Months", "Years"]>;
27
28
  maybeFocusedDate: S.OptionFromSelf<S.filter<S.Struct<{
28
29
  year: typeof S.Int;
29
30
  month: S.filter<typeof S.Int>;
@@ -85,10 +86,12 @@ export declare const GotCalendarMessage: import("../../schema/index.js").Callabl
85
86
  }>, import("../../schema/index.js").CallableTaggedStruct<"PressedKeyOnGrid", {
86
87
  key: typeof S.String;
87
88
  isShift: typeof S.Boolean;
88
- }>, import("../../schema/index.js").CallableTaggedStruct<"ClickedPreviousMonthButton", {}>, import("../../schema/index.js").CallableTaggedStruct<"ClickedNextMonthButton", {}>, import("../../schema/index.js").CallableTaggedStruct<"SelectedMonthFromDropdown", {
89
+ }>, import("../../schema/index.js").CallableTaggedStruct<"ClickedPreviousMonthButton", {}>, import("../../schema/index.js").CallableTaggedStruct<"ClickedNextMonthButton", {}>, import("../../schema/index.js").CallableTaggedStruct<"ClickedHeading", {}>, import("../../schema/index.js").CallableTaggedStruct<"SelectedMonth", {
89
90
  month: typeof S.Int;
90
- }>, import("../../schema/index.js").CallableTaggedStruct<"SelectedYearFromDropdown", {
91
+ }>, import("../../schema/index.js").CallableTaggedStruct<"SelectedYear", {
91
92
  year: typeof S.Int;
93
+ }>, import("../../schema/index.js").CallableTaggedStruct<"PagedYears", {
94
+ direction: S.Literal<[1, -1]>;
92
95
  }>, import("../../schema/index.js").CallableTaggedStruct<"FocusedGrid", {}>, import("../../schema/index.js").CallableTaggedStruct<"BlurredGrid", {}>, import("../../schema/index.js").CallableTaggedStruct<"RefreshedToday", {
93
96
  today: S.filter<S.Struct<{
94
97
  year: typeof S.Int;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/datePicker/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,MAAM,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhE,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAA;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,EACL,KAAK,SAAS,EACd,KAAK,IAAI,EAGV,MAAM,qBAAqB,CAAA;AAG5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,KAAK,UAAU,MAAM,sBAAsB,CAAA;AAKlD;;mDAEmD;AACnD,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,wDAAwD;AACxD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;EAE7B,CAAA;AACF,uDAAuD;AACvD,eAAO,MAAM,iBAAiB;;;;;;;EAE5B,CAAA;AACF;gEACgE;AAChE,eAAO,MAAM,YAAY;;;;;;EAAqD,CAAA;AAC9E,+EAA+E;AAC/E,eAAO,MAAM,OAAO,qEAAe,CAAA;AACnC;4EAC4E;AAC5E,eAAO,MAAM,MAAM,oEAAc,CAAA;AACjC;kCACkC;AAClC,eAAO,MAAM,MAAM,oEAAc,CAAA;AAEjC,mEAAmE;AACnE,eAAO,MAAM,OAAO,EAAE,CAAC,CAAC,KAAK,CAC3B;IACE,OAAO,kBAAkB;IACzB,OAAO,iBAAiB;IACxB,OAAO,YAAY;IACnB,OAAO,OAAO;IACd,OAAO,MAAM;IACb,OAAO,MAAM;CACd,CAQF,CAAA;AACD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC;sDACsD;AACtD,eAAO,MAAM,gBAAgB;;;EAG3B,CAAA;AAEF;;iBAEiB;AACjB,eAAO,MAAM,UAAU;;;EAAmB,CAAA;AAC1C,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAA;AAI/C,kEAAkE;AAClE,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,YAAY,CAAA;IACnB,mBAAmB,CAAC,EAAE,YAAY,CAAA;IAClC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,MAAM,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAA;IAC9B,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,kBAAkB,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IACtD,aAAa,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;CAC5C,CAAC,CAAA;AAEF;;;6CAG6C;AAC7C,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAwBxC,CAAA;AAIF,KAAK,YAAY,GAAG,SAAS;IAC3B,KAAK;IACL,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC;CAC1B,CAAA;AA0DD;0BAC0B;AAC1B,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,SAAS,OAAO,KAAG,YAsDrD,CAAA;AAEH;oEACoE;AACpE,eAAO,MAAM,IAAI,GACf,OAAO,KAAK,KACX,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAG1D,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,KAAK,GAChB,OAAO,KAAK,KACX,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAG1D,CAAA;AAED,8EAA8E;AAC9E,eAAO,MAAM,UAAU,GACrB,OAAO,KAAK,EACZ,MAAM,YAAY,KACjB,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAK1D,CAAA;AAED,iDAAiD;AACjD,eAAO,MAAM,KAAK,GAChB,OAAO,KAAK,KACX,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAG1D,CAAA;AAED;;;;;;;uEAOuE;AACvE,eAAO,MAAM,UAAU,GACrB,OAAO,KAAK,EACZ,cAAc,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,KACxC,KAGC,CAAA;AAEJ;;gBAEgB;AAChB,eAAO,MAAM,UAAU,GACrB,OAAO,KAAK,EACZ,cAAc,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,KACxC,KAGC,CAAA;AAEJ;6EAC6E;AAC7E,eAAO,MAAM,gBAAgB,GAC3B,OAAO,KAAK,EACZ,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,KAGC,CAAA;AAEJ;;wBAEwB;AACxB,eAAO,MAAM,qBAAqB,GAChC,OAAO,KAAK,EACZ,oBAAoB,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,KACpD,KAIC,CAAA;AAaJ,6DAA6D;AAC7D,MAAM,MAAM,UAAU,CAAC,aAAa,IAAI,QAAQ,CAAC;IAC/C,KAAK,EAAE,KAAK,CAAA;IACZ,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,aAAa,CAAA;IACpD;;;;qDAIiD;IACjD,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,aAAa,CAAA;IACtD,MAAM,EAAE,YAAY,CAAA;IACpB;iEAC6D;IAC7D,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,IAAI,CAAA;IAChE;;8DAE0D;IAC1D,cAAc,EAAE,CACd,UAAU,EAAE,UAAU,CAAC,kBAAkB,CAAC,aAAa,CAAC,KACrD,IAAI,CAAA;IACT,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;wDAEoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAA;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,iBAAiB,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAA;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAA;IACzD,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,kBAAkB,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAA;CAC7D,CAAC,CAAA;AAEF;;;;gEAIgE;AAChE,eAAO,MAAM,IAAI,GAAI,aAAa,EAChC,QAAQ,UAAU,CAAC,aAAa,CAAC,KAChC,IAuEF,CAAA;AAED;mFACmF;AACnF,eAAO,MAAM,IAAI,GAAI,aAAa,EAChC,cAAc,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAAC,KACzE,CAAC,CACF,KAAK,EAAE,KAAK,EACZ,eAAe,EAAE,UAAU,CAAC,aAAa,CAAC,CAAC,iBAAiB,CAAC,KAC1D,IAAI,CAgBR,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/datePicker/index.ts"],"names":[],"mappings":"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"}
@@ -108,7 +108,10 @@ export const update = (model, message) => M.value(message).pipe(withUpdateReturn
108
108
  Opened: () => {
109
109
  const [nextPopover, popoverCommands] = Popover.open(model.popover);
110
110
  return [
111
- evo(model, { popover: () => nextPopover }),
111
+ evo(model, {
112
+ popover: () => nextPopover,
113
+ calendar: () => UiCalendar.dropToDays(model.calendar),
114
+ }),
112
115
  mapPopoverCommands(popoverCommands),
113
116
  Option.none(),
114
117
  ];
@@ -116,7 +119,10 @@ export const update = (model, message) => M.value(message).pipe(withUpdateReturn
116
119
  Closed: () => {
117
120
  const [nextPopover, popoverCommands] = Popover.close(model.popover);
118
121
  return [
119
- evo(model, { popover: () => nextPopover }),
122
+ evo(model, {
123
+ popover: () => nextPopover,
124
+ calendar: () => UiCalendar.dropToDays(model.calendar),
125
+ }),
120
126
  mapPopoverCommands(popoverCommands),
121
127
  Option.none(),
122
128
  ];
@@ -68,8 +68,26 @@ export declare const update: (model: Model, message: Message) => readonly [Model
68
68
  *
69
69
  * Should be called after the container has rendered. If the container is not
70
70
  * yet in the DOM the Command silently no-ops (the model still transitions
71
- * through `ScrollingToIndex` → `Idle` via the version-matched completion). */
71
+ * through `ScrollingToIndex` → `Idle` via the version-matched completion).
72
+ *
73
+ * Assumes uniform row heights: target scroll position is computed as
74
+ * `index * model.rowHeightPx`. For variable-height rows, use
75
+ * `scrollToIndexVariable`. */
72
76
  export declare const scrollToIndex: (model: Model, index: number) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
77
+ /** Variable-height counterpart of `scrollToIndex`. Walks the heights of items
78
+ * before `index` to compute the target `scrollTop`. Use this when rendering
79
+ * the list with `itemToRowHeightPx`; use `scrollToIndex` for uniform heights.
80
+ *
81
+ * Out-of-range indices clamp to the corresponding edge: negative or zero
82
+ * scrolls to the top, indices past the end scroll past the last row.
83
+ *
84
+ * Note: when restoring `initialScrollTop` on the first measurement of a
85
+ * variable-height list, the runtime falls back to uniform-height math (using
86
+ * `model.rowHeightPx`) because items aren't reachable from the `update`
87
+ * function. Consumers who need an accurate initial scroll on a
88
+ * variable-height list should call `scrollToIndexVariable` after the first
89
+ * `MeasuredContainer` arrives. */
90
+ export declare const scrollToIndexVariable: <Item>(model: Model, items: ReadonlyArray<Item>, itemToRowHeightPx: (item: Item, index: number) => number, index: number) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
73
91
  /** Slice of the data array that the view should render, plus the spacer
74
92
  * heights that keep the scrollbar physically correct. The first row in the
75
93
  * slice corresponds to data index `startIndex`. */
@@ -82,10 +100,25 @@ export type VisibleWindow = Readonly<{
82
100
  /** Computes the visible slice of a data array given the current scroll
83
101
  * position, container height, row height, and an overscan buffer.
84
102
  *
103
+ * Assumes uniform row heights via `model.rowHeightPx`. For variable-height
104
+ * rows, use `visibleWindowVariable`.
105
+ *
85
106
  * Returns `Option.none()` when the container has not yet been measured;
86
107
  * callers should render a placeholder (or `Html.empty`) and wait for the
87
108
  * first `MeasuredContainer` message. */
88
109
  export declare const visibleWindow: (model: Model, itemCount: number, overscan: number) => Option.Option<VisibleWindow>;
110
+ /** Variable-height counterpart of `visibleWindow`. Walks the heights of every
111
+ * item to build a prefix-sum array, then locates the visible slice with two
112
+ * linear searches.
113
+ *
114
+ * Cost is O(N) per call, walking the whole `items` array once to build the
115
+ * prefix sums. For lists in the 10k-item range, this comfortably fits inside
116
+ * a 60Hz scroll budget. Larger lists or hotter scroll paths can layer a
117
+ * prefix-sum cache invalidated when items change; that lives behind the same
118
+ * return shape so consumers don't have to know.
119
+ *
120
+ * Returns `Option.none()` when the container has not yet been measured. */
121
+ export declare const visibleWindowVariable: <Item>(model: Model, items: ReadonlyArray<Item>, itemToRowHeightPx: (item: Item, index: number) => number, overscan: number) => Option.Option<VisibleWindow>;
89
122
  /** Schema describing the subscription dependencies for container scroll and
90
123
  * resize tracking. */
91
124
  export declare const SubscriptionDeps: S.Struct<{
@@ -150,6 +183,14 @@ export type ViewConfig<Message, Item> = Readonly<{
150
183
  items: ReadonlyArray<Item>;
151
184
  itemToKey: (item: Item, index: number) => string;
152
185
  itemToView: (item: Item, index: number) => Html;
186
+ /** Optional per-item row height in pixels. When provided, the list renders
187
+ * with variable-height rows: each row's wrapper takes the height returned
188
+ * by this callback, and scroll math walks the items to compute the visible
189
+ * slice and spacers. When absent, all rows use `model.rowHeightPx`. Use
190
+ * this for tables with wrapping cells, taller detail rows, or any list
191
+ * where rows differ. Prefer the uniform `rowHeightPx` path when row
192
+ * heights are stable: it avoids the per-render walk over `items`. */
193
+ itemToRowHeightPx?: (item: Item, index: number) => number;
153
194
  /** Number of rows rendered above and below the visible viewport. Higher
154
195
  * values smooth out fast scroll at the cost of mounting more DOM. Default
155
196
  * is 5; react-window uses 1 and react-virtualized uses 3. Pick a value
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,MAAM,EACN,MAAM,IAAI,CAAC,EAGZ,MAAM,QAAQ,CAAA;AAEf,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,EACL,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,OAAO,EAGb,MAAM,qBAAqB,CAAA;AA6B5B;0DAC0D;AAC1D,eAAO,MAAM,KAAK;;;;;;;;;;;;EAOhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC;kCACkC;AAClC,eAAO,MAAM,iBAAiB;;EAE5B,CAAA;AACF;uCACuC;AACvC,eAAO,MAAM,iBAAiB;;EAE5B,CAAA;AACF;8DAC8D;AAC9D,eAAO,MAAM,oBAAoB;;EAE/B,CAAA;AAEF,oEAAoE;AACpE,eAAO,MAAM,OAAO,EAAE,CAAC,CAAC,KAAK,CAC3B;IACE,OAAO,iBAAiB;IACxB,OAAO,iBAAiB;IACxB,OAAO,oBAAoB;CAC5B,CACoE,CAAA;AAEvE,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAC,IAAI,CAAA;AAC7D,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAC,IAAI,CAAA;AAE7D,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAC,CAAA;AAEF;;kBAEkB;AAClB,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAOxC,CAAA;AAIF,eAAO,MAAM,WAAW;;;EAAsD,CAAA;AAiB9E,gFAAgF;AAChF,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CA6CxD,CAAA;AAiBH;;;;;;;;;;;;;;;+BAe+B;AAC/B,eAAO,MAAM,aAAa,GACxB,OAAO,KAAK,EACZ,OAAO,MAAM,KACZ,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CACE,CAAA;AAE7D;;;;;;;;;;;;mCAYmC;AACnC,eAAO,MAAM,qBAAqB,GAAI,IAAI,EACxC,OAAO,KAAK,EACZ,OAAO,aAAa,CAAC,IAAI,CAAC,EAC1B,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,EACxD,OAAO,MAAM,KACZ,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAQ1D,CAAA;AAID;;oDAEoD;AACpD,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,kBAAkB,EAAE,MAAM,CAAA;CAC3B,CAAC,CAAA;AAoBF;;;;;;;;yCAQyC;AACzC,eAAO,MAAM,aAAa,GACxB,OAAO,KAAK,EACZ,WAAW,MAAM,EACjB,UAAU,MAAM,KACf,MAAM,CAAC,MAAM,CAAC,aAAa,CA2B3B,CAAA;AAEH;;;;;;;;;;4EAU4E;AAC5E,eAAO,MAAM,qBAAqB,GAAI,IAAI,EACxC,OAAO,KAAK,EACZ,OAAO,aAAa,CAAC,IAAI,CAAC,EAC1B,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,EACxD,UAAU,MAAM,KACf,MAAM,CAAC,MAAM,CAAC,aAAa,CAkD3B,CAAA;AAOH;uBACuB;AACvB,eAAO,MAAM,gBAAgB;;;;EAI3B,CAAA;AAEF;;;;;;;;;;;;mEAYmE;AACnE,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA2FxB,CAAA;AAMF;;;;;;4CAM4C;AAC5C,MAAM,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,IAAI,QAAQ,CAAC;IAC/C,KAAK,EAAE,KAAK,CAAA;IACZ,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,CAAA;IAC1B,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAChD,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C;;;;;;0EAMsE;IACtE,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACzD;;;2CAGuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;CAC/C,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;uCAgBuC;AACvC,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,IAAI,EAChC,QAAQ,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,KAChC,IA0FF,CAAA;AAED;;oBAEoB;AACpB,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,IAAI,EAChC,cAAc,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,KAC/D,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,CAarD,CAAA"}
@@ -1,4 +1,4 @@
1
- import { Array, Effect, Match as M, Number, Option, Schema as S, Stream, } from 'effect';
1
+ import { Array, Effect, Match as M, Number, Option, Schema as S, Stream, pipe, } from 'effect';
2
2
  import * as Command from '../../command/index.js';
3
3
  import { createLazy, html, } from '../../html/index.js';
4
4
  import { m } from '../../message/index.js';
@@ -110,6 +110,16 @@ export const update = (model, message) => M.value(message).pipe(M.withReturnType
110
110
  }
111
111
  },
112
112
  }));
113
+ const buildScrollToIndex = (model, index, targetScrollTop) => {
114
+ const nextVersion = Number.increment(model.pendingScrollVersion);
115
+ return [
116
+ evo(model, {
117
+ pendingScrollVersion: () => nextVersion,
118
+ pendingScroll: () => ScrollingToIndex({ index, version: nextVersion }),
119
+ }),
120
+ [applyScroll(model.id, targetScrollTop, nextVersion)],
121
+ ];
122
+ };
113
123
  /** Programmatically scrolls the container so the row at `index` is visible.
114
124
  * Returns the next model and a Command that mutates `element.scrollTop`. The
115
125
  * natural scroll event then flows back through `ScrolledContainer` and the
@@ -121,22 +131,42 @@ export const update = (model, message) => M.value(message).pipe(M.withReturnType
121
131
  *
122
132
  * Should be called after the container has rendered. If the container is not
123
133
  * yet in the DOM the Command silently no-ops (the model still transitions
124
- * through `ScrollingToIndex` → `Idle` via the version-matched completion). */
125
- export const scrollToIndex = (model, index) => {
126
- const nextVersion = Number.increment(model.pendingScrollVersion);
127
- const targetScrollTop = index * model.rowHeightPx;
128
- return [
129
- evo(model, {
130
- pendingScrollVersion: () => nextVersion,
131
- pendingScroll: () => ScrollingToIndex({ index, version: nextVersion }),
132
- }),
133
- [applyScroll(model.id, targetScrollTop, nextVersion)],
134
- ];
134
+ * through `ScrollingToIndex` → `Idle` via the version-matched completion).
135
+ *
136
+ * Assumes uniform row heights: target scroll position is computed as
137
+ * `index * model.rowHeightPx`. For variable-height rows, use
138
+ * `scrollToIndexVariable`. */
139
+ export const scrollToIndex = (model, index) => buildScrollToIndex(model, index, index * model.rowHeightPx);
140
+ /** Variable-height counterpart of `scrollToIndex`. Walks the heights of items
141
+ * before `index` to compute the target `scrollTop`. Use this when rendering
142
+ * the list with `itemToRowHeightPx`; use `scrollToIndex` for uniform heights.
143
+ *
144
+ * Out-of-range indices clamp to the corresponding edge: negative or zero
145
+ * scrolls to the top, indices past the end scroll past the last row.
146
+ *
147
+ * Note: when restoring `initialScrollTop` on the first measurement of a
148
+ * variable-height list, the runtime falls back to uniform-height math (using
149
+ * `model.rowHeightPx`) because items aren't reachable from the `update`
150
+ * function. Consumers who need an accurate initial scroll on a
151
+ * variable-height list should call `scrollToIndexVariable` after the first
152
+ * `MeasuredContainer` arrives. */
153
+ export const scrollToIndexVariable = (model, items, itemToRowHeightPx, index) => {
154
+ const cumulativeOffsets = prefixSum(items, itemToRowHeightPx);
155
+ const targetScrollTop = pipe(cumulativeOffsets, Array.get(Math.max(0, index)), Option.getOrElse(() => lastOrZero(cumulativeOffsets)));
156
+ return buildScrollToIndex(model, index, targetScrollTop);
135
157
  };
136
158
  const clampIndex = (index, itemCount) => Math.max(0, Math.min(index, itemCount));
159
+ const prefixSum = (items, itemToRowHeightPx) => {
160
+ const heights = Array.map(items, itemToRowHeightPx);
161
+ return Array.scan(heights, 0, (cumulative, height) => cumulative + height);
162
+ };
163
+ const lastOrZero = (values) => pipe(values, Array.last, Option.getOrElse(() => 0));
137
164
  /** Computes the visible slice of a data array given the current scroll
138
165
  * position, container height, row height, and an overscan buffer.
139
166
  *
167
+ * Assumes uniform row heights via `model.rowHeightPx`. For variable-height
168
+ * rows, use `visibleWindowVariable`.
169
+ *
140
170
  * Returns `Option.none()` when the container has not yet been measured;
141
171
  * callers should render a placeholder (or `Html.empty`) and wait for the
142
172
  * first `MeasuredContainer` message. */
@@ -157,6 +187,41 @@ export const visibleWindow = (model, itemCount, overscan) => M.value(model.measu
157
187
  });
158
188
  },
159
189
  }));
190
+ /** Variable-height counterpart of `visibleWindow`. Walks the heights of every
191
+ * item to build a prefix-sum array, then locates the visible slice with two
192
+ * linear searches.
193
+ *
194
+ * Cost is O(N) per call, walking the whole `items` array once to build the
195
+ * prefix sums. For lists in the 10k-item range, this comfortably fits inside
196
+ * a 60Hz scroll budget. Larger lists or hotter scroll paths can layer a
197
+ * prefix-sum cache invalidated when items change; that lives behind the same
198
+ * return shape so consumers don't have to know.
199
+ *
200
+ * Returns `Option.none()` when the container has not yet been measured. */
201
+ export const visibleWindowVariable = (model, items, itemToRowHeightPx, overscan) => M.value(model.measurement).pipe(M.withReturnType(), M.tagsExhaustive({
202
+ Unmeasured: () => Option.none(),
203
+ Measured: ({ containerHeight }) => {
204
+ const itemCount = items.length;
205
+ const cumulativeOffsets = prefixSum(items, itemToRowHeightPx);
206
+ const totalHeight = lastOrZero(cumulativeOffsets);
207
+ const firstVisibleIndex = pipe(cumulativeOffsets, Array.findFirstIndex(Number.greaterThan(model.scrollTop)), Option.match({
208
+ onNone: () => itemCount,
209
+ onSome: index => Math.max(0, index - 1),
210
+ }));
211
+ const lastVisibleIndex = pipe(cumulativeOffsets, Array.findFirstIndex(Number.greaterThanOrEqualTo(model.scrollTop + containerHeight)), Option.getOrElse(() => itemCount));
212
+ const startIndex = clampIndex(firstVisibleIndex - overscan, itemCount);
213
+ const endIndex = clampIndex(lastVisibleIndex + overscan, itemCount);
214
+ const topSpacerHeight = pipe(cumulativeOffsets, Array.get(startIndex), Option.getOrElse(() => 0));
215
+ const offsetAtEnd = pipe(cumulativeOffsets, Array.get(endIndex), Option.getOrElse(() => totalHeight));
216
+ const bottomSpacerHeight = totalHeight - offsetAtEnd;
217
+ return Option.some({
218
+ startIndex,
219
+ endIndex,
220
+ topSpacerHeight,
221
+ bottomSpacerHeight,
222
+ });
223
+ },
224
+ }));
160
225
  // SUBSCRIPTION
161
226
  const containerElement = (id) => Option.fromNullable(document.getElementById(id));
162
227
  /** Schema describing the subscription dependencies for container scroll and
@@ -278,7 +343,7 @@ const DEFAULT_OVERSCAN = 5;
278
343
  * count of currently mounted rows. */
279
344
  export const view = (config) => {
280
345
  const { AriaPosinset, AriaSetsize, Class, DataAttribute, Id, Role, Style, keyed, } = html();
281
- const { model, items, itemToKey, itemToView, overscan = DEFAULT_OVERSCAN, rowElement = 'li', className, attributes = [], } = config;
346
+ const { model, items, itemToKey, itemToView, itemToRowHeightPx, overscan = DEFAULT_OVERSCAN, rowElement = 'li', className, attributes = [], } = config;
282
347
  const containerAttributes = [
283
348
  Id(model.id),
284
349
  Role('list'),
@@ -293,7 +358,13 @@ export const view = (config) => {
293
358
  ...attributes,
294
359
  ];
295
360
  const renderContainer = (children) => keyed('ul')(model.id, containerAttributes, children);
296
- return Option.match(visibleWindow(model, items.length, overscan), {
361
+ const maybeWindow = itemToRowHeightPx !== undefined
362
+ ? visibleWindowVariable(model, items, itemToRowHeightPx, overscan)
363
+ : visibleWindow(model, items.length, overscan);
364
+ const rowHeightFor = (item, dataIndex) => itemToRowHeightPx !== undefined
365
+ ? itemToRowHeightPx(item, dataIndex)
366
+ : model.rowHeightPx;
367
+ return Option.match(maybeWindow, {
297
368
  onNone: () => renderContainer([]),
298
369
  onSome: ({ startIndex, endIndex, topSpacerHeight, bottomSpacerHeight }) => {
299
370
  const visibleItems = items.slice(startIndex, endIndex);
@@ -306,7 +377,10 @@ export const view = (config) => {
306
377
  DataAttribute('virtual-list-item-index', String(dataIndex)),
307
378
  AriaSetsize(items.length),
308
379
  AriaPosinset(dataIndex + 1),
309
- Style({ height: `${model.rowHeightPx}px`, display: 'grid' }),
380
+ Style({
381
+ height: `${rowHeightFor(item, dataIndex)}px`,
382
+ display: 'grid',
383
+ }),
310
384
  ], [itemToView(item, dataIndex)]);
311
385
  });
312
386
  return renderContainer([topSpacer, ...renderedRows, bottomSpacer]);
@@ -1,3 +1,3 @@
1
- export { init, update, scrollToIndex, view, lazy, subscriptions, visibleWindow, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, SubscriptionDeps, } from './index.js';
1
+ export { init, update, scrollToIndex, scrollToIndexVariable, view, lazy, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, SubscriptionDeps, } from './index.js';
2
2
  export type { InitConfig, ViewConfig, VisibleWindow } from './index.js';
3
3
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,aAAa,EACb,KAAK,EACL,OAAO,EACP,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/virtualList/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,qBAAqB,EACrB,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,KAAK,EACL,OAAO,EACP,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- export { init, update, scrollToIndex, view, lazy, subscriptions, visibleWindow, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, SubscriptionDeps, } from './index.js';
1
+ export { init, update, scrollToIndex, scrollToIndexVariable, view, lazy, subscriptions, visibleWindow, visibleWindowVariable, Model, Message, ScrolledContainer, MeasuredContainer, CompletedApplyScroll, SubscriptionDeps, } from './index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.78.0",
3
+ "version": "0.80.0",
4
4
  "description": "A frontend framework for TypeScript, built on Effect, using The Elm Architecture",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",