@chromvoid/headless-ui 0.1.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.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/a11y-contracts/index.d.ts +23 -0
  4. package/dist/a11y-contracts/index.js +1 -0
  5. package/dist/accordion/index.d.ts +78 -0
  6. package/dist/accordion/index.js +264 -0
  7. package/dist/adapters/index.d.ts +9 -0
  8. package/dist/adapters/index.js +1 -0
  9. package/dist/alert/index.d.ts +33 -0
  10. package/dist/alert/index.js +54 -0
  11. package/dist/alert-dialog/index.d.ts +69 -0
  12. package/dist/alert-dialog/index.js +94 -0
  13. package/dist/badge/index.d.ts +48 -0
  14. package/dist/badge/index.js +89 -0
  15. package/dist/breadcrumb/index.d.ts +55 -0
  16. package/dist/breadcrumb/index.js +77 -0
  17. package/dist/button/index.d.ts +46 -0
  18. package/dist/button/index.js +86 -0
  19. package/dist/callout/index.d.ts +41 -0
  20. package/dist/callout/index.js +63 -0
  21. package/dist/card/index.d.ts +54 -0
  22. package/dist/card/index.js +103 -0
  23. package/dist/carousel/index.d.ts +98 -0
  24. package/dist/carousel/index.js +243 -0
  25. package/dist/checkbox/index.d.ts +50 -0
  26. package/dist/checkbox/index.js +87 -0
  27. package/dist/combobox/index.d.ts +114 -0
  28. package/dist/combobox/index.js +431 -0
  29. package/dist/command-palette/index.d.ts +73 -0
  30. package/dist/command-palette/index.js +147 -0
  31. package/dist/context-menu/index.d.ts +111 -0
  32. package/dist/context-menu/index.js +372 -0
  33. package/dist/copy-button/index.d.ts +62 -0
  34. package/dist/copy-button/index.js +183 -0
  35. package/dist/core/index.d.ts +20 -0
  36. package/dist/core/index.js +2 -0
  37. package/dist/core/selection.d.ts +5 -0
  38. package/dist/core/selection.js +39 -0
  39. package/dist/core/value-range.d.ts +49 -0
  40. package/dist/core/value-range.js +134 -0
  41. package/dist/date-picker/index.d.ts +210 -0
  42. package/dist/date-picker/index.js +895 -0
  43. package/dist/dialog/index.d.ts +95 -0
  44. package/dist/dialog/index.js +153 -0
  45. package/dist/disclosure/index.d.ts +52 -0
  46. package/dist/disclosure/index.js +159 -0
  47. package/dist/drawer/index.d.ts +30 -0
  48. package/dist/drawer/index.js +39 -0
  49. package/dist/feed/index.d.ts +77 -0
  50. package/dist/feed/index.js +260 -0
  51. package/dist/grid/index.d.ts +103 -0
  52. package/dist/grid/index.js +415 -0
  53. package/dist/index.d.ts +51 -0
  54. package/dist/index.js +51 -0
  55. package/dist/input/index.d.ts +86 -0
  56. package/dist/input/index.js +156 -0
  57. package/dist/interactions/composite-navigation.d.ts +69 -0
  58. package/dist/interactions/composite-navigation.js +169 -0
  59. package/dist/interactions/index.d.ts +15 -0
  60. package/dist/interactions/index.js +4 -0
  61. package/dist/interactions/keyboard-intents.d.ts +16 -0
  62. package/dist/interactions/keyboard-intents.js +33 -0
  63. package/dist/interactions/overlay-focus.d.ts +40 -0
  64. package/dist/interactions/overlay-focus.js +93 -0
  65. package/dist/interactions/typeahead.d.ts +20 -0
  66. package/dist/interactions/typeahead.js +41 -0
  67. package/dist/landmarks/index.d.ts +39 -0
  68. package/dist/landmarks/index.js +58 -0
  69. package/dist/link/index.d.ts +34 -0
  70. package/dist/link/index.js +39 -0
  71. package/dist/listbox/index.d.ts +92 -0
  72. package/dist/listbox/index.js +337 -0
  73. package/dist/menu/index.d.ts +132 -0
  74. package/dist/menu/index.js +541 -0
  75. package/dist/menu-button/index.d.ts +71 -0
  76. package/dist/menu-button/index.js +121 -0
  77. package/dist/meter/index.d.ts +45 -0
  78. package/dist/meter/index.js +106 -0
  79. package/dist/number/index.d.ts +113 -0
  80. package/dist/number/index.js +252 -0
  81. package/dist/popover/index.d.ts +70 -0
  82. package/dist/popover/index.js +126 -0
  83. package/dist/progress/index.d.ts +49 -0
  84. package/dist/progress/index.js +79 -0
  85. package/dist/radio-group/index.d.ts +61 -0
  86. package/dist/radio-group/index.js +150 -0
  87. package/dist/select/index.d.ts +92 -0
  88. package/dist/select/index.js +239 -0
  89. package/dist/sidebar/index.d.ts +74 -0
  90. package/dist/sidebar/index.js +186 -0
  91. package/dist/slider/index.d.ts +61 -0
  92. package/dist/slider/index.js +150 -0
  93. package/dist/slider-multi-thumb/index.d.ts +70 -0
  94. package/dist/slider-multi-thumb/index.js +222 -0
  95. package/dist/spinbutton/index.d.ts +75 -0
  96. package/dist/spinbutton/index.js +214 -0
  97. package/dist/spinner/index.d.ts +1 -0
  98. package/dist/spinner/index.js +1 -0
  99. package/dist/spinner/spinner.d.ts +23 -0
  100. package/dist/spinner/spinner.js +25 -0
  101. package/dist/switch/index.d.ts +40 -0
  102. package/dist/switch/index.js +61 -0
  103. package/dist/table/index.d.ts +117 -0
  104. package/dist/table/index.js +377 -0
  105. package/dist/tabs/index.d.ts +63 -0
  106. package/dist/tabs/index.js +174 -0
  107. package/dist/textarea/index.d.ts +68 -0
  108. package/dist/textarea/index.js +137 -0
  109. package/dist/toast/index.d.ts +67 -0
  110. package/dist/toast/index.js +145 -0
  111. package/dist/toolbar/index.d.ts +59 -0
  112. package/dist/toolbar/index.js +139 -0
  113. package/dist/tooltip/index.d.ts +52 -0
  114. package/dist/tooltip/index.js +169 -0
  115. package/dist/treegrid/index.d.ts +101 -0
  116. package/dist/treegrid/index.js +463 -0
  117. package/dist/treeview/index.d.ts +68 -0
  118. package/dist/treeview/index.js +370 -0
  119. package/dist/window-splitter/index.d.ts +65 -0
  120. package/dist/window-splitter/index.js +204 -0
  121. package/package.json +92 -0
  122. package/specs/ADR-001-headless-architecture.md +461 -0
  123. package/specs/ADR-002-repo-release-model.md +108 -0
  124. package/specs/ADR-003-public-api-versioning.md +136 -0
  125. package/specs/ADR-004-focus-selection-policy.md +117 -0
  126. package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
  127. package/specs/ISSUE-BACKLOG.md +681 -0
  128. package/specs/RELEASE-CANDIDATE.md +30 -0
  129. package/specs/components/accordion.md +130 -0
  130. package/specs/components/alert-dialog.md +72 -0
  131. package/specs/components/alert.md +65 -0
  132. package/specs/components/badge.md +220 -0
  133. package/specs/components/breadcrumb.md +74 -0
  134. package/specs/components/button.md +115 -0
  135. package/specs/components/callout.md +195 -0
  136. package/specs/components/card.md +280 -0
  137. package/specs/components/carousel.md +140 -0
  138. package/specs/components/checkbox.md +172 -0
  139. package/specs/components/combobox.md +423 -0
  140. package/specs/components/command-palette.md +92 -0
  141. package/specs/components/context-menu.md +556 -0
  142. package/specs/components/copy-button.md +293 -0
  143. package/specs/components/date-picker.md +400 -0
  144. package/specs/components/dialog.md +298 -0
  145. package/specs/components/disclosure.md +257 -0
  146. package/specs/components/drawer.md +353 -0
  147. package/specs/components/feed.md +265 -0
  148. package/specs/components/grid.md +186 -0
  149. package/specs/components/input.md +254 -0
  150. package/specs/components/landmarks.md +136 -0
  151. package/specs/components/link.md +134 -0
  152. package/specs/components/listbox.md +351 -0
  153. package/specs/components/menu-button.md +76 -0
  154. package/specs/components/menu.md +623 -0
  155. package/specs/components/meter.md +149 -0
  156. package/specs/components/number.md +393 -0
  157. package/specs/components/popover.md +252 -0
  158. package/specs/components/progress.md +188 -0
  159. package/specs/components/radio-group.md +151 -0
  160. package/specs/components/select.md +144 -0
  161. package/specs/components/sidebar.md +321 -0
  162. package/specs/components/slider-multi-thumb.md +78 -0
  163. package/specs/components/slider.md +84 -0
  164. package/specs/components/spinbutton.md +140 -0
  165. package/specs/components/spinner.md +132 -0
  166. package/specs/components/switch.md +175 -0
  167. package/specs/components/table.md +403 -0
  168. package/specs/components/tabs.md +265 -0
  169. package/specs/components/textarea.md +185 -0
  170. package/specs/components/toast.md +198 -0
  171. package/specs/components/toolbar.md +278 -0
  172. package/specs/components/tooltip.md +252 -0
  173. package/specs/components/treegrid.md +281 -0
  174. package/specs/components/treeview.md +91 -0
  175. package/specs/components/window-splitter.md +297 -0
  176. package/specs/ops/git-shard-sync.md +107 -0
  177. package/specs/ops/release-checklist.md +76 -0
  178. package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
  179. package/specs/release/api-freeze-candidate.md +54 -0
  180. package/specs/release/changelog-automation.md +76 -0
  181. package/specs/release/changelog.generated.md +53 -0
  182. package/specs/release/changelog.patch.generated.md +46 -0
  183. package/specs/release/consumer-integration.md +53 -0
  184. package/specs/release/migration-notes-pre-v1.md +40 -0
  185. package/specs/release/mvp-changelog.md +57 -0
  186. package/specs/release/release-notes-template.md +61 -0
  187. package/specs/release/release-rehearsal.md +113 -0
  188. package/specs/release/semver-deprecation-dry-run.md +89 -0
  189. package/specs/release/shard-release-drill-report.md +50 -0
  190. package/specs/release/shard-release-follow-ups.md +31 -0
  191. package/specs/signals.md +208 -0
