@ketvietlab/design-system 0.1.25 → 0.1.30

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 (208) hide show
  1. package/README.md +130 -5
  2. package/dist/atlas/island-runtime.d.mts +2 -0
  3. package/dist/atlas/island-runtime.d.mts.map +1 -0
  4. package/dist/atlas/island-runtime.mjs +2 -0
  5. package/dist/atlas/island-runtime.mjs.map +1 -0
  6. package/dist/atlas/relation-select-hydration.d.ts +2 -0
  7. package/dist/atlas/relation-select-hydration.d.ts.map +1 -0
  8. package/dist/atlas/relation-select-hydration.js +40 -0
  9. package/dist/atlas/relation-select-hydration.js.map +1 -0
  10. package/dist/atlas-cli.d.ts.map +1 -1
  11. package/dist/atlas-cli.js +28 -10
  12. package/dist/atlas-cli.js.map +1 -1
  13. package/dist/catalogue/date-pickers.d.ts +6 -0
  14. package/dist/catalogue/date-pickers.d.ts.map +1 -0
  15. package/dist/catalogue/date-pickers.js +16 -0
  16. package/dist/catalogue/date-pickers.js.map +1 -0
  17. package/dist/catalogue/index.d.ts +7 -2
  18. package/dist/catalogue/index.d.ts.map +1 -1
  19. package/dist/catalogue/index.js +20 -4
  20. package/dist/catalogue/index.js.map +1 -1
  21. package/dist/catalogue/inventory-policy.json +49 -0
  22. package/dist/catalogue/inventory.generated.js +1 -1
  23. package/dist/catalogue/inventory.generated.js.map +1 -1
  24. package/dist/catalogue/inventory.generated.json +2815 -1108
  25. package/dist/catalogue/layering.d.ts +10 -0
  26. package/dist/catalogue/layering.d.ts.map +1 -0
  27. package/dist/catalogue/layering.js +67 -0
  28. package/dist/catalogue/layering.js.map +1 -0
  29. package/dist/catalogue/primitives.css +77 -0
  30. package/dist/catalogue/primitives.d.ts +34 -0
  31. package/dist/catalogue/primitives.d.ts.map +1 -0
  32. package/dist/catalogue/primitives.js +149 -0
  33. package/dist/catalogue/primitives.js.map +1 -0
  34. package/dist/catalogue/registry.d.ts.map +1 -1
  35. package/dist/catalogue/registry.js +18 -0
  36. package/dist/catalogue/registry.js.map +1 -1
  37. package/dist/catalogue/specimens.d.ts.map +1 -1
  38. package/dist/catalogue/specimens.js +142 -22
  39. package/dist/catalogue/specimens.js.map +1 -1
  40. package/dist/catalogue/styles.css +1 -0
  41. package/dist/contract/index.d.ts +2 -0
  42. package/dist/contract/index.d.ts.map +1 -1
  43. package/dist/contract/index.js +11 -0
  44. package/dist/contract/index.js.map +1 -1
  45. package/dist/contract/layout-audit.d.ts +32 -0
  46. package/dist/contract/layout-audit.d.ts.map +1 -0
  47. package/dist/contract/layout-audit.js +204 -0
  48. package/dist/contract/layout-audit.js.map +1 -0
  49. package/dist/forms/date-time/date-math.d.ts +23 -0
  50. package/dist/forms/date-time/date-math.d.ts.map +1 -0
  51. package/dist/forms/date-time/date-math.js +83 -0
  52. package/dist/forms/date-time/date-math.js.map +1 -0
  53. package/dist/forms/date-time/index.d.ts +37 -6
  54. package/dist/forms/date-time/index.d.ts.map +1 -1
  55. package/dist/forms/date-time/index.js +140 -6
  56. package/dist/forms/date-time/index.js.map +1 -1
  57. package/dist/forms/date-time/runtime.d.ts +4 -0
  58. package/dist/forms/date-time/runtime.d.ts.map +1 -0
  59. package/dist/forms/date-time/runtime.js +637 -0
  60. package/dist/forms/date-time/runtime.js.map +1 -0
  61. package/dist/forms/date-time/styles.css +251 -12
  62. package/dist/foundations/tokens.css +48 -28
  63. package/dist/index.d.ts +15 -4
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +8 -3
  66. package/dist/index.js.map +1 -1
  67. package/dist/interactions/ket-table/demo.d.ts +4 -0
  68. package/dist/interactions/ket-table/demo.d.ts.map +1 -0
  69. package/dist/interactions/ket-table/demo.js +147 -0
  70. package/dist/interactions/ket-table/demo.js.map +1 -0
  71. package/dist/interactions/ket-table/index.d.ts +199 -0
  72. package/dist/interactions/ket-table/index.d.ts.map +1 -0
  73. package/dist/interactions/ket-table/index.js +376 -0
  74. package/dist/interactions/ket-table/index.js.map +1 -0
  75. package/dist/interactions/ket-table/styles.css +282 -0
  76. package/dist/interactions/lightbox/demo.d.ts +3 -0
  77. package/dist/interactions/lightbox/demo.d.ts.map +1 -0
  78. package/dist/interactions/lightbox/demo.js +21 -0
  79. package/dist/interactions/lightbox/demo.js.map +1 -0
  80. package/dist/interactions/lightbox/index.d.ts +58 -0
  81. package/dist/interactions/lightbox/index.d.ts.map +1 -0
  82. package/dist/interactions/lightbox/index.js +220 -0
  83. package/dist/interactions/lightbox/index.js.map +1 -0
  84. package/dist/interactions/lightbox/styles.css +153 -0
  85. package/dist/interactions/relation-select/demo.d.ts +3 -0
  86. package/dist/interactions/relation-select/demo.d.ts.map +1 -0
  87. package/dist/interactions/relation-select/demo.js +36 -0
  88. package/dist/interactions/relation-select/demo.js.map +1 -0
  89. package/dist/interactions/relation-select/index.d.ts +87 -0
  90. package/dist/interactions/relation-select/index.d.ts.map +1 -0
  91. package/dist/interactions/relation-select/index.js +307 -0
  92. package/dist/interactions/relation-select/index.js.map +1 -0
  93. package/dist/interactions/relation-select/styles.css +299 -0
  94. package/dist/interactions/reorder-list/index.d.ts +23 -0
  95. package/dist/interactions/reorder-list/index.d.ts.map +1 -0
  96. package/dist/interactions/reorder-list/index.js +15 -0
  97. package/dist/interactions/reorder-list/index.js.map +1 -0
  98. package/dist/interactions/reorder-list/runtime.d.ts +5 -0
  99. package/dist/interactions/reorder-list/runtime.d.ts.map +1 -0
  100. package/dist/interactions/reorder-list/runtime.js +85 -0
  101. package/dist/interactions/reorder-list/runtime.js.map +1 -0
  102. package/dist/interactions/reorder-list/styles.css +33 -0
  103. package/dist/interactions/search-filter/demo.d.ts +3 -0
  104. package/dist/interactions/search-filter/demo.d.ts.map +1 -0
  105. package/dist/interactions/search-filter/demo.js +74 -0
  106. package/dist/interactions/search-filter/demo.js.map +1 -0
  107. package/dist/interactions/search-filter/index.d.ts +153 -0
  108. package/dist/interactions/search-filter/index.d.ts.map +1 -0
  109. package/dist/interactions/search-filter/index.js +824 -0
  110. package/dist/interactions/search-filter/index.js.map +1 -0
  111. package/dist/interactions/search-filter/styles.css +568 -0
  112. package/dist/layout-audit-cli.d.ts +7 -0
  113. package/dist/layout-audit-cli.d.ts.map +1 -0
  114. package/dist/layout-audit-cli.js +63 -0
  115. package/dist/layout-audit-cli.js.map +1 -0
  116. package/dist/layouts/app-navigation/index.d.ts +11 -5
  117. package/dist/layouts/app-navigation/index.d.ts.map +1 -1
  118. package/dist/layouts/app-navigation/index.js +5 -3
  119. package/dist/layouts/app-navigation/index.js.map +1 -1
  120. package/dist/layouts/app-navigation/styles.css +47 -0
  121. package/dist/layouts/grouped/styles.css +5 -36
  122. package/dist/layouts/layering/styles.css +159 -0
  123. package/dist/layouts/layout/index.d.ts +6 -0
  124. package/dist/layouts/layout/index.d.ts.map +1 -1
  125. package/dist/layouts/layout/index.js +6 -1
  126. package/dist/layouts/layout/index.js.map +1 -1
  127. package/dist/layouts/layout/styles.css +15 -6
  128. package/dist/layouts/page-identity/index.d.ts +10 -7
  129. package/dist/layouts/page-identity/index.d.ts.map +1 -1
  130. package/dist/layouts/page-identity/index.js +9 -4
  131. package/dist/layouts/page-identity/index.js.map +1 -1
  132. package/dist/layouts/shell/index.d.ts +34 -1
  133. package/dist/layouts/shell/index.d.ts.map +1 -1
  134. package/dist/layouts/shell/index.js +26 -3
  135. package/dist/layouts/shell/index.js.map +1 -1
  136. package/dist/layouts/shell/styles.css +284 -2
  137. package/dist/patterns/board-page/index.d.ts +1 -1
  138. package/dist/patterns/board-page/index.d.ts.map +1 -1
  139. package/dist/patterns/board-page/index.js +0 -1
  140. package/dist/patterns/board-page/index.js.map +1 -1
  141. package/dist/patterns/board-page/styles.css +0 -12
  142. package/dist/patterns/dashboard-page/index.d.ts +1 -1
  143. package/dist/patterns/dashboard-page/index.d.ts.map +1 -1
  144. package/dist/patterns/dashboard-page/index.js +0 -1
  145. package/dist/patterns/dashboard-page/index.js.map +1 -1
  146. package/dist/patterns/dashboard-page/styles.css +0 -13
  147. package/dist/patterns/data-table/styles.css +1 -1
  148. package/dist/patterns/form-page/index.d.ts +1 -1
  149. package/dist/patterns/form-page/index.d.ts.map +1 -1
  150. package/dist/patterns/form-page/index.js +0 -1
  151. package/dist/patterns/form-page/index.js.map +1 -1
  152. package/dist/patterns/form-page/styles.css +0 -12
  153. package/dist/patterns/list-page/index.d.ts +10 -4
  154. package/dist/patterns/list-page/index.d.ts.map +1 -1
  155. package/dist/patterns/list-page/index.js +10 -8
  156. package/dist/patterns/list-page/index.js.map +1 -1
  157. package/dist/patterns/list-page/responsive.css +1 -13
  158. package/dist/patterns/list-page/styles.css +20 -20
  159. package/dist/patterns/modal-sheet/index.d.ts +2 -0
  160. package/dist/patterns/modal-sheet/index.d.ts.map +1 -1
  161. package/dist/patterns/modal-sheet/index.js +3 -1
  162. package/dist/patterns/modal-sheet/index.js.map +1 -1
  163. package/dist/patterns/modal-sheet/styles.css +1 -1
  164. package/dist/patterns/page-shell/styles.css +0 -12
  165. package/dist/patterns/record-form/styles.css +34 -0
  166. package/dist/patterns/record-page/index.d.ts +1 -2
  167. package/dist/patterns/record-page/index.d.ts.map +1 -1
  168. package/dist/patterns/record-page/index.js +2 -3
  169. package/dist/patterns/record-page/index.js.map +1 -1
  170. package/dist/patterns/record-page/styles.css +0 -12
  171. package/dist/patterns/workspace-page/index.d.ts +1 -2
  172. package/dist/patterns/workspace-page/index.d.ts.map +1 -1
  173. package/dist/patterns/workspace-page/index.js +0 -1
  174. package/dist/patterns/workspace-page/index.js.map +1 -1
  175. package/dist/primitives/actions/index.d.ts +4 -0
  176. package/dist/primitives/actions/index.d.ts.map +1 -1
  177. package/dist/primitives/actions/index.js +2 -2
  178. package/dist/primitives/actions/index.js.map +1 -1
  179. package/dist/primitives/actions/styles.css +3 -1
  180. package/dist/primitives/feedback/styles.css +11 -6
  181. package/dist/primitives/field/index.d.ts +5 -0
  182. package/dist/primitives/field/index.d.ts.map +1 -1
  183. package/dist/primitives/field/index.js +1 -1
  184. package/dist/primitives/field/index.js.map +1 -1
  185. package/dist/primitives/field/native-control.d.ts.map +1 -1
  186. package/dist/primitives/field/native-control.js +3 -3
  187. package/dist/primitives/field/native-control.js.map +1 -1
  188. package/dist/primitives/field/styles.css +124 -2
  189. package/dist/primitives/progress/styles.css +3 -0
  190. package/dist/primitives/status/index.d.ts +12 -1
  191. package/dist/primitives/status/index.d.ts.map +1 -1
  192. package/dist/primitives/status/index.js +16 -1
  193. package/dist/primitives/status/index.js.map +1 -1
  194. package/dist/primitives/status/styles.css +34 -2
  195. package/dist/record/display/index.d.ts +6 -0
  196. package/dist/record/display/index.d.ts.map +1 -1
  197. package/dist/record/display/index.js +6 -1
  198. package/dist/record/display/index.js.map +1 -1
  199. package/dist/record/display/styles.css +25 -0
  200. package/dist/record/formatted-values/index.d.ts +2 -1
  201. package/dist/record/formatted-values/index.d.ts.map +1 -1
  202. package/dist/record/formatted-values/index.js +2 -1
  203. package/dist/record/formatted-values/index.js.map +1 -1
  204. package/dist/runtime/index.d.ts.map +1 -1
  205. package/dist/runtime/index.js +132 -2
  206. package/dist/runtime/index.js.map +1 -1
  207. package/dist/styles.css +1 -1
  208. package/package.json +4 -3