@@ -0,0 +1,400 @@
1
+ # Date Picker Component Contract
2
+
3
+ ## Purpose
4
+
5
+ `DatePicker` is a headless APG-aligned contract for a date+time input control with an editable text input and a popup calendar dialog. It combines combobox-style trigger behavior with dialog-based calendar and time selection. The component supports dual-mode commit: user edits to date/time are staged as draft state in the calendar/input and only pushed to committed value through explicit commit actions.
6
+
7
+ ## Component Files
8
+
9
+ - `src/date-picker/index.ts` - model and public `createDatePicker` API
10
+ - `src/date-picker/date-picker.test.ts` - unit behavior tests
11
+
12
+ ## Public API
13
+
14
+ ### `createDatePicker(options)`
15
+
16
+ #### CreateDatePickerOptions
17
+
18
+ | Option | Type | Default | Description |
19
+ | ---------------- | --------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------ |
20
+ | `idBase` | `string` | `'date-picker'` | Prefix for generated IDs |
21
+ | `value` | `string \| null` | `null` | Initial committed value in `YYYY-MM-DDTHH:mm` format (24-hour) |
22
+ | `required` | `boolean` | `false` | Required marker for input validation |
23
+ | `disabled` | `boolean` | `false` | Component disabled state |
24
+ | `readonly` | `boolean` | `false` | Prevents user-driven changes |
25
+ | `placeholder` | `string` | `'Select date and time'` | Placeholder for the input |
26
+ | `locale` | `string` | `'en-US'` | Locale for label/date formatting |
27
+ | `timeZone` | `'local' \| 'utc'` | `'local'` | Formatting/parsing basis for value interpretation |
28
+ | `min` | `string` | `null` | Inclusive minimum `YYYY-MM-DDTHH:mm` value |
29
+ | `max` | `string` | `null` | Inclusive maximum `YYYY-MM-DDTHH:mm` value |
30
+ | `minuteStep` | `number` | `1` | Minute granularity for draft time updates |
31
+ | `hourCycle` | `12 \| 24` | `24` | Hour input rendering format |
32
+ | `closeOnEscape` | `boolean` | `true` | Whether Escape closes dialog |
33
+ | `ariaLabel` | `string` | `'Select date and time'` | Dialog accessible label |
34
+ | `parseDateTime` | `(value: string, locale: string) => ParsedDateTime | null` | default parser for `YYYY-MM-DD`, `YYYY-MM-DDTHH:mm`, and `YYYY-MM-DD HH:mm` | Hook to customize editable parsing |
35
+ | `formatDateTime` | `(value: ParsedDateTime, locale: string) => string` | default formatter returning `YYYY-MM-DDTHH:mm` | Hook to control displayed input text |
36
+ | `onInput` | `(value: string) => void` | `undefined` | Called after user typing changes input text |
37
+ | `onCommit` | `(value: string | null) => void` | `undefined` | Called after committed value changes |
38
+ | `onClear` | `() => void` | `undefined` | Called after `clear()` succeeds |
39
+
40
+ #### Types
41
+
42
+ ```ts
43
+ interface ParsedDateTime {
44
+ date: string // YYYY-MM-DD
45
+ time: string // HH:mm
46
+ full: string // YYYY-MM-DDTHH:mm
47
+ }
48
+
49
+ interface CalendarDay {
50
+ date: string // YYYY-MM-DD
51
+ month: 'prev' | 'current' | 'next'
52
+ inRange: boolean
53
+ isToday: boolean
54
+ disabled: boolean
55
+ }
56
+ ```
57
+
58
+ ## State Signals (signal-backed)
59
+
60
+ | Signal | Type | Derived | Description |
61
+ | ------------------------- | ------------------------ | ------- | -------------------------------------------------------------------------- |
62
+ | `inputValue()` | `string` | No | Current editable text shown in the input |
63
+ | `isOpen()` | `boolean` | No | Calendar dialog open state |
64
+ | `focusedDate()` | `string \| null` | No | Focused day in the visible grid (YYYY-MM-DD) |
65
+ | `committedDate()` | `string \| null` | No | Committed date part |
66
+ | `committedTime()` | `string \| null` | No | Committed time part in `HH:mm` |
67
+ | `draftDate()` | `string \| null` | No | Working date value while dialog is open |
68
+ | `draftTime()` | `string \| null` | No | Working time value while dialog is open |
69
+ | `displayedYear()` | `number` | No | Calendar year currently rendered |
70
+ | `displayedMonth()` | `number` | No | Calendar month currently rendered (1-12) |
71
+ | `isInputFocused()` | `boolean` | No | Tracks whether combobox input is focused |
72
+ | `isCalendarFocused()` | `boolean` | No | Tracks whether calendar/time zone is focused |
73
+ | `disabled()` | `boolean` | No | Reflects disabled mode |
74
+ | `readonly()` | `boolean` | No | Reflects readonly mode |
75
+ | `required()` | `boolean` | No | Reflects required mode |
76
+ | `placeholder()` | `string` | No | Current placeholder |
77
+ | `locale()` | `string` | No | Active locale |
78
+ | `timeZone()` | `'local' \| 'utc'` | No | Active timezone mode |
79
+ | `min()` | `string \| null` | No | Lower bound |
80
+ | `max()` | `string \| null` | No | Upper bound |
81
+ | `minuteStep()` | `number` | No | Minute step |
82
+ | `hourCycle()` | `12 \| 24` | No | Hour cycle |
83
+ | `isDualCommit()` | `boolean` | Yes | Always `true` for this component spec |
84
+ | `hasCommittedSelection()` | `boolean` | Yes | `committedDate() !== null && committedTime() !== null` |
85
+ | `hasDraftSelection()` | `boolean` | Yes | `draftDate() !== null && draftTime() !== null` |
86
+ | `committedValue()` | `string \| null` | Yes | `hasCommittedSelection() ? `${committedDate()}T${committedTime()}` : null` |
87
+ | `draftValue()` | `string \| null` | Yes | `hasDraftSelection() ? `${draftDate()}T${draftTime()}` : null` |
88
+ | `canCommitInput()` | `boolean` | Yes | `parsedValue() !== null` |
89
+ | `parsedValue()` | `ParsedDateTime \| null` | Yes | result of `parseDateTime(inputValue(), locale())` |
90
+ | `inputInvalid()` | `boolean` | Yes | `inputValue().length > 0 && !canCommitInput()` |
91
+ | `visibleDays()` | `readonly CalendarDay[]` | Yes | full 6x7 visible matrix for current `displayedMonth`/`displayedYear` |
92
+ | `today()` | `string` | Yes | Local current date in `YYYY-MM-DD` |
93
+ | `selectedCellId()` | `string \| null` | Yes | `isOpen() ? draftDate() : committedDate()` |
94
+
95
+ ## Actions
96
+
97
+ - `open()` — opens dialog, initializes `draftDate/draftTime` from committed state and syncs focused date to first selectable cell
98
+ - `close()` — closes dialog, clears calendar focus and restores input focus logic
99
+ - `toggle()` — delegates to `open`/`close`
100
+ - `setInputValue(value: string)` — updates input text only (user typing); updates `inputInvalid` through derived parse result
101
+ - `commitInput()` — commits parsed `inputValue` into `committedDate/committedTime` when valid and in range
102
+ - `clear()` — clears input and both committed/draft values
103
+ - `setDisabled(value: boolean)` — updates disabled state (clears open popover)
104
+ - `setReadonly(value: boolean)` — updates readonly state
105
+ - `setRequired(value: boolean)` — updates required state
106
+ - `setPlaceholder(value: string)` — updates placeholder
107
+ - `setLocale(value: string)` — updates locale and re-renders formatted values
108
+ - `setTimeZone(value: 'local' | 'utc')` — updates timezone mode (affects `today()` and `jumpToNow()` sources)
109
+ - `setMin(value: string | null)` / `setMax(value: string | null)` — updates boundaries
110
+ - `setMinuteStep(value: number)` — updates minute grid/validation granularity
111
+ - `setHourCycle(value: 12 | 24)` — updates time parsing/formatting behavior
112
+ - `setDisplayedMonth(year: number, month: number)` — updates visible calendar month/year
113
+ - `moveMonth(offset: -1 | 1)` — prev/next month navigation
114
+ - `moveYear(offset: -1 | 1)` — prev/next year navigation
115
+ - `setFocusedDate(date: string | null)` — sets focused calendar day when valid
116
+ - `moveFocusPreviousDay()` / `moveFocusNextDay()` — keyboard date focus navigation
117
+ - `moveFocusPreviousWeek()` / `moveFocusNextWeek()` — keyboard date focus navigation
118
+ - `selectDraftDate(date: string)` — update draft date only (no commit in dual mode)
119
+ - `setDraftTime(time: string)` — set draft time only (format `HH:mm`); value is normalized by snapping to `minuteStep`
120
+ - `jumpToNow()` — sets draft to current local date/time and brings it into view (no commit)
121
+ - `commitDraft()` — explicit dual-mode commit from dialog (sets committed state and closes)
122
+ - `cancelDraft()` — discards draft and restores draft from committed state
123
+ - `handleInputKeyDown(event)` — keyboard routing on input target
124
+ - `handleDialogKeyDown(event)` — keyboard routing on dialog wrapper
125
+ - `handleCalendarKeyDown(event)` — keyboard routing on grid (day navigation/selection)
126
+ - `handleTimeKeyDown(event)` — keyboard routing for hour/minute controls
127
+ - `handleOutsidePointer()` — dialog dismissal hook from outside pointer events
128
+
129
+ ## Contracts
130
+
131
+ ### Contract Methods
132
+
133
+ - `getInputProps()` → `DatePickerInputProps`
134
+ - `getDialogProps()` → `DatePickerDialogProps`
135
+ - `getCalendarGridProps()` → `DatePickerCalendarGridProps`
136
+ - `getCalendarDayProps(date: string)` → `DatePickerCalendarDayProps`
137
+ - `getMonthNavButtonProps(direction: 'prev' | 'next')` → `DatePickerMonthNavButtonProps`
138
+ - `getYearNavButtonProps(direction: 'prev' | 'next')` → `DatePickerYearNavButtonProps`
139
+ - `getHourInputProps()` → `DatePickerTimeSegmentProps`
140
+ - `getMinuteInputProps()` → `DatePickerTimeSegmentProps`
141
+ - `getApplyButtonProps()` → `DatePickerButtonProps`
142
+ - `getCancelButtonProps()` → `DatePickerButtonProps`
143
+ - `getClearButtonProps()` → `DatePickerButtonProps`
144
+ - `getVisibleDays()` → `readonly CalendarDay[]`
145
+
146
+ ### Contract Return Types
147
+
148
+ ```ts
149
+ interface DatePickerInputProps {
150
+ id: string
151
+ role: 'combobox'
152
+ tabindex: '0'
153
+ autocomplete: 'off'
154
+ disabled: boolean
155
+ readonly?: true
156
+ required?: true
157
+ value: string
158
+ placeholder: string
159
+ 'aria-haspopup': 'dialog'
160
+ 'aria-expanded': 'true' | 'false'
161
+ 'aria-controls': string
162
+ 'aria-activedescendant'?: string
163
+ 'aria-invalid'?: 'true'
164
+ 'aria-label'?: string
165
+ onInput: (value: string) => void
166
+ onKeyDown: (event: KeyboardEvent) => void
167
+ onFocus: () => void
168
+ onBlur: () => void
169
+ }
170
+
171
+ interface DatePickerDialogProps {
172
+ id: string
173
+ role: 'dialog'
174
+ tabindex: '-1'
175
+ hidden: boolean
176
+ 'aria-modal': 'true'
177
+ 'aria-label': string
178
+ onKeyDown: (event: KeyboardEvent) => void
179
+ onPointerDownOutside: () => void
180
+ }
181
+
182
+ interface DatePickerCalendarGridProps {
183
+ id: string
184
+ role: 'grid'
185
+ tabindex: '-1'
186
+ 'aria-label': string
187
+ onKeyDown: (event: KeyboardEvent) => void
188
+ }
189
+
190
+ interface DatePickerCalendarDayProps {
191
+ id: string
192
+ role: 'gridcell'
193
+ tabindex: '0' | '-1'
194
+ 'aria-selected': 'true' | 'false'
195
+ 'aria-disabled'?: 'true'
196
+ 'aria-current'?: 'date'
197
+ 'data-date': string
198
+ onClick: () => void
199
+ onMouseEnter: () => void
200
+ }
201
+
202
+ interface DatePickerMonthNavButtonProps {
203
+ id: string
204
+ role: 'button'
205
+ tabindex: '0'
206
+ 'aria-label': 'Previous month' | 'Next month'
207
+ onClick: () => void
208
+ }
209
+
210
+ interface DatePickerYearNavButtonProps {
211
+ id: string
212
+ role: 'button'
213
+ tabindex: '0'
214
+ 'aria-label': 'Previous year' | 'Next year'
215
+ onClick: () => void
216
+ }
217
+
218
+ interface DatePickerTimeSegmentProps {
219
+ id: string
220
+ type: 'text'
221
+ inputmode: 'numeric'
222
+ 'aria-label': string
223
+ value: string
224
+ minlength: '2'
225
+ maxlength: '2'
226
+ disabled: boolean
227
+ readonly: boolean
228
+ onInput: (value: string) => void
229
+ onKeyDown: (event: KeyboardEvent) => void
230
+ }
231
+
232
+ interface DatePickerButtonProps {
233
+ id: string
234
+ role: 'button'
235
+ tabindex: '0'
236
+ 'aria-label': string
237
+ disabled: boolean
238
+ onClick: () => void
239
+ }
240
+ ```
241
+
242
+ > Calendar day ids are generated deterministically as `${idBase}-day-${date}`.
243
+
244
+ ### APG and A11y Contract
245
+
246
+ - Input follows combobox conventions with popup trigger:
247
+ - `role='combobox'`
248
+ - `aria-haspopup='dialog'`
249
+ - `aria-expanded`, `aria-controls`, `aria-activedescendant`
250
+ - Dialog:
251
+ - `role='dialog'`, `aria-modal='true'`, `hidden` synced to `!isOpen()`
252
+ - Note: the contract always provides `'aria-modal': 'true'`; `hidden` is the primary open/close signal.
253
+ - Calendar structure:
254
+ - calendar uses `role='grid'`
255
+ - day cells use `role='gridcell'`
256
+ - one logical `aria-current='date'` on the current day when visible
257
+ - Required states conveyed via attributes:
258
+ - `aria-invalid='true'` on input when parse fails or value out of range
259
+ - `aria-required='true'` is set when required and no committed value is selected
260
+
261
+ ## Behavior Contract
262
+
263
+ ### Dual Commit Model
264
+
265
+ - Draft and committed state are separate by design.
266
+ - Day/time selection in dialog updates draft only.
267
+ - Input typing updates `inputValue` only.
268
+ - `commitInput()` or `commitDraft()` are the **only** ways that mutate committed state in non-disabled/ non-readonly mode.
269
+ - `close()` never commits draft in dual commit mode.
270
+
271
+ ### Editable Input + Calendar
272
+
273
+ - Input is editable at all times (unless `readonly` / `disabled`).
274
+ - Typing updates `inputValue` immediately and emits `onInput` callback.
275
+ - Valid parsed input can be committed via `commitInput()` (typically on Enter).
276
+ - Invalid text does not change committed state and keeps `inputInvalid=true`.
277
+ - Enter in dialog footer applies draft through `commitDraft()`.
278
+
279
+ ### Calendar Interaction
280
+
281
+ - Opening dialog sets `displayedMonth/displayedYear` to committed value month if present, otherwise current month.
282
+ - Focused day defaults to committed day if exists, otherwise today if selectable, otherwise first selectable day in visible month.
283
+ - Out-of-range and disabled dates are skipped by navigation and cannot be selected.
284
+ - `selectDraftDate` never mutates committed state in dual mode.
285
+ - `commitDraft` applies draft date/time and closes if successful.
286
+ - `cancelDraft` discards draft and reverts to committed value.
287
+
288
+ ### Keyboard
289
+
290
+ #### Closed input state
291
+
292
+ - `ArrowDown` / `ArrowUp` / `Space` opens dialog and positions focus in calendar.
293
+ - `Enter` calls `commitInput()` (no-op if input is invalid / out of range).
294
+ - `Escape` has no effect when already closed.
295
+
296
+ #### Open dialog state
297
+
298
+ - `Escape` closes without commit when `closeOnEscape=true`.
299
+ - `Tab` cycles between calendar grid, hour input, minute input, and Apply/Cancel controls.
300
+ - Arrow navigation in grid follows APG calendar keyboard model:
301
+ - Left/Right: previous/next day
302
+ - Up/Down: previous/next week
303
+ - PageUp/PageDown: previous/next month
304
+ - Shift+PageUp/PageDown: previous/next year
305
+ - `Home`/`End`: first/last day in current week
306
+ - `Enter`/`Space` on focused day updates draft and sets `selectedCellId` but does not commit
307
+ - `Ctrl+Enter` or Enter on Apply button calls `commitDraft()`
308
+
309
+ ## Transition Model
310
+
311
+ ### Core State Transitions
312
+
313
+ | Event / action | Guards | Next state |
314
+ | ------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
315
+ | `open()` | `!isOpen()` and `!disabled()` | `isOpen=true`, `draftDate=committedDate`, `draftTime=committedTime`, `focusedDate` initialized |
316
+ | `close()` | any | `isOpen=false`, `draftDate=committedDate`, `draftTime=committedTime` |
317
+ | `clear()` | `!disabled() && !readonly()` | all value signals to `null`, `inputValue=''`, `isOpen` unchanged |
318
+ | `setInputValue(v)` | any | `inputValue=v` only |
319
+ | `commitInput()` | `!disabled() && !readonly() && canCommitInput()` and within bounds | `committedDate=parsed.date`, `committedTime=parsed.time`, `inputValue=formatDateTime(parsed)`, `isOpen=false` |
320
+ | `commitInput()` | input invalid or out of range | no-op on committed state, `inputInvalid=true` |
321
+ | `setDisplayedMonth(year,month)` | valid month/year | `displayedMonth/year` updated |
322
+ | `moveMonth(-1/1)` | any | `displayedMonth/year` shifted |
323
+ | `moveYear(-1/1)` | any | `displayedYear` shifted |
324
+ | `setFocusedDate(d)` | valid, non-disabled date in current month view | `focusedDate=d` |
325
+ | `selectDraftDate(d)` | date visible/selectable | `draftDate=d` |
326
+ | `setDraftTime(t)` | valid time format | `draftTime=normalizedToMinuteStep(t)` |
327
+ | `commitDraft()` | `isOpen()` and `hasDraftSelection()` and both draft values within bounds | `committedDate=draftDate`, `committedTime=draftTime`, `inputValue=formatDateTime(draft)`, `isOpen=false` |
328
+ | `commitDraft()` | draft invalid | no-op |
329
+ | `cancelDraft()` | `isOpen()` | `draftDate=committedDate`, `draftTime=committedTime`, `inputInvalid=false`, `isOpen=true` |
330
+ | `jumpToNow()` | `!disabled() && !readonly()` | `draftDate=today`, `draftTime=currentTimeAlignedToStep`, calendar month set to draft month |
331
+
332
+ ### Keyboard Transitions
333
+
334
+ | Key context | Action triggered |
335
+ | ------------------------ | ----------------------------------------------------------------------- |
336
+ | Closed, input focused | `ArrowDown`/`ArrowUp`/`Space` -> `open()`; `Enter` -> `commitInput()` |
337
+ | Open, calendar focus | `ArrowRight`/`ArrowLeft` -> `moveFocusNextDay` / `moveFocusPreviousDay` |
338
+ | Open, calendar focus | `ArrowDown`/`ArrowUp` -> `moveFocusNextWeek` / `moveFocusPreviousWeek` |
339
+ | Open, calendar focus | `PageDown`/`PageUp` -> `moveMonth(1)` / `moveMonth(-1)` |
340
+ | Open, calendar focus | `Shift+PageDown`/`Shift+PageUp` -> `moveYear(1)` / `moveYear(-1)` |
341
+ | Open, focused day | `Enter`/`Space` -> `selectDraftDate(focusedDate)` |
342
+ | Open, draft form | `Esc` -> `close()` |
343
+ | Open, focused time input | `Enter` -> `commitDraft()` |
344
+
345
+ ## Invariants
346
+
347
+ 1. `isDualCommit()` is always `true` and never writable.
348
+ 2. `committedValue` is `null` iff either `committedDate()` or `committedTime()` is `null`.
349
+ 3. `draftDate()` and `draftTime()` are always restored from committed values when dialog closes without commit.
350
+ 4. `visibleDays()` always contains day entries for exactly 6 calendar rows (42 cells).
351
+ 5. For every entry in `visibleDays()`, `inRange === (min <= date && date <= max)` when min/max are provided; when min/max are absent, `inRange === true`.
352
+ 6. Disabled/readonly modes are no-op for commit, commitInput, draft mutations, and keyboard-triggered selection.
353
+ 7. Day selection cannot land on dates where `inRange === false`.
354
+ 8. `inputInvalid` is `true` exactly when `inputValue` is non-empty and `parsedValue` is `null` or out of bounds.
355
+ 9. `getInputProps().'aria-expanded'` equals `isOpen() ? 'true' : 'false'`.
356
+ 10. `getDialogProps().hidden` is `!isOpen()`.
357
+ 11. `onInput` callback is only invoked from `setInputValue`, never from programmatic `set` actions.
358
+ 12. `onCommit` callback is invoked only from successful `commitInput` and `commitDraft`.
359
+ 13. `commitDraft()` never commits invalid draft values.
360
+
361
+ ## Adapter Expectations
362
+
363
+ UIKit (`cv-date-picker`) binds to the model as follows:
364
+
365
+ - Signals read:
366
+ - `state.inputValue()` / `state.isOpen()` / `state.isInputFocused()` / `state.isCalendarFocused()`
367
+ - `state.disabled()` / `state.readonly()` / `state.required()` / `state.placeholder()`
368
+ - `state.visibleDays()` / `state.focusedDate()` / `state.displayedMonth()` / `state.displayedYear()`
369
+ - `state.hasCommittedSelection()` / `state.committedValue()` / `state.canCommitInput()` / `state.inputInvalid()`
370
+ - `state.min()` / `state.max()` / `state.hourCycle()` / `state.minuteStep()`
371
+
372
+ - Actions called:
373
+ - `open()` / `close()` / `toggle()`
374
+ - `setInputValue(value)` / `commitInput()` / `clear()`
375
+ - `moveMonth()` / `moveYear()` / `setDisplayedMonth()` / `setFocusedDate()`
376
+ - `moveFocusNextDay()` / `moveFocusPreviousDay()` / `moveFocusNextWeek()` / `moveFocusPreviousWeek()`
377
+ - `selectDraftDate()` / `setDraftTime()` / `commitDraft()` / `cancelDraft()` / `jumpToNow()`
378
+ - `handleInputKeyDown()` / `handleDialogKeyDown()` / `handleCalendarKeyDown()` / `handleTimeKeyDown()` / `handleOutsidePointer()`
379
+
380
+ - Contracts spread:
381
+ - `contracts.getInputProps()` on the trigger input
382
+ - `contracts.getDialogProps()` on the popup shell
383
+ - `contracts.getCalendarGridProps()` on calendar body
384
+ - `contracts.getCalendarDayProps(date)` for each visible day
385
+ - `contracts.getMonthNavButtonProps()` and `contracts.getYearNavButtonProps()`
386
+ - `contracts.getHourInputProps()` / `contracts.getMinuteInputProps()`
387
+ - `contracts.getApplyButtonProps()` / `contracts.getCancelButtonProps()` / `contracts.getClearButtonProps()`
388
+
389
+ ## Minimum Test Matrix
390
+
391
+ - open/close behavior, focus initialization, and outside-dismiss behavior
392
+ - editable input typing and validation flow
393
+ - dual commit path: dialog selection updates draft only
394
+ - apply/cancel path: commitDraft and cancelDraft semantics
395
+ - keyboard calendar navigation and focused-cell updates
396
+ - month/year navigation boundaries and disabled date skipping
397
+ - min/max boundary enforcement in input and draft selection
398
+ - minute-step rounding/validation behavior
399
+ - ARIA contract surfaces from all spread props
400
+ - disabled/readonly behavior blocking all mutating actions