@@ -0,0 +1,637 @@
1
+ // @ts-check
2
+ import {
3
+ addDays,
4
+ formatRange,
5
+ monthEnd,
6
+ monthStart,
7
+ moveMonth,
8
+ orderedRange,
9
+ parseDate,
10
+ presetRange,
11
+ withinBounds,
12
+ } from './date-math.js'
13
+
14
+ /** An already validated calendar day. @param {string} value */
15
+ const civil = (value) => {
16
+ const date = parseDate(value)
17
+ if (!date) throw new RangeError(`Invalid civil date: ${value}`)
18
+ return date
19
+ }
20
+ /** @param {HTMLInputElement} input @param {string} value */
21
+ const allowed = (input, value) => {
22
+ if (!withinBounds(value, input.min, input.max)) return false
23
+ const step = Number(input.step || 1)
24
+ if (input.step === 'any' || !Number.isFinite(step) || step <= 0) return true
25
+ const base = parseDate(input.min) ?? parseDate(input.getAttribute('value') ?? '') ?? civil('1970-01-01')
26
+ return Math.abs(((civil(value).getTime() - base.getTime()) / 86400000 / step) % 1) < 0.000001
27
+ }
28
+ /** @type {WeakMap<HTMLElement, { users: number, prior: boolean }>} */
29
+ const inertLayers = new WeakMap()
30
+ let scrollLocks = 0
31
+ let priorOverflow = ''
32
+ let priorOverflowPriority = ''
33
+ /** Make the fullscreen calendar modal while preserving any outer dialog's locks.
34
+ * @param {HTMLElement} popup */
35
+ const lockBackground = (popup) => {
36
+ /** @type {HTMLElement[]} */
37
+ const siblings = []
38
+ let branch = popup
39
+ while (branch.parentElement) {
40
+ const parent = branch.parentElement
41
+ for (const sibling of parent.children) {
42
+ if (!(sibling instanceof HTMLElement) || sibling === branch) continue
43
+ const state = inertLayers.get(sibling) ?? { users: 0, prior: sibling.inert }
44
+ state.users++
45
+ inertLayers.set(sibling, state)
46
+ sibling.inert = true
47
+ siblings.push(sibling)
48
+ }
49
+ if (parent === document.body) break
50
+ branch = parent
51
+ }
52
+ const style = document.documentElement.style
53
+ if (scrollLocks++ === 0) {
54
+ priorOverflow = style.overflow
55
+ priorOverflowPriority = style.getPropertyPriority('overflow')
56
+ style.setProperty('overflow', 'hidden')
57
+ }
58
+ return () => {
59
+ for (const sibling of siblings) {
60
+ const state = inertLayers.get(sibling)
61
+ if (state && --state.users === 0) {
62
+ sibling.inert = state.prior
63
+ inertLayers.delete(sibling)
64
+ }
65
+ }
66
+ if (--scrollLocks === 0) {
67
+ if (priorOverflow) style.setProperty('overflow', priorOverflow, priorOverflowPriority)
68
+ else style.removeProperty('overflow')
69
+ }
70
+ }
71
+ }
72
+ /** @param {HTMLElement} picker */
73
+ const enhance = (picker) => {
74
+ const panel = picker.querySelector('[data-ui="date-calendar"]')
75
+ const trigger = picker.querySelector('[name="kv-date-open"]')
76
+ const inputs = /** @type {HTMLInputElement[]} */ ([
77
+ ...picker.querySelectorAll('[data-ui="date-inputs"] input[type="date"], [data-ui="date-boundary"]'),
78
+ ])
79
+ if (
80
+ !(panel instanceof HTMLElement) ||
81
+ !(trigger instanceof HTMLButtonElement) ||
82
+ !inputs.length ||
83
+ !('showPopover' in panel)
84
+ )
85
+ return () => {}
86
+ const popup = panel
87
+ const opener = trigger
88
+ const range = picker.dataset.dateMode === 'range'
89
+ const locale = picker.dataset.dateLocale === 'en' ? 'en-GB' : 'vi-VN'
90
+ const weekStart = Number(picker.dataset.dateWeekStart ?? 1)
91
+ const labels = /** @type {import('./index.tsx').DatePickerLabels} */ (
92
+ JSON.parse(picker.dataset.dateLabels ?? '{}')
93
+ )
94
+ const months = /** @type {HTMLElement} */ (popup.querySelector('[data-ui="date-calendar-months"]'))
95
+ const status = /** @type {HTMLElement} */ (popup.querySelector('[data-ui="date-calendar-status"]'))
96
+ const apply = /** @type {HTMLButtonElement} */ (popup.querySelector('[value="apply"]'))
97
+ const message = picker.querySelector('[data-ui="date-range-error"]')
98
+ const end = inputs[1]
99
+ const defaults = inputs.map((input) => input.value)
100
+ const display = picker.querySelector('input[type="text"]')
101
+ const preset = picker.querySelector('select')
102
+ const field = display instanceof HTMLInputElement ? display : end
103
+ const originalInvalid = field?.getAttribute('aria-invalid')
104
+ const originalDescription = field?.getAttribute('aria-describedby')
105
+ let returnFocus = /** @type {HTMLElement} */ (opener)
106
+ const readToday = () => {
107
+ const local = new Date()
108
+ const fallback = `${local.getFullYear()}-${String(local.getMonth() + 1).padStart(2, '0')}-${String(local.getDate()).padStart(2, '0')}`
109
+ return parseDate(picker.dataset.dateToday ?? '') ? (picker.dataset.dateToday ?? fallback) : fallback
110
+ }
111
+ const formatter = new Intl.DateTimeFormat(locale, {
112
+ day: 'numeric',
113
+ month: 'long',
114
+ year: 'numeric',
115
+ timeZone: 'UTC',
116
+ })
117
+ const short = new Intl.DateTimeFormat(locale, {
118
+ day: '2-digit',
119
+ month: '2-digit',
120
+ year: 'numeric',
121
+ timeZone: 'UTC',
122
+ })
123
+ const monthFormat = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' })
124
+ const weekdayFormat = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' })
125
+ let today = readToday()
126
+ let month = ''
127
+ let focusDate = ''
128
+ let preferredPreset = ''
129
+ /** @type {string[]} */
130
+ let draft = []
131
+ const isOpen = () => popup.matches(':popover-open')
132
+ const isMobile = () => window.innerWidth < 768
133
+ /** @type {(() => void) | null} */
134
+ let unlock = null
135
+ const syncPresentation = () => {
136
+ if (isOpen() && isMobile()) {
137
+ unlock ??= lockBackground(popup)
138
+ popup.setAttribute('aria-modal', 'true')
139
+ } else {
140
+ unlock?.()
141
+ unlock = null
142
+ popup.removeAttribute('aria-modal')
143
+ }
144
+ }
145
+ const monthCount = () => (range ? (isMobile() ? 12 : 2) : 1)
146
+ /** @param {string} value */
147
+ const calendarStart = (value) => {
148
+ const start = monthStart(value)
149
+ return range && isMobile() ? moveMonth(start, -2) || start : start
150
+ }
151
+ const visibleMonth = () => {
152
+ const top = months.getBoundingClientRect().top
153
+ const section = [...months.children].find((child) => child.getBoundingClientRect().bottom > top + 1)
154
+ return section instanceof HTMLElement ? (section.dataset.dateMonth ?? month) : month
155
+ }
156
+ const revealMonth = () => {
157
+ if (!range || !isMobile()) return
158
+ const section = months.querySelector(`[data-date-month="${monthStart(focusDate)}"]`)
159
+ if (section instanceof HTMLElement) {
160
+ months.scrollTop +=
161
+ section.getBoundingClientRect().top -
162
+ months.getBoundingClientRect().top -
163
+ parseFloat(getComputedStyle(months).paddingTop)
164
+ }
165
+ }
166
+ /** @param {HTMLElement} day */
167
+ const revealDay = (day) => {
168
+ if (!isMobile()) return
169
+ const box = day.getBoundingClientRect()
170
+ const viewport = months.getBoundingClientRect()
171
+ if (box.top < viewport.top) months.scrollTop += box.top - viewport.top
172
+ else if (box.bottom > viewport.bottom) months.scrollTop += box.bottom - viewport.bottom
173
+ }
174
+ const enabled = () => inputs.every((input) => !input.disabled && !input.readOnly)
175
+ const valid = () =>
176
+ !!draft[0] &&
177
+ allowed(inputs[0], draft[0]) &&
178
+ (!range || (!!draft[1] && draft[0] <= draft[1] && allowed(end, draft[1])))
179
+ const validate = () => {
180
+ if (!end || !field) return true
181
+ const startValue = inputs[0].value
182
+ const endValue = end.value
183
+ const empty = !startValue && !endValue
184
+ const invalid =
185
+ !inputs[0].disabled &&
186
+ !end.disabled &&
187
+ (empty
188
+ ? inputs.some((input) => input.required)
189
+ : !allowed(inputs[0], startValue) || !allowed(end, endValue) || startValue > endValue)
190
+ if (display instanceof HTMLInputElement) display.value = formatRange(startValue, endValue)
191
+ if (message instanceof HTMLElement) {
192
+ message.hidden = !invalid
193
+ field.setAttribute(
194
+ 'aria-describedby',
195
+ [originalDescription, invalid ? message.id : ''].filter(Boolean).join(' '),
196
+ )
197
+ }
198
+ if (invalid) field.setAttribute('aria-invalid', 'true')
199
+ else if (originalInvalid) field.setAttribute('aria-invalid', originalInvalid)
200
+ else field.removeAttribute('aria-invalid')
201
+ if (preset instanceof HTMLSelectElement) {
202
+ preset.disabled = !enabled()
203
+ const matching = [...preset.options].filter((option) => {
204
+ const period = presetRange(option.value, today)
205
+ option.disabled = !!period && (!allowed(inputs[0], period[0]) || !allowed(end, period[1]))
206
+ return period && period[0] === startValue && period[1] === endValue
207
+ })
208
+ preset.value =
209
+ (matching.find((option) => option.value === preferredPreset) ?? matching[0])?.value ?? 'custom'
210
+ }
211
+ return !invalid
212
+ }
213
+ /** @param {string[]} values */
214
+ const commit = (values) => {
215
+ const changed = inputs.filter((input, index) => input.value !== values[index])
216
+ inputs.forEach((input, index) => {
217
+ input.value = values[index]
218
+ })
219
+ validate()
220
+ for (const input of [
221
+ ...changed,
222
+ ...(changed.length && display instanceof HTMLInputElement ? [display] : []),
223
+ ]) {
224
+ input.dispatchEvent(new Event('input', { bubbles: true }))
225
+ input.dispatchEvent(new Event('change', { bubbles: true }))
226
+ }
227
+ }
228
+ /** @param {string} [preview] */
229
+ const paint = (preview = '') => {
230
+ const [from, to] =
231
+ range && draft[0] && !draft[1] && preview
232
+ ? orderedRange(draft[0], preview)
233
+ : [draft[0], draft[1] ?? draft[0]]
234
+ for (const node of months.querySelectorAll('button[data-date]')) {
235
+ const button = /** @type {HTMLButtonElement} */ (node)
236
+ const day = button.dataset.date ?? ''
237
+ const cell = button.parentElement
238
+ const selected = day === draft[0] || day === draft[1]
239
+ button.dataset.selected = String(selected)
240
+ if (cell) {
241
+ cell.dataset.inRange = String(range && !!from && !!to && day >= from && day <= to)
242
+ cell.setAttribute(
243
+ 'aria-selected',
244
+ String(selected || (range && !!from && !!to && day >= from && day <= to)),
245
+ )
246
+ }
247
+ }
248
+ }
249
+ const position = () => {
250
+ if (!isOpen() || isMobile()) return
251
+ const rect = opener.getBoundingClientRect()
252
+ const inset = parseFloat(getComputedStyle(popup).scrollMarginTop)
253
+ const gap = inset
254
+ popup.style.setProperty('--kv-date-max-height', `${Math.max(0, window.innerHeight - inset * 2)}px`)
255
+ const box = popup.getBoundingClientRect()
256
+ const left = Math.max(
257
+ inset,
258
+ Math.min(
259
+ rect.left + box.width <= window.innerWidth - inset ? rect.left : rect.right - box.width,
260
+ window.innerWidth - box.width - inset,
261
+ ),
262
+ )
263
+ const below = rect.bottom + gap
264
+ const top =
265
+ below + box.height <= window.innerHeight - inset ? below : Math.max(inset, rect.top - box.height - gap)
266
+ popup.style.setProperty('--kv-date-left', `${left}px`)
267
+ popup.style.setProperty('--kv-date-top', `${top}px`)
268
+ }
269
+ /** @param {boolean} [focus] */
270
+ const render = (focus = false) => {
271
+ const scrollTop = months.scrollTop
272
+ months.replaceChildren()
273
+ popup.dataset.months = String(monthCount())
274
+ for (let index = 0; index < monthCount(); index++) {
275
+ const current = moveMonth(month, index)
276
+ if (!current) continue
277
+ const section = document.createElement('section')
278
+ section.dataset.ui = 'date-month'
279
+ section.dataset.dateMonth = current
280
+ const title = document.createElement('h4')
281
+ title.dataset.ui = 'date-month-title'
282
+ title.id = `${popup.id}-month-${index}`
283
+ title.textContent = monthFormat.format(civil(current))
284
+ if (!isMobile()) title.setAttribute('aria-live', 'polite')
285
+ section.append(title)
286
+ const table = document.createElement('table')
287
+ table.dataset.ui = 'date-grid'
288
+ table.setAttribute('role', 'grid')
289
+ table.setAttribute('aria-labelledby', title.id)
290
+ if (range) table.setAttribute('aria-multiselectable', 'true')
291
+ const head = table.createTHead().insertRow()
292
+ for (let column = 0; column < 7; column++) {
293
+ const th = document.createElement('th')
294
+ th.dataset.ui = 'date-weekday'
295
+ th.scope = 'col'
296
+ th.textContent = weekdayFormat.format(civil(addDays('2026-09-06', (column + weekStart) % 7)))
297
+ head.append(th)
298
+ }
299
+ const body = table.createTBody()
300
+ const offset = (civil(current).getUTCDay() - weekStart + 7) % 7
301
+ const days = civil(monthEnd(current)).getUTCDate()
302
+ const weeks = isMobile() ? Math.ceil((offset + days) / 7) : 6
303
+ for (let row = 0; row < weeks; row++) {
304
+ const tr = body.insertRow()
305
+ for (let column = 0; column < 7; column++) {
306
+ const cell = tr.insertCell()
307
+ cell.dataset.ui = 'date-cell'
308
+ const number = row * 7 + column - offset + 1
309
+ if (number < 1 || number > days) {
310
+ cell.setAttribute('aria-hidden', 'true')
311
+ continue
312
+ }
313
+ const value = addDays(current, number - 1)
314
+ const button = document.createElement('button')
315
+ button.type = 'button'
316
+ button.dataset.ui = 'date-day'
317
+ button.dataset.date = value
318
+ button.textContent = String(number)
319
+ button.setAttribute('aria-label', formatter.format(civil(value)))
320
+ if (value === today) button.setAttribute('aria-current', 'date')
321
+ if (range && draft[0] && !draft[1]) {
322
+ const [start, finish] = orderedRange(draft[0], value)
323
+ button.disabled = !allowed(inputs[0], start) || !allowed(end, finish)
324
+ } else button.disabled = !allowed(inputs[0], value)
325
+ button.tabIndex = value === focusDate && !button.disabled ? 0 : -1
326
+ cell.append(button)
327
+ }
328
+ }
329
+ section.append(table)
330
+ months.append(section)
331
+ }
332
+ months.scrollTop = scrollTop
333
+ const focusable =
334
+ months.querySelector('button[tabindex="0"]') ?? months.querySelector('button:not(:disabled)')
335
+ if (focusable instanceof HTMLButtonElement) {
336
+ focusable.tabIndex = 0
337
+ focusDate = focusable.dataset.date ?? focusDate
338
+ if (focus) {
339
+ focusable.focus({ preventScroll: true })
340
+ revealDay(focusable)
341
+ }
342
+ }
343
+ for (const command of ['previous', 'next']) {
344
+ const button = /** @type {HTMLButtonElement} */ (popup.querySelector(`[value="${command}"]`))
345
+ button.disabled = !moveMonth(
346
+ range && isMobile() ? monthStart(focusDate) : month,
347
+ command === 'previous' ? -1 : range && isMobile() ? 1 : monthCount(),
348
+ )
349
+ }
350
+ apply.disabled = !valid()
351
+ status.textContent = draft[0]
352
+ ? range && !draft[1]
353
+ ? `${short.format(civil(draft[0]))} · ${labels.chooseEnd}`
354
+ : `${short.format(civil(draft[0]))}${range ? ` — ${short.format(civil(draft[1]))}` : ''}`
355
+ : range
356
+ ? labels.chooseStart
357
+ : labels.chooseDate
358
+ paint(focus && range && draft[0] && !draft[1] ? focusDate : '')
359
+ position()
360
+ }
361
+ /** @param {HTMLElement} [source] */
362
+ const open = (source = opener) => {
363
+ if (!enabled()) return
364
+ returnFocus = source
365
+ today = readToday()
366
+ draft = inputs.map((input) => (parseDate(input.value) ? input.value : ''))
367
+ focusDate = draft[0] || today
368
+ if (parseDate(inputs[0].min) && focusDate < inputs[0].min) focusDate = inputs[0].min
369
+ if (parseDate(inputs[0].max) && focusDate > inputs[0].max) focusDate = inputs[0].max
370
+ month = calendarStart(focusDate)
371
+ render()
372
+ popup.showPopover()
373
+ syncPresentation()
374
+ opener.setAttribute('aria-expanded', 'true')
375
+ position()
376
+ revealMonth()
377
+ const first = months.querySelector('button[tabindex="0"]')
378
+ if (first instanceof HTMLElement) {
379
+ first.focus({ preventScroll: true })
380
+ revealDay(first)
381
+ } else {
382
+ const cancel = popup.querySelector('[value="cancel"]')
383
+ if (cancel instanceof HTMLElement) cancel.focus()
384
+ }
385
+ }
386
+ /** @param {boolean} [restore] */
387
+ const close = (restore = false) => {
388
+ if (isOpen()) popup.hidePopover()
389
+ syncPresentation()
390
+ opener.setAttribute('aria-expanded', 'false')
391
+ if (restore && returnFocus.isConnected) returnFocus.focus({ preventScroll: true })
392
+ }
393
+ /** @param {Event} event */
394
+ const click = (event) => {
395
+ if (event.target === display && display instanceof HTMLElement) {
396
+ isOpen() ? close(true) : open(display)
397
+ return
398
+ }
399
+ const target = event.target instanceof Element ? event.target.closest('button') : null
400
+ if (!(target instanceof HTMLButtonElement) || target.disabled) return
401
+ if (target === opener) {
402
+ isOpen() ? close(true) : open()
403
+ return
404
+ }
405
+ if (!popup.contains(target) || !enabled()) return
406
+ if (target.dataset.date) {
407
+ const value = target.dataset.date
408
+ preferredPreset = ''
409
+ draft = range ? (draft[0] && !draft[1] ? orderedRange(draft[0], value) : [value, '']) : [value]
410
+ focusDate = value
411
+ render(true)
412
+ } else if (target.name === 'kv-date-command') {
413
+ if (target.value === 'cancel') close(true)
414
+ else if (target.value === 'apply' && valid()) {
415
+ const values = [...draft]
416
+ close(true)
417
+ commit(values)
418
+ } else if (target.value === 'previous' || target.value === 'next') {
419
+ const next = moveMonth(
420
+ range && isMobile() ? visibleMonth() : month,
421
+ target.value === 'previous' ? -1 : 1,
422
+ )
423
+ if (!next) return
424
+ focusDate = next
425
+ month = range && isMobile() ? calendarStart(next) : next
426
+ render()
427
+ revealMonth()
428
+ }
429
+ }
430
+ }
431
+ /** @param {KeyboardEvent} event */
432
+ const keydown = (event) => {
433
+ if (
434
+ !isOpen() &&
435
+ event.target === display &&
436
+ display instanceof HTMLElement &&
437
+ ['Enter', ' ', 'ArrowDown'].includes(event.key)
438
+ ) {
439
+ event.preventDefault()
440
+ open(display)
441
+ return
442
+ }
443
+ if (!isOpen()) return
444
+ if (event.key === 'Escape') {
445
+ event.preventDefault()
446
+ event.stopImmediatePropagation()
447
+ close(true)
448
+ return
449
+ }
450
+ if (event.key === 'Tab' && isMobile()) {
451
+ const items = [
452
+ ...popup.querySelectorAll('button:not(:disabled):not([tabindex="-1"]), select:not(:disabled)'),
453
+ ]
454
+ const first = items[0]
455
+ const last = items.at(-1)
456
+ if (event.shiftKey && document.activeElement === first && last instanceof HTMLElement) {
457
+ event.preventDefault()
458
+ last.focus()
459
+ return
460
+ }
461
+ if (!event.shiftKey && document.activeElement === last && first instanceof HTMLElement) {
462
+ event.preventDefault()
463
+ first.focus()
464
+ return
465
+ }
466
+ }
467
+ const target = event.target
468
+ if (!(target instanceof HTMLButtonElement) || !target.dataset.date) return
469
+ const value = target.dataset.date
470
+ const weekday = (civil(value).getUTCDay() - weekStart + 7) % 7
471
+ const offsets = {
472
+ ArrowLeft: -1,
473
+ ArrowRight: 1,
474
+ ArrowUp: -7,
475
+ ArrowDown: 7,
476
+ Home: -weekday,
477
+ End: 6 - weekday,
478
+ }
479
+ let next =
480
+ event.key in offsets
481
+ ? addDays(value, offsets[/** @type {keyof typeof offsets} */ (event.key)])
482
+ : event.key === 'PageUp' || event.key === 'PageDown'
483
+ ? moveMonth(value, (event.key === 'PageUp' ? -1 : 1) * (event.shiftKey ? 12 : 1))
484
+ : ''
485
+ if (!next) return
486
+ event.preventDefault()
487
+ for (let attempt = 0; attempt < 366 && next; attempt++) {
488
+ const period = range && draft[0] && !draft[1] ? orderedRange(draft[0], next) : [next]
489
+ if (allowed(inputs[0], period[0]) && (!period[1] || allowed(end, period[1]))) break
490
+ next = addDays(next, next < value ? -1 : 1)
491
+ }
492
+ const period = range && draft[0] && !draft[1] ? orderedRange(draft[0], next) : [next]
493
+ if (!next || !allowed(inputs[0], period[0]) || (period[1] && !allowed(end, period[1]))) return
494
+ focusDate = next
495
+ if (next < month || next > monthEnd(moveMonth(month, monthCount() - 1))) month = monthStart(next)
496
+ render(true)
497
+ }
498
+ /** @param {PointerEvent} event */
499
+ const hover = (event) => {
500
+ const target = event.target
501
+ if (range && draft[0] && !draft[1] && target instanceof HTMLButtonElement && !target.disabled)
502
+ paint(target.dataset.date)
503
+ }
504
+ const leave = () => paint()
505
+ const toggle = () => {
506
+ syncPresentation()
507
+ opener.setAttribute('aria-expanded', String(isOpen()))
508
+ if (!isOpen()) draft = []
509
+ }
510
+ /** @param {FocusEvent} event */
511
+ const focusout = (event) => {
512
+ if (isOpen() && event.target instanceof Node && !popup.contains(event.target) && event.target !== opener)
513
+ close()
514
+ }
515
+ let mobileLayout = isMobile()
516
+ const resize = () => {
517
+ if (isOpen()) {
518
+ const changed = mobileLayout !== isMobile()
519
+ if (changed) month = calendarStart(focusDate)
520
+ syncPresentation()
521
+ render(months.contains(document.activeElement))
522
+ if (changed) revealMonth()
523
+ }
524
+ mobileLayout = isMobile()
525
+ }
526
+ /** @param {Event} event */
527
+ const change = (event) => {
528
+ if (event.target === preset && preset instanceof HTMLSelectElement && enabled()) {
529
+ today = readToday()
530
+ const period = presetRange(preset.value, today)
531
+ if (period && allowed(inputs[0], period[0]) && allowed(end, period[1])) {
532
+ preferredPreset = preset.value
533
+ close(true)
534
+ commit(period)
535
+ } else {
536
+ validate()
537
+ open(preset)
538
+ }
539
+ } else validate()
540
+ }
541
+ /** @param {SubmitEvent} event */
542
+ const submit = (event) => {
543
+ if (
544
+ !validate() &&
545
+ !inputs[0].form?.noValidate &&
546
+ !(event.submitter instanceof HTMLButtonElement && event.submitter.formNoValidate)
547
+ ) {
548
+ event.preventDefault()
549
+ field?.focus()
550
+ }
551
+ }
552
+ const reset = () => {
553
+ close()
554
+ setTimeout(() => {
555
+ if (range)
556
+ inputs.forEach((input, index) => {
557
+ input.value = defaults[index]
558
+ })
559
+ preferredPreset = ''
560
+ validate()
561
+ })
562
+ }
563
+ picker.dataset.dateEnhanced = 'true'
564
+ validate()
565
+ picker.addEventListener('click', click)
566
+ picker.addEventListener('keydown', keydown)
567
+ picker.addEventListener('change', change)
568
+ popup.addEventListener('toggle', toggle)
569
+ months.addEventListener('pointerover', hover)
570
+ months.addEventListener('pointerleave', leave)
571
+ document.addEventListener('focusin', focusout)
572
+ window.addEventListener('resize', resize)
573
+ window.addEventListener('scroll', position, true)
574
+ inputs[0].form?.addEventListener('reset', reset)
575
+ inputs[0].form?.addEventListener('submit', submit)
576
+ return () => {
577
+ close()
578
+ delete picker.dataset.dateEnhanced
579
+ picker.removeEventListener('click', click)
580
+ picker.removeEventListener('keydown', keydown)
581
+ picker.removeEventListener('change', change)
582
+ popup.removeEventListener('toggle', toggle)
583
+ months.removeEventListener('pointerover', hover)
584
+ months.removeEventListener('pointerleave', leave)
585
+ document.removeEventListener('focusin', focusout)
586
+ window.removeEventListener('resize', resize)
587
+ window.removeEventListener('scroll', position, true)
588
+ inputs[0].form?.removeEventListener('reset', reset)
589
+ inputs[0].form?.removeEventListener('submit', submit)
590
+ if (preset instanceof HTMLSelectElement) preset.disabled = true
591
+ }
592
+ }
593
+ /** Shared ownership prevents duplicate listeners when document and island roots overlap.
594
+ * @type {WeakMap<HTMLElement, { users: number, cleanup: () => void }>} */
595
+ const instances = new WeakMap()
596
+ /** @param {HTMLElement} node */
597
+ const acquire = (node) => {
598
+ let instance = instances.get(node)
599
+ if (!instance) {
600
+ instance = { users: 0, cleanup: enhance(node) }
601
+ instances.set(node, instance)
602
+ }
603
+ const owned = instance
604
+ owned.users++
605
+ return () => {
606
+ if (--owned.users === 0) {
607
+ owned.cleanup()
608
+ instances.delete(node)
609
+ }
610
+ }
611
+ }
612
+ /** Enhances current and subsequently inserted fields; cleanup also releases removed islands.
613
+ * @param {ParentNode} root */
614
+ export const attachDatePickers = (root) => {
615
+ if (typeof HTMLElement === 'undefined' || typeof Node === 'undefined') return () => {}
616
+ /** @type {Map<HTMLElement, () => void>} */
617
+ const attached = new Map()
618
+ const sync = () => {
619
+ for (const [element, cleanup] of attached)
620
+ if (element !== root && !root.contains(element)) {
621
+ cleanup()
622
+ attached.delete(element)
623
+ }
624
+ const candidates = [...root.querySelectorAll('[data-date-mode]')]
625
+ if (root instanceof HTMLElement && root.matches('[data-date-mode]')) candidates.unshift(root)
626
+ for (const node of candidates)
627
+ if (node instanceof HTMLElement && !attached.has(node)) attached.set(node, acquire(node))
628
+ }
629
+ sync()
630
+ const observer = typeof MutationObserver === 'undefined' ? null : new MutationObserver(sync)
631
+ if (root instanceof Node) observer?.observe(root, { childList: true, subtree: true })
632
+ return () => {
633
+ observer?.disconnect()
634
+ for (const cleanup of attached.values()) cleanup()
635
+ attached.clear()
636
+ }
637
+ }