tyrell-components 1.0.0-RC13 → 1.0.0-RC14

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 (355) hide show
  1. package/README.md +1 -1
  2. package/css/tyrell-theme.css +167 -178
  3. package/css/tyrell.css +19 -0
  4. package/dist/tyrell-theme.css +167 -178
  5. package/dist/tyrell.css +19 -0
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.d.ts +20 -10
  8. package/lib/base/ty-component.js +21 -144
  9. package/lib/components/button.d.ts +2 -16
  10. package/lib/components/button.js +0 -36
  11. package/lib/components/calendar-month.d.ts +2 -48
  12. package/lib/components/calendar-month.js +4 -166
  13. package/lib/components/calendar-navigation.d.ts +3 -48
  14. package/lib/components/calendar-navigation.js +3 -126
  15. package/lib/components/calendar.d.ts +3 -80
  16. package/lib/components/calendar.js +8 -230
  17. package/lib/components/checkbox.d.ts +4 -71
  18. package/lib/components/checkbox.js +7 -151
  19. package/lib/components/copy.d.ts +3 -56
  20. package/lib/components/copy.js +1 -120
  21. package/lib/components/date-picker.d.ts +13 -162
  22. package/lib/components/date-picker.js +41 -592
  23. package/lib/components/file-upload.d.ts +9 -15
  24. package/lib/components/file-upload.js +19 -41
  25. package/lib/components/icon.d.ts +3 -24
  26. package/lib/components/icon.js +1 -90
  27. package/lib/components/input.d.ts +7 -107
  28. package/lib/components/input.js +5 -193
  29. package/lib/components/modal.d.ts +17 -26
  30. package/lib/components/modal.js +57 -201
  31. package/lib/components/option.d.ts +2 -9
  32. package/lib/components/option.js +0 -48
  33. package/lib/components/popup.d.ts +2 -17
  34. package/lib/components/popup.js +8 -139
  35. package/lib/components/radio.d.ts +3 -23
  36. package/lib/components/radio.js +10 -65
  37. package/lib/components/resize-observer.d.ts +3 -30
  38. package/lib/components/resize-observer.js +12 -46
  39. package/lib/components/scroll-container.d.ts +2 -12
  40. package/lib/components/scroll-container.js +3 -46
  41. package/lib/components/select.d.ts +22 -153
  42. package/lib/components/select.js +33 -590
  43. package/lib/components/selected-tags.d.ts +4 -47
  44. package/lib/components/selected-tags.js +1 -95
  45. package/lib/components/step.d.ts +1 -6
  46. package/lib/components/step.js +0 -6
  47. package/lib/components/switch.d.ts +0 -11
  48. package/lib/components/switch.js +2 -36
  49. package/lib/components/tab.d.ts +1 -6
  50. package/lib/components/tab.js +0 -7
  51. package/lib/components/tabs.d.ts +5 -63
  52. package/lib/components/tabs.js +20 -283
  53. package/lib/components/tag.d.ts +2 -36
  54. package/lib/components/tag.js +5 -105
  55. package/lib/components/textarea.d.ts +6 -86
  56. package/lib/components/textarea.js +2 -173
  57. package/lib/components/tooltip.d.ts +3 -11
  58. package/lib/components/tooltip.js +24 -160
  59. package/lib/components/wizard.d.ts +6 -54
  60. package/lib/components/wizard.js +15 -228
  61. package/lib/icons/fontawesome/brands.d.ts +0 -1
  62. package/lib/icons/fontawesome/brands.js +0 -8
  63. package/lib/icons/fontawesome/regular.d.ts +0 -1
  64. package/lib/icons/fontawesome/regular.js +0 -8
  65. package/lib/icons/fontawesome/solid.d.ts +0 -1
  66. package/lib/icons/fontawesome/solid.js +0 -8
  67. package/lib/icons/heroicons/micro.d.ts +0 -1
  68. package/lib/icons/heroicons/micro.js +0 -8
  69. package/lib/icons/heroicons/mini.d.ts +0 -1
  70. package/lib/icons/heroicons/mini.js +0 -8
  71. package/lib/icons/heroicons/outline.d.ts +0 -1
  72. package/lib/icons/heroicons/outline.js +0 -8
  73. package/lib/icons/heroicons/solid.d.ts +0 -1
  74. package/lib/icons/heroicons/solid.js +0 -8
  75. package/lib/icons/lucide.d.ts +0 -1
  76. package/lib/icons/lucide.js +0 -1872
  77. package/lib/icons/material/filled.d.ts +0 -1
  78. package/lib/icons/material/filled.js +0 -10
  79. package/lib/icons/material/outlined.d.ts +0 -1
  80. package/lib/icons/material/outlined.js +0 -10
  81. package/lib/icons/material/round.d.ts +0 -1
  82. package/lib/icons/material/round.js +0 -10
  83. package/lib/icons/material/sharp.d.ts +0 -1
  84. package/lib/icons/material/sharp.js +0 -10
  85. package/lib/icons/material/two-tone.d.ts +0 -1
  86. package/lib/icons/material/two-tone.js +0 -10
  87. package/lib/index.d.ts +0 -1
  88. package/lib/index.js +0 -20
  89. package/lib/styles/button.d.ts +2 -20
  90. package/lib/styles/button.js +22 -122
  91. package/lib/styles/calendar-month.d.ts +1 -5
  92. package/lib/styles/calendar-month.js +4 -61
  93. package/lib/styles/calendar-navigation.d.ts +2 -6
  94. package/lib/styles/calendar-navigation.js +2 -27
  95. package/lib/styles/calendar.d.ts +1 -2
  96. package/lib/styles/calendar.js +1 -14
  97. package/lib/styles/checkbox.d.ts +0 -1
  98. package/lib/styles/checkbox.js +1 -23
  99. package/lib/styles/copy.d.ts +0 -1
  100. package/lib/styles/copy.js +2 -17
  101. package/lib/styles/custom-scrollbar.d.ts +1 -2
  102. package/lib/styles/custom-scrollbar.js +3 -18
  103. package/lib/styles/date-picker.d.ts +1 -5
  104. package/lib/styles/date-picker.js +35 -39
  105. package/lib/styles/file-upload.d.ts +1 -2
  106. package/lib/styles/file-upload.js +1 -16
  107. package/lib/styles/icon.d.ts +2 -6
  108. package/lib/styles/icon.js +7 -21
  109. package/lib/styles/input.d.ts +1 -6
  110. package/lib/styles/input.js +5 -54
  111. package/lib/styles/modal.d.ts +1 -2
  112. package/lib/styles/modal.js +21 -23
  113. package/lib/styles/option.d.ts +2 -6
  114. package/lib/styles/option.js +17 -12
  115. package/lib/styles/popup.d.ts +1 -2
  116. package/lib/styles/popup.js +26 -30
  117. package/lib/styles/radio.d.ts +0 -1
  118. package/lib/styles/radio.js +2 -16
  119. package/lib/styles/resize-observer.d.ts +1 -2
  120. package/lib/styles/resize-observer.js +0 -7
  121. package/lib/styles/scroll-container.d.ts +1 -2
  122. package/lib/styles/scroll-container.js +0 -19
  123. package/lib/styles/select-base.d.ts +0 -1
  124. package/lib/styles/select-base.js +68 -79
  125. package/lib/styles/select.d.ts +1 -2
  126. package/lib/styles/select.js +7 -15
  127. package/lib/styles/step.d.ts +1 -2
  128. package/lib/styles/step.js +0 -4
  129. package/lib/styles/switch.d.ts +0 -1
  130. package/lib/styles/switch.js +2 -20
  131. package/lib/styles/tab.d.ts +1 -2
  132. package/lib/styles/tab.js +0 -5
  133. package/lib/styles/tabs.d.ts +3 -11
  134. package/lib/styles/tabs.js +10 -48
  135. package/lib/styles/tag.d.ts +1 -5
  136. package/lib/styles/tag.js +2 -31
  137. package/lib/styles/textarea.d.ts +1 -2
  138. package/lib/styles/textarea.js +2 -45
  139. package/lib/styles/tooltip.d.ts +0 -1
  140. package/lib/styles/tooltip.js +0 -10
  141. package/lib/styles/wizard.d.ts +5 -21
  142. package/lib/styles/wizard.js +8 -88
  143. package/lib/types/common.d.ts +0 -2
  144. package/lib/types/common.js +0 -12
  145. package/lib/utils/calendar-utils.d.ts +4 -21
  146. package/lib/utils/calendar-utils.js +4 -177
  147. package/lib/utils/custom-scrollbar.d.ts +6 -20
  148. package/lib/utils/custom-scrollbar.js +1 -45
  149. package/lib/utils/flavor-sheet.d.ts +0 -1
  150. package/lib/utils/flavor-sheet.js +0 -23
  151. package/lib/utils/icon-registry.d.ts +0 -2
  152. package/lib/utils/icon-registry.js +1 -107
  153. package/lib/utils/icons.d.ts +0 -1
  154. package/lib/utils/icons.js +0 -9
  155. package/lib/utils/loader-registry.d.ts +0 -1
  156. package/lib/utils/loader-registry.js +0 -32
  157. package/lib/utils/locale.d.ts +2 -23
  158. package/lib/utils/locale.js +0 -86
  159. package/lib/utils/mobile.d.ts +0 -1
  160. package/lib/utils/mobile.js +0 -17
  161. package/lib/utils/number-format.d.ts +6 -3
  162. package/lib/utils/number-format.js +11 -67
  163. package/lib/utils/positioning.d.ts +50 -7
  164. package/lib/utils/positioning.js +52 -62
  165. package/lib/utils/property-capture.d.ts +5 -27
  166. package/lib/utils/property-capture.js +0 -108
  167. package/lib/utils/property-manager.d.ts +0 -1
  168. package/lib/utils/property-manager.js +0 -70
  169. package/lib/utils/resize-observer.d.ts +0 -1
  170. package/lib/utils/resize-observer.js +0 -36
  171. package/lib/utils/scroll-lock.d.ts +4 -11
  172. package/lib/utils/scroll-lock.js +0 -107
  173. package/lib/utils/styles.d.ts +0 -1
  174. package/lib/utils/styles.js +0 -24
  175. package/lib/version.d.ts +1 -2
  176. package/lib/version.js +1 -11
  177. package/package.json +2 -2
  178. package/lib/base/ty-component.d.ts.map +0 -1
  179. package/lib/base/ty-component.js.map +0 -1
  180. package/lib/components/button.d.ts.map +0 -1
  181. package/lib/components/button.js.map +0 -1
  182. package/lib/components/calendar-month.d.ts.map +0 -1
  183. package/lib/components/calendar-month.js.map +0 -1
  184. package/lib/components/calendar-navigation.d.ts.map +0 -1
  185. package/lib/components/calendar-navigation.js.map +0 -1
  186. package/lib/components/calendar.d.ts.map +0 -1
  187. package/lib/components/calendar.js.map +0 -1
  188. package/lib/components/checkbox.d.ts.map +0 -1
  189. package/lib/components/checkbox.js.map +0 -1
  190. package/lib/components/copy.d.ts.map +0 -1
  191. package/lib/components/copy.js.map +0 -1
  192. package/lib/components/date-picker.d.ts.map +0 -1
  193. package/lib/components/date-picker.js.map +0 -1
  194. package/lib/components/file-upload.d.ts.map +0 -1
  195. package/lib/components/file-upload.js.map +0 -1
  196. package/lib/components/icon.d.ts.map +0 -1
  197. package/lib/components/icon.js.map +0 -1
  198. package/lib/components/input.d.ts.map +0 -1
  199. package/lib/components/input.js.map +0 -1
  200. package/lib/components/modal.d.ts.map +0 -1
  201. package/lib/components/modal.js.map +0 -1
  202. package/lib/components/option.d.ts.map +0 -1
  203. package/lib/components/option.js.map +0 -1
  204. package/lib/components/popup.d.ts.map +0 -1
  205. package/lib/components/popup.js.map +0 -1
  206. package/lib/components/radio.d.ts.map +0 -1
  207. package/lib/components/radio.js.map +0 -1
  208. package/lib/components/resize-observer.d.ts.map +0 -1
  209. package/lib/components/resize-observer.js.map +0 -1
  210. package/lib/components/scroll-container.d.ts.map +0 -1
  211. package/lib/components/scroll-container.js.map +0 -1
  212. package/lib/components/select.d.ts.map +0 -1
  213. package/lib/components/select.js.map +0 -1
  214. package/lib/components/selected-tags.d.ts.map +0 -1
  215. package/lib/components/selected-tags.js.map +0 -1
  216. package/lib/components/step.d.ts.map +0 -1
  217. package/lib/components/step.js.map +0 -1
  218. package/lib/components/switch.d.ts.map +0 -1
  219. package/lib/components/switch.js.map +0 -1
  220. package/lib/components/tab.d.ts.map +0 -1
  221. package/lib/components/tab.js.map +0 -1
  222. package/lib/components/tabs.d.ts.map +0 -1
  223. package/lib/components/tabs.js.map +0 -1
  224. package/lib/components/tag.d.ts.map +0 -1
  225. package/lib/components/tag.js.map +0 -1
  226. package/lib/components/textarea.d.ts.map +0 -1
  227. package/lib/components/textarea.js.map +0 -1
  228. package/lib/components/tooltip.d.ts.map +0 -1
  229. package/lib/components/tooltip.js.map +0 -1
  230. package/lib/components/wizard.d.ts.map +0 -1
  231. package/lib/components/wizard.js.map +0 -1
  232. package/lib/icons/fontawesome/brands.d.ts.map +0 -1
  233. package/lib/icons/fontawesome/brands.js.map +0 -1
  234. package/lib/icons/fontawesome/regular.d.ts.map +0 -1
  235. package/lib/icons/fontawesome/regular.js.map +0 -1
  236. package/lib/icons/fontawesome/solid.d.ts.map +0 -1
  237. package/lib/icons/fontawesome/solid.js.map +0 -1
  238. package/lib/icons/heroicons/micro.d.ts.map +0 -1
  239. package/lib/icons/heroicons/micro.js.map +0 -1
  240. package/lib/icons/heroicons/mini.d.ts.map +0 -1
  241. package/lib/icons/heroicons/mini.js.map +0 -1
  242. package/lib/icons/heroicons/outline.d.ts.map +0 -1
  243. package/lib/icons/heroicons/outline.js.map +0 -1
  244. package/lib/icons/heroicons/solid.d.ts.map +0 -1
  245. package/lib/icons/heroicons/solid.js.map +0 -1
  246. package/lib/icons/lucide.d.ts.map +0 -1
  247. package/lib/icons/lucide.js.map +0 -1
  248. package/lib/icons/material/filled.d.ts.map +0 -1
  249. package/lib/icons/material/filled.js.map +0 -1
  250. package/lib/icons/material/outlined.d.ts.map +0 -1
  251. package/lib/icons/material/outlined.js.map +0 -1
  252. package/lib/icons/material/round.d.ts.map +0 -1
  253. package/lib/icons/material/round.js.map +0 -1
  254. package/lib/icons/material/sharp.d.ts.map +0 -1
  255. package/lib/icons/material/sharp.js.map +0 -1
  256. package/lib/icons/material/two-tone.d.ts.map +0 -1
  257. package/lib/icons/material/two-tone.js.map +0 -1
  258. package/lib/index.d.ts.map +0 -1
  259. package/lib/index.js.map +0 -1
  260. package/lib/styles/button.d.ts.map +0 -1
  261. package/lib/styles/button.js.map +0 -1
  262. package/lib/styles/calendar-month.d.ts.map +0 -1
  263. package/lib/styles/calendar-month.js.map +0 -1
  264. package/lib/styles/calendar-navigation.d.ts.map +0 -1
  265. package/lib/styles/calendar-navigation.js.map +0 -1
  266. package/lib/styles/calendar.d.ts.map +0 -1
  267. package/lib/styles/calendar.js.map +0 -1
  268. package/lib/styles/checkbox.d.ts.map +0 -1
  269. package/lib/styles/checkbox.js.map +0 -1
  270. package/lib/styles/copy.d.ts.map +0 -1
  271. package/lib/styles/copy.js.map +0 -1
  272. package/lib/styles/custom-scrollbar.d.ts.map +0 -1
  273. package/lib/styles/custom-scrollbar.js.map +0 -1
  274. package/lib/styles/date-picker.d.ts.map +0 -1
  275. package/lib/styles/date-picker.js.map +0 -1
  276. package/lib/styles/file-upload.d.ts.map +0 -1
  277. package/lib/styles/file-upload.js.map +0 -1
  278. package/lib/styles/icon.d.ts.map +0 -1
  279. package/lib/styles/icon.js.map +0 -1
  280. package/lib/styles/input.d.ts.map +0 -1
  281. package/lib/styles/input.js.map +0 -1
  282. package/lib/styles/modal.d.ts.map +0 -1
  283. package/lib/styles/modal.js.map +0 -1
  284. package/lib/styles/multiselect.d.ts +0 -8
  285. package/lib/styles/multiselect.d.ts.map +0 -1
  286. package/lib/styles/multiselect.js +0 -849
  287. package/lib/styles/multiselect.js.map +0 -1
  288. package/lib/styles/option.d.ts.map +0 -1
  289. package/lib/styles/option.js.map +0 -1
  290. package/lib/styles/popup.d.ts.map +0 -1
  291. package/lib/styles/popup.js.map +0 -1
  292. package/lib/styles/radio.d.ts.map +0 -1
  293. package/lib/styles/radio.js.map +0 -1
  294. package/lib/styles/resize-observer.d.ts.map +0 -1
  295. package/lib/styles/resize-observer.js.map +0 -1
  296. package/lib/styles/scroll-container.d.ts.map +0 -1
  297. package/lib/styles/scroll-container.js.map +0 -1
  298. package/lib/styles/select-base.d.ts.map +0 -1
  299. package/lib/styles/select-base.js.map +0 -1
  300. package/lib/styles/select.d.ts.map +0 -1
  301. package/lib/styles/select.js.map +0 -1
  302. package/lib/styles/step.d.ts.map +0 -1
  303. package/lib/styles/step.js.map +0 -1
  304. package/lib/styles/switch.d.ts.map +0 -1
  305. package/lib/styles/switch.js.map +0 -1
  306. package/lib/styles/tab.d.ts.map +0 -1
  307. package/lib/styles/tab.js.map +0 -1
  308. package/lib/styles/tabs.d.ts.map +0 -1
  309. package/lib/styles/tabs.js.map +0 -1
  310. package/lib/styles/tag.d.ts.map +0 -1
  311. package/lib/styles/tag.js.map +0 -1
  312. package/lib/styles/textarea.d.ts.map +0 -1
  313. package/lib/styles/textarea.js.map +0 -1
  314. package/lib/styles/tooltip.d.ts.map +0 -1
  315. package/lib/styles/tooltip.js.map +0 -1
  316. package/lib/styles/wizard.d.ts.map +0 -1
  317. package/lib/styles/wizard.js.map +0 -1
  318. package/lib/types/common.d.ts.map +0 -1
  319. package/lib/types/common.js.map +0 -1
  320. package/lib/utils/calendar-utils.d.ts.map +0 -1
  321. package/lib/utils/calendar-utils.js.map +0 -1
  322. package/lib/utils/custom-scrollbar.d.ts.map +0 -1
  323. package/lib/utils/custom-scrollbar.js.map +0 -1
  324. package/lib/utils/flavor-sheet.d.ts.map +0 -1
  325. package/lib/utils/flavor-sheet.js.map +0 -1
  326. package/lib/utils/icon-registry.d.ts.map +0 -1
  327. package/lib/utils/icon-registry.js.map +0 -1
  328. package/lib/utils/icons.d.ts.map +0 -1
  329. package/lib/utils/icons.js.map +0 -1
  330. package/lib/utils/loader-registry.d.ts.map +0 -1
  331. package/lib/utils/loader-registry.js.map +0 -1
  332. package/lib/utils/locale.d.ts.map +0 -1
  333. package/lib/utils/locale.js.map +0 -1
  334. package/lib/utils/mobile.d.ts.map +0 -1
  335. package/lib/utils/mobile.js.map +0 -1
  336. package/lib/utils/number-format.d.ts.map +0 -1
  337. package/lib/utils/number-format.js.map +0 -1
  338. package/lib/utils/parse-boolean.d.ts +0 -39
  339. package/lib/utils/parse-boolean.d.ts.map +0 -1
  340. package/lib/utils/parse-boolean.js +0 -58
  341. package/lib/utils/parse-boolean.js.map +0 -1
  342. package/lib/utils/positioning.d.ts.map +0 -1
  343. package/lib/utils/positioning.js.map +0 -1
  344. package/lib/utils/property-capture.d.ts.map +0 -1
  345. package/lib/utils/property-capture.js.map +0 -1
  346. package/lib/utils/property-manager.d.ts.map +0 -1
  347. package/lib/utils/property-manager.js.map +0 -1
  348. package/lib/utils/resize-observer.d.ts.map +0 -1
  349. package/lib/utils/resize-observer.js.map +0 -1
  350. package/lib/utils/scroll-lock.d.ts.map +0 -1
  351. package/lib/utils/scroll-lock.js.map +0 -1
  352. package/lib/utils/styles.d.ts.map +0 -1
  353. package/lib/utils/styles.js.map +0 -1
  354. package/lib/version.d.ts.map +0 -1
  355. package/lib/version.js.map +0 -1
@@ -1,69 +1,14 @@
1
- /**
2
- * TyDatePicker Web Component
3
- * PORTED FROM: cljs/ty/components/date_picker.cljs
4
- *
5
- * A date picker component with read-only input and calendar dropdown.
6
- * Supports date-only and date+time modes with smart time input.
7
- *
8
- * Architecture:
9
- * - Read-only input stub (displays formatted date)
10
- * - Calendar dropdown (modal dialog with ty-calendar)
11
- * - Optional time input (with smart digit navigation)
12
- * - Form participation via ElementInternals
13
- * - UTC output, local display
14
- *
15
- * Features:
16
- * - Date selection with calendar dropdown
17
- * - Optional time input with smart navigation
18
- * - Form integration (works with FormData)
19
- * - UTC value output for server communication
20
- * - Localized display formatting (Intl API)
21
- * - Clearable with clear button
22
- * - Keyboard navigation (Escape to close)
23
- * - Outside click handling
24
- *
25
- * @example
26
- * ```html
27
- * <!-- Basic date picker -->
28
- * <ty-date-picker
29
- * label="Select Date"
30
- * placeholder="Choose a date">
31
- * </ty-date-picker>
32
- *
33
- * <!-- With time -->
34
- * <ty-date-picker
35
- * with-time="true"
36
- * label="Select Date & Time">
37
- * </ty-date-picker>
38
- *
39
- * <!-- Form integration -->
40
- * <form>
41
- * <ty-date-picker name="booking-date"></ty-date-picker>
42
- * <button type="submit">Submit</button>
43
- * </form>
44
- *
45
- * <!-- Pre-filled value (UTC) -->
46
- * <ty-date-picker value="2024-09-21T08:30:00.000Z"></ty-date-picker>
47
- * ```
48
- */
49
1
  import { ensureStyles } from '../utils/styles.js';
50
2
  import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
51
3
  import { datePickerStyles, datePickerCustomFlavorCss } from '../styles/date-picker.js';
52
4
  import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
53
- import { computeAnchoredPosition } from '../utils/positioning.js';
5
+ import { computeAnchoredPosition, placementToAnchored } from '../utils/positioning.js';
54
6
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
55
7
  import { isMobileTouch } from '../utils/mobile.js';
56
8
  import { TyComponent } from '../base/ty-component.js';
57
- // ============================================================================
58
- // Constants
59
- // ============================================================================
60
9
  const CALENDAR_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='16' height='16' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='4' width='18' height='18' rx='2' ry='2'></rect><line x1='16' y1='2' x2='16' y2='6'></line><line x1='8' y1='2' x2='8' y2='6'></line><line x1='3' y1='10' x2='21' y2='10'></line></svg>`;
61
10
  const CLEAR_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='14' height='14' xmlns='http://www.w3.org/2000/svg'><line x1='18' y1='6' x2='6' y2='18'></line><line x1='6' y1='6' x2='18' y2='18'></line></svg>`;
62
11
  const SCHEDULE_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='16' height='16' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='10'></circle><polyline points='12,6 12,12 16,14'></polyline></svg>`;
63
- // Stable per-instance id (survives across full-rebuild re-renders, unlike a
64
- // fresh Math.random() per render) — used to wire the stub's aria-labelledby
65
- // to the label element it's paired with. Same pattern as select.ts's
66
- // getElementHash.
67
12
  const elementIds = new WeakMap();
68
13
  let elementIdCounter = 0;
69
14
  function getElementHash(element) {
@@ -74,86 +19,43 @@ function getElementHash(element) {
74
19
  }
75
20
  return id;
76
21
  }
77
- // ============================================================================
78
- // Helper Functions - Date Parsing & Conversion
79
- // ============================================================================
80
- /**
81
- * Parse ANY input format into year/month/day/hour/minute components.
82
- * Accepts:
83
- * - UTC strings: '2024-09-21T08:30:00Z' or '2024-09-21T08:30:00.000Z'
84
- * - Datetime-local: '2024-09-21T10:30'
85
- * - Date only: '2024-09-21'
86
- * - Timestamps: milliseconds since epoch
87
- * - With timezone: '2024-09-21T10:30:00+02:00'
88
- *
89
- * Always extracts components in LOCAL timezone for display/editing.
90
- *
91
- * PORTED FROM: parse-value in date_picker.cljs
92
- */
93
22
  function parseValue(value, withTime) {
94
23
  if (!value)
95
24
  return null;
96
25
  let dateObj = null;
97
26
  if (typeof value === 'string') {
98
- // Date-only format: YYYY-MM-DD
99
27
  if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
100
28
  const [yearStr, monthStr, dayStr] = value.split('-');
101
29
  const year = parseInt(yearStr, 10);
102
- const month = parseInt(monthStr, 10) - 1; // 0-based for JS Date
30
+ const month = parseInt(monthStr, 10) - 1;
103
31
  const day = parseInt(dayStr, 10);
104
- // Create at midnight local time
105
32
  dateObj = new Date(year, month, day, 0, 0, 0, 0);
106
33
  }
107
34
  else {
108
- // Let JS Date handle datetime-local, UTC, with timezone
109
35
  dateObj = new Date(value);
110
36
  }
111
37
  }
112
38
  else if (typeof value === 'number') {
113
- // Numeric inputs (milliseconds)
114
39
  dateObj = new Date(value);
115
40
  }
116
41
  if (!dateObj || isNaN(dateObj.getTime())) {
117
42
  return null;
118
43
  }
119
- // Extract components in LOCAL timezone for display
120
44
  const components = {
121
45
  year: dateObj.getFullYear(),
122
- month: dateObj.getMonth() + 1, // Convert to 1-based
46
+ month: dateObj.getMonth() + 1,
123
47
  day: dateObj.getDate(),
124
48
  hour: withTime ? dateObj.getHours() : 0,
125
49
  minute: withTime ? dateObj.getMinutes() : 0,
126
50
  };
127
51
  return components;
128
52
  }
129
- /**
130
- * Convert internal components to Date object.
131
- *
132
- * PORTED FROM: components->date-object in date_picker.cljs
133
- */
134
53
  function componentsToDateObject(components) {
135
54
  const { year, month, day, hour, minute } = components;
136
55
  if (!year || !month || !day)
137
56
  return null;
138
- // Create local date
139
- return new Date(year, month - 1, // Convert to 0-based
140
- day, hour || 0, minute || 0, 0, // seconds
141
- 0 // milliseconds
142
- );
57
+ return new Date(year, month - 1, day, hour || 0, minute || 0, 0, 0);
143
58
  }
144
- /**
145
- * Convert internal components to UTC ISO 8601 format.
146
- *
147
- * For date+time mode: Outputs full UTC timestamp with milliseconds
148
- * Example: '2024-09-21T08:30:00.000Z'
149
- *
150
- * For date-only mode: Outputs UTC timestamp at midnight local time
151
- * Example: '2024-09-20T22:00:00.000Z' (midnight Sept 21 CEST = 10pm Sept 20 UTC)
152
- *
153
- * Always returns UTC to ensure unambiguous server communication.
154
- *
155
- * PORTED FROM: components->output-value in date_picker.cljs
156
- */
157
59
  function componentsToOutputValue(components) {
158
60
  if (!components.year || !components.month || !components.day) {
159
61
  return null;
@@ -161,18 +63,6 @@ function componentsToOutputValue(components) {
161
63
  const dateObj = componentsToDateObject(components);
162
64
  return dateObj ? dateObj.toISOString() : null;
163
65
  }
164
- /**
165
- * Convert internal components to local datetime-local format.
166
- *
167
- * For date+time mode: Outputs local datetime without timezone
168
- * Example: '2024-09-21T10:30'
169
- *
170
- * For date-only mode: Outputs date only
171
- * Example: '2024-09-21'
172
- *
173
- * This format matches HTML5 <input type="datetime-local"> and is useful
174
- * for setting other inputs or displaying local time without timezone conversion.
175
- */
176
66
  function componentsToLocalValue(components, withTime) {
177
67
  if (!components.year || !components.month || !components.day) {
178
68
  return null;
@@ -189,11 +79,6 @@ function componentsToLocalValue(components, withTime) {
189
79
  return `${year}-${month}-${day}`;
190
80
  }
191
81
  }
192
- /**
193
- * Format components for display in input using Intl API.
194
- *
195
- * PORTED FROM: format-display-value in date_picker.cljs
196
- */
197
82
  function formatDisplayValue(components, formatType, locale, withTime) {
198
83
  if (!components.year || !components.month || !components.day) {
199
84
  return null;
@@ -204,21 +89,12 @@ function formatDisplayValue(components, formatType, locale, withTime) {
204
89
  const options = {
205
90
  dateStyle: formatType || 'long',
206
91
  };
207
- // Add time styling if with-time is enabled
208
92
  if (withTime) {
209
93
  options.timeStyle = 'short';
210
94
  }
211
95
  const formatter = new Intl.DateTimeFormat(locale, options);
212
96
  return formatter.format(dateObj);
213
97
  }
214
- // ============================================================================
215
- // Helper Functions - Time Input
216
- // ============================================================================
217
- /**
218
- * Parse hour and minute from raw digits (4 chars: "HHmm")
219
- *
220
- * PORTED FROM: parse-time-components in date_picker.cljs
221
- */
222
98
  function parseTimeComponents(rawDigits) {
223
99
  if (!rawDigits || rawDigits.length !== 4)
224
100
  return null;
@@ -231,99 +107,60 @@ function parseTimeComponents(rawDigits) {
231
107
  }
232
108
  return { hour, minute };
233
109
  }
234
- /**
235
- * Validate if digit is valid for given position
236
- * Position 0: first hour digit (0-2)
237
- * Position 1: second hour digit (0-9, but 0-3 if first is 2)
238
- * Position 3: first minute digit (0-5)
239
- * Position 4: second minute digit (0-9)
240
- *
241
- * PORTED FROM: validate-time-digit in date_picker.cljs
242
- */
243
110
  function validateTimeDigit(digit, position, currentDigits) {
244
111
  switch (position) {
245
112
  case 0:
246
- return digit <= 2; // First hour digit: 0-2
113
+ return digit <= 2;
247
114
  case 1: {
248
115
  const firstHour = parseInt(currentDigits[0], 10);
249
- return firstHour === 2 ? digit <= 3 : true; // If hour starts with 2, max 23
116
+ return firstHour === 2 ? digit <= 3 : true;
250
117
  }
251
118
  case 3:
252
- return digit <= 5; // First minute digit: 0-5
119
+ return digit <= 5;
253
120
  case 4:
254
- return true; // Second minute digit: 0-9
121
+ return true;
255
122
  default:
256
123
  return false;
257
124
  }
258
125
  }
259
- /**
260
- * Format hour and minute into "HH:mm" display
261
- *
262
- * PORTED FROM: format-time-display in date_picker.cljs
263
- */
264
126
  function formatTimeDisplay(hour, minute) {
265
127
  const hourStr = hour.toString().padStart(2, '0');
266
128
  const minuteStr = minute.toString().padStart(2, '0');
267
129
  return `${hourStr}:${minuteStr}`;
268
130
  }
269
- /**
270
- * Find next editable position, skipping delimiter at position 2
271
- *
272
- * PORTED FROM: find-next-editable-position in date_picker.cljs
273
- */
274
131
  function findNextEditablePosition(currentPos) {
275
132
  switch (currentPos) {
276
- case 0: return 1; // 0 -> 1 (within hour)
277
- case 1: return 3; // 1 -> 3 (skip delimiter, go to minute)
278
- case 3: return 4; // 3 -> 4 (within minute)
279
- case 4: return 5; // 4 -> 5 (after last digit)
280
- case 5: return 5; // 5 -> 5 (stay at end)
133
+ case 0: return 1;
134
+ case 1: return 3;
135
+ case 3: return 4;
136
+ case 4: return 5;
137
+ case 5: return 5;
281
138
  default: return currentPos;
282
139
  }
283
140
  }
284
- /**
285
- * Find previous editable position, skipping delimiter at position 2
286
- *
287
- * PORTED FROM: find-prev-editable-position in date_picker.cljs
288
- */
289
141
  function findPrevEditablePosition(currentPos) {
290
142
  switch (currentPos) {
291
- case 5: return 4; // 5 -> 4 (from after last digit)
292
- case 4: return 3; // 4 -> 3 (within minute)
293
- case 3: return 1; // 3 -> 1 (skip delimiter, go to hour)
294
- case 1: return 0; // 1 -> 0 (within hour)
295
- case 0: return 0; // 0 -> 0 (stay at start)
143
+ case 5: return 4;
144
+ case 4: return 3;
145
+ case 3: return 1;
146
+ case 1: return 0;
147
+ case 0: return 0;
296
148
  default: return currentPos;
297
149
  }
298
150
  }
299
- /**
300
- * Convert internal position (0,1,3,4) to raw digits index (0,1,2,3)
301
- *
302
- * PORTED FROM: position->raw-digit-index in date_picker.cljs
303
- */
304
151
  function positionToRawDigitIndex(internalPos) {
305
152
  switch (internalPos) {
306
- case 0: return 0; // Position 0 → raw digit 0 (first hour)
307
- case 1: return 1; // Position 1 → raw digit 1 (second hour)
308
- case 3: return 2; // Position 3 → raw digit 2 (first minute)
309
- case 4: return 3; // Position 4 → raw digit 3 (second minute)
153
+ case 0: return 0;
154
+ case 1: return 1;
155
+ case 3: return 2;
156
+ case 4: return 3;
310
157
  default: return 0;
311
158
  }
312
159
  }
313
- // ============================================================================
314
- // TimeInput Helper Class
315
- // ============================================================================
316
- /**
317
- * TimeInput manages the state and behavior of the time input element.
318
- * Handles smart cursor navigation, digit replacement, and validation.
319
- *
320
- * PORTED FROM: Time input state management functions in date_picker.cljs
321
- */
322
160
  class TimeInput {
323
161
  constructor(element, datePickerElement, hour = 0, minute = 0) {
324
162
  this.element = element;
325
163
  this.datePickerElement = datePickerElement;
326
- // Initialize state
327
164
  const display = formatTimeDisplay(hour, minute);
328
165
  const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
329
166
  this.state = {
@@ -333,23 +170,15 @@ class TimeInput {
333
170
  displayValue: display,
334
171
  rawDigits,
335
172
  };
336
- // Set initial value
337
173
  this.element.value = display;
338
- // Bind event handlers
339
174
  this.setupEventListeners();
340
175
  }
341
- /**
342
- * Setup all event listeners for the time input
343
- */
344
176
  setupEventListeners() {
345
177
  this.element.addEventListener('keydown', (e) => this.handleKeyDown(e));
346
178
  this.element.addEventListener('input', (e) => this.handleInput(e));
347
179
  this.element.addEventListener('click', () => this.handleClick());
348
180
  this.element.addEventListener('focus', () => this.handleFocus());
349
181
  }
350
- /**
351
- * Handle keydown events
352
- */
353
182
  handleKeyDown(event) {
354
183
  const key = event.key;
355
184
  switch (key) {
@@ -374,89 +203,52 @@ class TimeInput {
374
203
  this.updateState({ caretPosition: 5 });
375
204
  break;
376
205
  case 'Tab':
377
- // Allow default tab behavior
378
206
  break;
379
207
  default:
380
- // Handle digit input
381
208
  if (/^\d$/.test(key)) {
382
209
  this.handleDigitInput(event, parseInt(key, 10));
383
210
  }
384
211
  break;
385
212
  }
386
213
  }
387
- /**
388
- * Handle input events (prevent default browser input)
389
- */
390
214
  handleInput(event) {
391
215
  event.preventDefault();
392
216
  }
393
- /**
394
- * Handle click events - position cursor at first digit
395
- */
396
217
  handleClick() {
397
218
  this.updateState({ caretPosition: 0 });
398
219
  }
399
- /**
400
- * Handle focus events - position cursor at first digit
401
- */
402
220
  handleFocus() {
403
221
  this.updateState({ caretPosition: 0 });
404
222
  }
405
- /**
406
- * Handle arrow right - move to next editable position
407
- *
408
- * PORTED FROM: handle-time-arrow-right! in date_picker.cljs
409
- */
410
223
  handleArrowRight(event) {
411
224
  event.preventDefault();
412
225
  const nextPos = findNextEditablePosition(this.state.caretPosition);
413
226
  this.updateState({ caretPosition: nextPos });
414
227
  }
415
- /**
416
- * Handle arrow left - move to previous editable position
417
- *
418
- * PORTED FROM: handle-time-arrow-left! in date_picker.cljs
419
- */
420
228
  handleArrowLeft(event) {
421
229
  event.preventDefault();
422
230
  const prevPos = findPrevEditablePosition(this.state.caretPosition);
423
231
  this.updateState({ caretPosition: prevPos });
424
232
  }
425
- /**
426
- * Handle digit input - replace digit at cursor position
427
- *
428
- * PORTED FROM: handle-time-digit-input! in date_picker.cljs
429
- */
430
233
  handleDigitInput(event, digit) {
431
234
  event.preventDefault();
432
235
  const currentPos = this.state.caretPosition;
433
- // Only process if at editable position and digit is valid
434
236
  if (![0, 1, 3, 4].includes(currentPos))
435
237
  return;
436
238
  if (!validateTimeDigit(digit, currentPos, this.state.rawDigits))
437
239
  return;
438
- // Replace digit at current position
439
240
  const newState = this.replaceDigitAtPosition(currentPos, digit);
440
241
  if (!newState)
441
242
  return;
442
- // Move to next position
443
243
  const nextPos = findNextEditablePosition(currentPos);
444
244
  this.updateState({ ...newState, caretPosition: nextPos });
445
- // Notify date picker of time change
446
245
  this.notifyTimeChange();
447
246
  }
448
- /**
449
- * Handle backspace - zero digit and move back
450
- *
451
- * PORTED FROM: handle-time-backspace! in date_picker.cljs
452
- */
453
247
  handleBackspace(event) {
454
248
  event.preventDefault();
455
249
  const currentPos = this.state.caretPosition;
456
- // Can't go back from position 0
457
250
  if (currentPos === 0)
458
251
  return;
459
- // Find target position to zero
460
252
  const targetPos = currentPos === 1 ? 0 :
461
253
  currentPos === 3 ? 1 :
462
254
  currentPos === 4 ? 3 :
@@ -467,15 +259,9 @@ class TimeInput {
467
259
  this.updateState({ ...newState, caretPosition: targetPos });
468
260
  this.notifyTimeChange();
469
261
  }
470
- /**
471
- * Handle delete - zero digit at current position
472
- *
473
- * PORTED FROM: handle-time-delete! in date_picker.cljs
474
- */
475
262
  handleDelete(event) {
476
263
  event.preventDefault();
477
264
  const currentPos = this.state.caretPosition;
478
- // Only at editable positions
479
265
  if (![0, 1, 3, 4].includes(currentPos))
480
266
  return;
481
267
  const newState = this.zeroDigitAtPosition(currentPos);
@@ -484,11 +270,6 @@ class TimeInput {
484
270
  this.updateState({ ...newState, caretPosition: currentPos });
485
271
  this.notifyTimeChange();
486
272
  }
487
- /**
488
- * Replace digit at specific position
489
- *
490
- * PORTED FROM: replace-digit-at-position in date_picker.cljs
491
- */
492
273
  replaceDigitAtPosition(position, newDigit) {
493
274
  const rawIndex = positionToRawDigitIndex(position);
494
275
  const digitsArray = this.state.rawDigits.split('');
@@ -504,24 +285,12 @@ class TimeInput {
504
285
  displayValue: formatTimeDisplay(parsed.hour, parsed.minute),
505
286
  };
506
287
  }
507
- /**
508
- * Zero digit at specific position
509
- *
510
- * PORTED FROM: zero-digit-at-position in date_picker.cljs
511
- */
512
288
  zeroDigitAtPosition(position) {
513
289
  return this.replaceDigitAtPosition(position, 0);
514
290
  }
515
- /**
516
- * Update internal state and refresh display
517
- *
518
- * PORTED FROM: update-time-input-state! in date_picker.cljs
519
- */
520
291
  updateState(updates) {
521
292
  this.state = { ...this.state, ...updates };
522
- // Update display value
523
293
  this.element.value = this.state.displayValue;
524
- // Set cursor position, mapping internal positions to DOM positions
525
294
  const caretPos = this.state.caretPosition;
526
295
  const actualPos = caretPos === 0 ? 0 :
527
296
  caretPos === 1 ? 1 :
@@ -531,24 +300,15 @@ class TimeInput {
531
300
  caretPos === 5 ? 5 : caretPos;
532
301
  this.element.setSelectionRange(actualPos, actualPos);
533
302
  }
534
- /**
535
- * Notify date picker of time change
536
- */
537
303
  notifyTimeChange() {
538
304
  this.datePickerElement.handleTimeInputChange(this.state.hour, this.state.minute);
539
305
  }
540
- /**
541
- * Get current time values
542
- */
543
306
  getTime() {
544
307
  return {
545
308
  hour: this.state.hour,
546
309
  minute: this.state.minute,
547
310
  };
548
311
  }
549
- /**
550
- * Update time from external source
551
- */
552
312
  setTime(hour, minute) {
553
313
  const display = formatTimeDisplay(hour, minute);
554
314
  const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
@@ -562,76 +322,43 @@ class TimeInput {
562
322
  this.element.value = display;
563
323
  }
564
324
  }
565
- // ============================================================================
566
- // TyDatePicker Custom Element
567
- // ============================================================================
568
325
  export class TyDatePicker extends TyComponent {
569
- // observedAttributes is auto-generated by TyComponent from properties config
570
326
  constructor() {
571
- super(); // TyComponent handles attachShadow and attachInternals
572
- // ============================================================================
573
- // INTERNAL STATE
574
- // ============================================================================
575
- // Internal state
327
+ super();
576
328
  this._state = {
577
329
  withTime: false,
578
330
  open: false,
579
331
  };
580
- // Reopen guard — prevents click event from reopening after pointerdown close
581
332
  this._closeTimestamp = 0;
582
333
  this._customFlavorSheet = null;
583
- // Initialize styles in shadow root
584
334
  const shadow = this.shadowRoot;
585
335
  ensureStyles(shadow, { css: datePickerStyles, id: 'ty-date-picker' });
586
336
  }
587
- // ==========================================================================
588
- // Lifecycle Hooks (TyComponent)
589
- // ==========================================================================
590
- /**
591
- * Called when component connects to DOM
592
- * TyComponent handles property capture automatically
593
- */
594
337
  onConnect() {
595
- // Initialize component state
596
338
  this.initializeState();
597
- // Initial render
598
339
  this.render();
599
- // Setup locale observer to watch for ancestor lang changes
600
340
  this._localeObserver = observeLocaleChanges(this, () => {
601
341
  this.render();
602
342
  });
603
343
  this._syncCustomFlavor();
604
344
  }
605
- /** Custom (non-built-in) flavors — see utils/flavor-sheet.ts. */
606
345
  _syncCustomFlavor() {
607
- // 'default' is this component's neutral: map it to a built-in so no
608
- // custom sheet is generated for it.
609
346
  const flavor = this.flavor === 'default' ? 'neutral' : this.flavor;
610
347
  this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, flavor, ({ base }) => datePickerCustomFlavorCss(base));
611
348
  }
612
- /**
613
- * Called when component disconnects from DOM
614
- */
615
349
  onDisconnect() {
616
- // Cleanup locale observer
617
350
  if (this._localeObserver) {
618
351
  this._localeObserver();
619
352
  this._localeObserver = undefined;
620
353
  }
621
- // Cleanup event listeners and state
622
354
  this.cleanup();
623
355
  }
624
- /**
625
- * Called when properties change
626
- * Handle state updates BEFORE render
627
- */
628
356
  onPropertiesChanged(changes) {
629
357
  for (const { name, newValue } of changes) {
630
358
  switch (name) {
631
359
  case 'flavor':
632
360
  this._syncCustomFlavor();
633
361
  break;
634
- // Simple visual properties - just trigger re-render (handled by TyComponent)
635
362
  case 'size':
636
363
  case 'label':
637
364
  case 'placeholder':
@@ -640,30 +367,21 @@ export class TyDatePicker extends TyComponent {
640
367
  case 'clearable':
641
368
  case 'format':
642
369
  case 'locale':
643
- // These properties just affect rendering, no internal state to update
644
- // TyComponent will call render() automatically for visual properties
645
370
  break;
646
- // Complex properties
647
371
  case 'value': {
648
- // Parse the new value using current withTime setting
649
372
  const newComponents = parseValue(newValue, this._state.withTime);
650
- // If newComponents is null, CLEAR the state completely
651
373
  if (newComponents === null) {
652
- // Check if we actually have a date to clear
653
374
  const hasDate = this._state.year !== undefined ||
654
375
  this._state.month !== undefined ||
655
376
  this._state.day !== undefined;
656
377
  if (hasDate) {
657
- // Clear all date components, keeping only withTime and open flags
658
378
  this._state = {
659
379
  withTime: this._state.withTime,
660
380
  open: this._state.open,
661
- // year, month, day, hour, minute are now undefined
662
381
  };
663
382
  }
664
383
  break;
665
384
  }
666
- // Check if components actually changed
667
385
  const currentComponents = {
668
386
  year: this._state.year,
669
387
  month: this._state.month,
@@ -677,7 +395,6 @@ export class TyDatePicker extends TyComponent {
677
395
  newComponents?.hour !== currentComponents.hour ||
678
396
  newComponents?.minute !== currentComponents.minute;
679
397
  if (changed) {
680
- // Update state with new date components
681
398
  this._state = {
682
399
  ...this._state,
683
400
  ...newComponents,
@@ -686,13 +403,10 @@ export class TyDatePicker extends TyComponent {
686
403
  break;
687
404
  }
688
405
  case 'with-time': {
689
- // Update internal withTime flag
690
406
  const newWithTime = newValue;
691
407
  const oldWithTime = this._state.withTime;
692
408
  if (newWithTime !== oldWithTime) {
693
409
  this._state.withTime = newWithTime;
694
- // If we have an existing date, re-sync form value
695
- // (format changes based on withTime flag)
696
410
  const hasDate = this._state.year !== undefined &&
697
411
  this._state.month !== undefined &&
698
412
  this._state.day !== undefined;
@@ -705,20 +419,9 @@ export class TyDatePicker extends TyComponent {
705
419
  }
706
420
  }
707
421
  }
708
- // attributeChangedCallback removed - TyComponent handles all attribute changes via onPropertiesChanged
709
- /**
710
- * Get form value - returns UTC string from current state
711
- * TyComponent calls this automatically when value property changes
712
- */
713
422
  getFormValue() {
714
423
  return componentsToOutputValue(this.getComponents());
715
424
  }
716
- // ==========================================================================
717
- // Internal Helpers
718
- // ==========================================================================
719
- /**
720
- * Extract current date components from internal state
721
- */
722
425
  getComponents() {
723
426
  return {
724
427
  year: this._state.year,
@@ -728,34 +431,19 @@ export class TyDatePicker extends TyComponent {
728
431
  minute: this._state.minute,
729
432
  };
730
433
  }
731
- /**
732
- * Whether internal state has a complete date
733
- */
734
434
  hasDate() {
735
435
  return !!(this._state.year && this._state.month && this._state.day);
736
436
  }
737
- /**
738
- * Format current components for display using Intl API
739
- */
740
437
  getFormattedValue(components) {
741
438
  const c = components || this.getComponents();
742
439
  if (!c.year || !c.month || !c.day)
743
440
  return null;
744
441
  return formatDisplayValue(c, this.getProperty('format') || 'long', getEffectiveLocale(this, this.getProperty('locale')), this._state.withTime);
745
442
  }
746
- // ==========================================================================
747
- // State Management
748
- // ==========================================================================
749
- /**
750
- * Initialize component state from attributes
751
- *
752
- * PORTED FROM: init-component-state! in date_picker.cljs
753
- */
754
443
  initializeState() {
755
444
  const valueAttr = this.getProperty('value');
756
445
  const withTime = this.getProperty('with-time');
757
446
  const components = parseValue(valueAttr, withTime);
758
- // If components is null, initialize with empty state
759
447
  if (components === null) {
760
448
  this._state = {
761
449
  withTime,
@@ -769,34 +457,19 @@ export class TyDatePicker extends TyComponent {
769
457
  open: false,
770
458
  };
771
459
  }
772
- // Convert to UTC and sync to form
773
460
  this.syncFormValue();
774
461
  }
775
- /**
776
- * Update internal state
777
- *
778
- * PORTED FROM: update-component-state! in date_picker.cljs
779
- */
780
462
  updateState(updates, forceSync = false) {
781
463
  this._state = { ...this._state, ...updates };
782
- // STAGING: Only sync attributes if dialog is closed OR force-sync is true
783
- // This prevents re-renders during time input editing
784
464
  const shouldSync = forceSync || !this._state.open || !this._state.withTime;
785
465
  if (shouldSync) {
786
466
  this.syncFormValue();
787
467
  }
788
468
  }
789
- /**
790
- * Sync form value with current state
791
- * Compares before setting to prevent circular triggers
792
- */
793
469
  syncFormValue() {
794
470
  const utcValue = this.getFormValue();
795
471
  const currentValue = this.getProperty('value');
796
- // Only update property if value actually changed (prevents circular triggers)
797
472
  if (utcValue !== currentValue) {
798
- // Use property setter to maintain TyComponent lifecycle
799
- // This will automatically handle attribute sync and form value update
800
473
  if (utcValue) {
801
474
  this.setProperty('value', utcValue);
802
475
  }
@@ -804,17 +477,7 @@ export class TyDatePicker extends TyComponent {
804
477
  this.setProperty('value', null);
805
478
  }
806
479
  }
807
- // Form value sync handled automatically by TyComponent (formValue: true)
808
- }
809
- // handleValueChange removed - logic moved to onPropertiesChanged hook
810
- // ==========================================================================
811
- // Time Input Handling
812
- // ==========================================================================
813
- /**
814
- * Handle time input changes from TimeInput class
815
- *
816
- * PORTED FROM: handle-time-change! in date_picker.cljs
817
- */
480
+ }
818
481
  handleTimeInputChange(hour, minute) {
819
482
  if (!this.hasDate())
820
483
  return;
@@ -822,11 +485,6 @@ export class TyDatePicker extends TyComponent {
822
485
  this.updateState(components, true);
823
486
  this.emitChangeEvent(components, 'time-change');
824
487
  }
825
- /**
826
- * Emit change event
827
- *
828
- * PORTED FROM: emit-change-event! in date_picker.cljs
829
- */
830
488
  emitChangeEvent(components, source) {
831
489
  const utcValue = components ? componentsToOutputValue(components) : null;
832
490
  const localValue = components ? componentsToLocalValue(components, this._state.withTime) : null;
@@ -846,12 +504,10 @@ export class TyDatePicker extends TyComponent {
846
504
  });
847
505
  this.dispatchEvent(event);
848
506
  }
849
- // ==========================================================================
850
- // Public API (Properties) - Simple Accessors
851
- // ==========================================================================
852
- // String properties
853
507
  get size() { return this.getProperty('size'); }
854
508
  set size(v) { this.setProperty('size', v); }
509
+ get placement() { return this.getProperty('placement'); }
510
+ set placement(v) { this.setProperty('placement', v); }
855
511
  get flavor() { return this.getProperty('flavor'); }
856
512
  set flavor(v) { this.setProperty('flavor', v); }
857
513
  get label() { return this.getProperty('label'); }
@@ -864,7 +520,6 @@ export class TyDatePicker extends TyComponent {
864
520
  set format(v) { this.setProperty('format', v); }
865
521
  get locale() { return this.getProperty('locale'); }
866
522
  set locale(v) { this.setProperty('locale', v); }
867
- // Boolean properties
868
523
  get required() { return this.getProperty('required'); }
869
524
  set required(v) { this.setProperty('required', v); }
870
525
  get disabled() { return this.getProperty('disabled'); }
@@ -875,48 +530,23 @@ export class TyDatePicker extends TyComponent {
875
530
  set min(v) { this.setProperty('min', v ?? ''); }
876
531
  get max() { return this.getProperty('max'); }
877
532
  set max(v) { this.setProperty('max', v ?? ''); }
878
- // With-time property
879
533
  get withTime() { return this.getProperty('with-time'); }
880
534
  set withTime(v) { this.setProperty('with-time', v); }
881
- // Value property - Keep custom getter/setter for now (complex UTC parsing logic)
882
- /**
883
- * Get current value (UTC ISO string)
884
- */
885
535
  get value() {
886
536
  return this.getProperty('value') || null;
887
537
  }
888
- /**
889
- * Set value (UTC ISO string, Date object, or null)
890
- *
891
- * When set to null/undefined/empty string, the attribute is removed.
892
- * When set to a valid date, the attribute is set to ISO UTC string.
893
- */
894
538
  set value(val) {
895
539
  if (val === null || val === undefined || val === '') {
896
- this.setProperty('value', null); // TyComponent will remove attribute
540
+ this.setProperty('value', null);
897
541
  }
898
542
  else {
899
543
  const strValue = val instanceof Date ? val.toISOString() : val;
900
544
  this.setProperty('value', strValue);
901
545
  }
902
546
  }
903
- /**
904
- * Clear the date value programmatically WITH a change event (source:
905
- * 'clear'). Works regardless of the `clearable` attribute — lets any
906
- * external trigger (e.g. a button elsewhere in the page) clear the value
907
- * without needing the built-in clear button. Mirrors ty-select's clear().
908
- */
909
547
  clear() {
910
548
  this.clearValue();
911
549
  }
912
- // ==========================================================================
913
- // Rendering & DOM Management
914
- // ==========================================================================
915
- /**
916
- * Build CSS classes for the stub element
917
- *
918
- * PORTED FROM: build-stub-classes in date_picker.cljs
919
- */
920
550
  buildStubClasses() {
921
551
  const classes = ['date-picker-stub'];
922
552
  const size = this.getProperty('size') || 'md';
@@ -929,11 +559,6 @@ export class TyDatePicker extends TyComponent {
929
559
  classes.push('open');
930
560
  return classes.join(' ');
931
561
  }
932
- /**
933
- * Render the date picker stub (input display)
934
- *
935
- * PORTED FROM: render-date-picker-stub in date_picker.cljs
936
- */
937
562
  renderStub() {
938
563
  const stub = document.createElement('div');
939
564
  stub.className = this.buildStubClasses();
@@ -941,11 +566,6 @@ export class TyDatePicker extends TyComponent {
941
566
  if (isDisabled) {
942
567
  stub.setAttribute('disabled', 'true');
943
568
  }
944
- // Not text-editable (click/Enter/Space just opens the calendar dialog),
945
- // so role="button" — not role="combobox", which implies typing. The
946
- // popup is a real <dialog> opened via showModal() (see openDropdown),
947
- // so it already gets role="dialog" + focus-trap from the browser for
948
- // free; only the trigger itself needs wiring.
949
569
  stub.setAttribute('role', 'button');
950
570
  stub.setAttribute('aria-haspopup', 'dialog');
951
571
  stub.setAttribute('aria-expanded', String(this._state.open));
@@ -955,9 +575,6 @@ export class TyDatePicker extends TyComponent {
955
575
  if (this.getProperty('label')) {
956
576
  stub.setAttribute('aria-labelledby', `date-picker-label-${getElementHash(this)}`);
957
577
  }
958
- // Same gap as ty-select had: a plain div's Enter/Space don't turn into
959
- // click events on their own, so without this a keyboard-only user could
960
- // Tab to the field but never open it.
961
578
  stub.addEventListener('keydown', (e) => {
962
579
  const ke = e;
963
580
  if (this._state.open)
@@ -967,10 +584,8 @@ export class TyDatePicker extends TyComponent {
967
584
  this.handleStubClick(e);
968
585
  }
969
586
  });
970
- // Start slot — leading icon (search, calendar variant, etc.)
971
587
  const startSlot = document.createElement('slot');
972
588
  startSlot.name = 'start';
973
- // Display text
974
589
  const displayText = document.createElement('span');
975
590
  displayText.className = 'stub-text';
976
591
  const formattedValue = this.getFormattedValue();
@@ -979,10 +594,8 @@ export class TyDatePicker extends TyComponent {
979
594
  if (!formattedValue) {
980
595
  displayText.classList.add('placeholder');
981
596
  }
982
- // Icons container
983
597
  const iconContainer = document.createElement('div');
984
598
  iconContainer.className = 'stub-icons';
985
- // Clear button
986
599
  const clearable = this.getProperty('clearable');
987
600
  if (clearable && formattedValue && !isDisabled) {
988
601
  const clearButton = document.createElement('button');
@@ -992,23 +605,16 @@ export class TyDatePicker extends TyComponent {
992
605
  clearButton.addEventListener('click', (e) => this.handleClearClick(e));
993
606
  iconContainer.appendChild(clearButton);
994
607
  }
995
- // Calendar icon
996
608
  const calendarIcon = document.createElement('span');
997
609
  calendarIcon.className = 'stub-arrow';
998
610
  calendarIcon.innerHTML = CALENDAR_ICON_SVG;
999
611
  iconContainer.appendChild(calendarIcon);
1000
- // Stub click handler
1001
612
  stub.addEventListener('click', (e) => this.handleStubClick(e));
1002
613
  stub.appendChild(startSlot);
1003
614
  stub.appendChild(displayText);
1004
615
  stub.appendChild(iconContainer);
1005
616
  return stub;
1006
617
  }
1007
- /**
1008
- * Create time input element
1009
- *
1010
- * PORTED FROM: create-time-input in date_picker.cljs
1011
- */
1012
618
  createTimeInputElement() {
1013
619
  const input = document.createElement('input');
1014
620
  input.type = 'text';
@@ -1018,9 +624,6 @@ export class TyDatePicker extends TyComponent {
1018
624
  input.maxLength = 5;
1019
625
  return input;
1020
626
  }
1021
- /**
1022
- * Render time input section
1023
- */
1024
627
  renderTimeSection() {
1025
628
  const timeSection = document.createElement('div');
1026
629
  timeSection.className = 'time-section';
@@ -1028,7 +631,6 @@ export class TyDatePicker extends TyComponent {
1028
631
  timeLabel.className = 'time-label';
1029
632
  timeLabel.textContent = 'Time:';
1030
633
  const timeInputElement = this.createTimeInputElement();
1031
- // Create TimeInput instance
1032
634
  const hour = this._state.hour || 0;
1033
635
  const minute = this._state.minute || 0;
1034
636
  this._timeInput = new TimeInput(timeInputElement, this, hour, minute);
@@ -1040,19 +642,12 @@ export class TyDatePicker extends TyComponent {
1040
642
  timeSection.appendChild(timeIcon);
1041
643
  return timeSection;
1042
644
  }
1043
- /**
1044
- * Render calendar dropdown with dialog
1045
- *
1046
- * PORTED FROM: render-calendar-dropdown in date_picker.cljs
1047
- */
1048
645
  renderCalendarDropdown() {
1049
646
  const dialog = document.createElement('dialog');
1050
647
  dialog.className = 'calendar-dialog';
1051
648
  const contentWrapper = document.createElement('div');
1052
649
  contentWrapper.className = 'calendar-content';
1053
- // Create ty-calendar
1054
650
  const calendar = document.createElement('ty-calendar');
1055
- // Set current date if available
1056
651
  if (this._state.year && this._state.month && this._state.day) {
1057
652
  calendar.setAttribute('year', this._state.year.toString());
1058
653
  calendar.setAttribute('month', this._state.month.toString());
@@ -1062,38 +657,26 @@ export class TyDatePicker extends TyComponent {
1062
657
  if (locale) {
1063
658
  calendar.setAttribute('locale', locale);
1064
659
  }
1065
- // Pass through date bounds
1066
660
  const min = this.getProperty('min');
1067
661
  const max = this.getProperty('max');
1068
662
  if (min)
1069
663
  calendar.setAttribute('min', min);
1070
664
  if (max)
1071
665
  calendar.setAttribute('max', max);
1072
- // Match the popup calendar to the stub's flavor. 'default' is the
1073
- // stub's own unstyled state, not a real flavor — leave the calendar on
1074
- // its own default rather than forwarding the literal string.
1075
666
  const flavor = this.getProperty('flavor');
1076
667
  if (flavor && flavor !== 'default')
1077
668
  calendar.setAttribute('flavor', flavor);
1078
- // Add calendar change handler
1079
669
  calendar.addEventListener('change', (e) => this.handleCalendarChange(e));
1080
670
  contentWrapper.appendChild(calendar);
1081
- // Add time section if with-time enabled
1082
671
  if (this._state.withTime) {
1083
672
  contentWrapper.appendChild(this.renderTimeSection());
1084
673
  }
1085
674
  dialog.appendChild(contentWrapper);
1086
- // Dialog close handler
1087
675
  dialog.addEventListener('close', () => {
1088
676
  this.updateState({ open: false });
1089
677
  });
1090
678
  return dialog;
1091
679
  }
1092
- /**
1093
- * Render native date input for mobile touch devices.
1094
- * Uses <input type="date"> or <input type="datetime-local"> when with-time.
1095
- * Reuses .date-picker-stub styling and existing event/state infrastructure.
1096
- */
1097
680
  renderNativeInput() {
1098
681
  const wrapper = document.createElement('div');
1099
682
  wrapper.className = 'dropdown-wrapper';
@@ -1103,7 +686,6 @@ export class TyDatePicker extends TyComponent {
1103
686
  if (isDisabled) {
1104
687
  stub.setAttribute('disabled', 'true');
1105
688
  }
1106
- // Native input (hidden, activated via picker indicator)
1107
689
  const input = document.createElement('input');
1108
690
  input.className = 'native-date-input';
1109
691
  input.type = this._state.withTime ? 'datetime-local' : 'date';
@@ -1111,25 +693,21 @@ export class TyDatePicker extends TyComponent {
1111
693
  input.disabled = true;
1112
694
  if (this.getProperty('required'))
1113
695
  input.required = true;
1114
- // Native min/max (datetime-local wants a time suffix)
1115
696
  const minBound = this.getProperty('min');
1116
697
  const maxBound = this.getProperty('max');
1117
698
  if (minBound)
1118
699
  input.min = this._state.withTime ? `${minBound}T00:00` : minBound;
1119
700
  if (maxBound)
1120
701
  input.max = this._state.withTime ? `${maxBound}T23:59` : maxBound;
1121
- // Pre-fill value
1122
702
  const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
1123
703
  if (localValue) {
1124
704
  input.value = localValue;
1125
705
  }
1126
- // Placeholder span (native date inputs ignore placeholder attr)
1127
706
  const placeholder = this.getProperty('placeholder') || 'Select date...';
1128
707
  const formattedValue = this.getFormattedValue();
1129
708
  const placeholderEl = document.createElement('span');
1130
709
  placeholderEl.className = 'stub-text' + (formattedValue ? '' : ' placeholder');
1131
710
  placeholderEl.textContent = formattedValue || placeholder;
1132
- // Change listener
1133
711
  input.addEventListener('change', () => {
1134
712
  if (!input.value) {
1135
713
  this.clearValue();
@@ -1141,13 +719,11 @@ export class TyDatePicker extends TyComponent {
1141
719
  this.emitChangeEvent(parsed, 'selection');
1142
720
  }
1143
721
  });
1144
- // Start slot — leading icon
1145
722
  const startSlot = document.createElement('slot');
1146
723
  startSlot.name = 'start';
1147
724
  stub.appendChild(startSlot);
1148
725
  stub.appendChild(input);
1149
726
  stub.appendChild(placeholderEl);
1150
- // Icons container (clear + calendar icon)
1151
727
  const iconContainer = document.createElement('div');
1152
728
  iconContainer.className = 'stub-icons';
1153
729
  const clearable = this.getProperty('clearable');
@@ -1167,15 +743,9 @@ export class TyDatePicker extends TyComponent {
1167
743
  wrapper.appendChild(stub);
1168
744
  return wrapper;
1169
745
  }
1170
- /**
1171
- * Calculate calendar position
1172
- *
1173
- * PORTED FROM: calculate-calendar-position! in date_picker.cljs
1174
- */
1175
746
  calculateCalendarPosition() {
1176
747
  if (!this.shadowRoot)
1177
748
  return;
1178
- // Mobile uses native input, no positioning needed
1179
749
  if (isMobileTouch())
1180
750
  return;
1181
751
  const stub = this.shadowRoot.querySelector('.date-picker-stub');
@@ -1183,26 +753,26 @@ export class TyDatePicker extends TyComponent {
1183
753
  if (!stub || !dialog)
1184
754
  return;
1185
755
  const stubRect = stub.getBoundingClientRect();
1186
- // Measure when shown; the estimate only covers the pre-layout call
1187
756
  const dialogRect = dialog.getBoundingClientRect();
1188
757
  const estimatedHeight = dialogRect.height || 400;
1189
758
  const popupWidth = dialogRect.width || 320;
1190
759
  const wrapPadding = 8;
760
+ const anchored = placementToAnchored(this.getProperty('placement'));
1191
761
  const pos = computeAnchoredPosition({
1192
762
  anchorRect: stubRect,
1193
763
  popupWidth,
1194
764
  popupHeight: estimatedHeight,
1195
765
  gap: 0,
766
+ align: anchored.align,
767
+ side: anchored.side,
1196
768
  });
1197
769
  const positionBelow = pos.below;
1198
770
  const x = pos.x - wrapPadding;
1199
771
  const y = positionBelow ? pos.topY : pos.bottomY;
1200
- // Set CSS variables for positioning
1201
772
  this.style.setProperty('--calendar-x', `${x}px`);
1202
773
  this.style.setProperty('--calendar-y', `${y}px`);
1203
774
  this.style.setProperty('--calendar-offset-x', '0px');
1204
775
  this.style.setProperty('--calendar-offset-y', '0px');
1205
- // Set direction classes
1206
776
  if (positionBelow) {
1207
777
  dialog.classList.add('position-below');
1208
778
  dialog.classList.remove('position-above');
@@ -1212,11 +782,6 @@ export class TyDatePicker extends TyComponent {
1212
782
  dialog.classList.remove('position-below');
1213
783
  }
1214
784
  }
1215
- /**
1216
- * Render container structure
1217
- *
1218
- * PORTED FROM: render-container-structure in date_picker.cljs
1219
- */
1220
785
  renderContainer() {
1221
786
  const container = document.createElement('div');
1222
787
  container.className = 'dropdown-container';
@@ -1230,40 +795,29 @@ export class TyDatePicker extends TyComponent {
1230
795
  }
1231
796
  return container;
1232
797
  }
1233
- /**
1234
- * Main render method
1235
- *
1236
- * PORTED FROM: render! in date_picker.cljs
1237
- */
1238
798
  render() {
1239
799
  if (!this.shadowRoot)
1240
800
  return;
1241
801
  ensureStyles(this.shadowRoot, { css: datePickerStyles, id: 'ty-date-picker' });
1242
- // Mobile: native input, no dialog/event listeners/scroll lock
1243
802
  if (isMobileTouch()) {
1244
803
  const existingNativeInput = this.shadowRoot.querySelector('.native-date-input');
1245
804
  if (existingNativeInput) {
1246
- // PARTIAL UPDATE: don't destroy DOM while native picker may be open
1247
805
  this.updateDisplay();
1248
806
  return;
1249
807
  }
1250
- // FIRST RENDER: build from scratch
1251
808
  this.shadowRoot.innerHTML = '';
1252
809
  const container = this.renderContainer();
1253
810
  container.appendChild(this.renderNativeInput());
1254
811
  this.shadowRoot.appendChild(container);
1255
812
  return;
1256
813
  }
1257
- // Check if dialog is currently open
1258
814
  const existingDialog = this.shadowRoot.querySelector('.calendar-dialog');
1259
815
  const isDialogOpen = existingDialog && existingDialog.open;
1260
816
  if (isDialogOpen && this._state.open) {
1261
- // PARTIAL UPDATE: Dialog is open - just update display
1262
817
  this.updateDisplay();
1263
818
  this.calculateCalendarPosition();
1264
819
  return;
1265
820
  }
1266
- // FULL REBUILD: Dialog is closed or doesn't exist
1267
821
  this.shadowRoot.innerHTML = '';
1268
822
  const container = this.renderContainer();
1269
823
  const wrapper = document.createElement('div');
@@ -1272,14 +826,8 @@ export class TyDatePicker extends TyComponent {
1272
826
  wrapper.appendChild(this.renderCalendarDropdown());
1273
827
  container.appendChild(wrapper);
1274
828
  this.shadowRoot.appendChild(container);
1275
- // Setup document-level event listeners
1276
829
  this.setupEventListeners();
1277
830
  }
1278
- /**
1279
- * Update display without destroying DOM (for open dialog)
1280
- *
1281
- * PORTED FROM: update-display! in date_picker.cljs
1282
- */
1283
831
  updateDisplay() {
1284
832
  if (!this.shadowRoot)
1285
833
  return;
@@ -1288,18 +836,15 @@ export class TyDatePicker extends TyComponent {
1288
836
  const hasValue = this.hasDate();
1289
837
  const clearable = this.getProperty('clearable');
1290
838
  const isDisabled = this.getProperty('disabled');
1291
- // Update stub text (shared between mobile and desktop)
1292
839
  const stubText = this.shadowRoot.querySelector('.stub-text');
1293
840
  if (stubText) {
1294
841
  stubText.textContent = formattedValue || placeholder;
1295
842
  stubText.classList.toggle('placeholder', !formattedValue);
1296
843
  }
1297
- // Update clear button visibility
1298
844
  const clearButton = this.shadowRoot.querySelector('.stub-clear');
1299
845
  if (clearButton) {
1300
846
  clearButton.style.display = (clearable && hasValue && !isDisabled) ? '' : 'none';
1301
847
  }
1302
- // Mobile: also sync native input value
1303
848
  if (isMobileTouch()) {
1304
849
  const nativeInput = this.shadowRoot.querySelector('.native-date-input');
1305
850
  if (nativeInput) {
@@ -1308,17 +853,12 @@ export class TyDatePicker extends TyComponent {
1308
853
  }
1309
854
  return;
1310
855
  }
1311
- // Desktop: update calendar and time input
1312
856
  const calendar = this.shadowRoot.querySelector('ty-calendar');
1313
857
  if (calendar && hasValue) {
1314
858
  calendar.setAttribute('year', this._state.year.toString());
1315
859
  calendar.setAttribute('month', this._state.month.toString());
1316
860
  calendar.setAttribute('day', this._state.day.toString());
1317
861
  }
1318
- // Keep an already-open popup's flavor in sync. render() only rebuilds
1319
- // (and re-forwards flavor) while the dialog is CLOSED — while open it
1320
- // takes this partial-update path instead, so without this the calendar
1321
- // would stay stuck on whatever flavor was active when it was opened.
1322
862
  if (calendar) {
1323
863
  const flavor = this.getProperty('flavor');
1324
864
  if (flavor && flavor !== 'default')
@@ -1330,55 +870,35 @@ export class TyDatePicker extends TyComponent {
1330
870
  this._timeInput.setTime(this._state.hour, this._state.minute);
1331
871
  }
1332
872
  }
1333
- /**
1334
- * Setup document-level event listeners
1335
- */
1336
873
  setupEventListeners() {
1337
- // Remove old listeners if they exist
1338
874
  if (this._clickListener) {
1339
875
  document.removeEventListener('click', this._clickListener);
1340
876
  }
1341
877
  if (this._keydownListener) {
1342
878
  document.removeEventListener('keydown', this._keydownListener);
1343
879
  }
1344
- // Create new listeners
1345
880
  this._clickListener = (e) => this.handleOutsideClick(e);
1346
881
  this._keydownListener = (e) => this.handleEscapeKey(e);
1347
- // Add document listeners
1348
882
  document.addEventListener('click', this._clickListener);
1349
883
  document.addEventListener('keydown', this._keydownListener);
1350
- // Add dialog click listener
1351
884
  const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
1352
885
  if (dialog) {
1353
886
  this._dialogClickListener = (e) => this.handleDialogClick(e);
1354
887
  dialog.addEventListener('click', this._dialogClickListener);
1355
888
  }
1356
889
  }
1357
- // ==========================================================================
1358
- // Event Handlers
1359
- // ==========================================================================
1360
890
  handleStubClick(event) {
1361
891
  event.preventDefault();
1362
- // Heal state/DOM desync: a host re-render can replace the dialog element
1363
- // while `open` is still true, stranding the picker (openDropdown
1364
- // early-returns on _state.open). Reality wins: closed dialog → closed state.
1365
892
  const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
1366
893
  if (this._state.open && (!dialog || !dialog.open)) {
1367
894
  this.updateState({ open: false }, true);
1368
895
  unlockScroll(`date-picker-${this.id || this.toString()}`);
1369
896
  }
1370
897
  const disabled = this.getProperty('disabled');
1371
- // FIXME: Timestamp guard is a workaround. Calendar day cells use pointerdown,
1372
- // which closes the dialog before the click event fires — the click then hits the
1373
- // stub and reopens. Proper fix: switch calendar-month day cells from pointerdown
1374
- // to click so the event is consumed before the dialog closes.
1375
898
  if (!disabled && Date.now() - this._closeTimestamp > 300) {
1376
899
  this.openDropdown();
1377
900
  }
1378
901
  }
1379
- /**
1380
- * Clear the date value, sync state, emit event, and re-render.
1381
- */
1382
902
  clearValue() {
1383
903
  this.updateState({
1384
904
  year: undefined,
@@ -1396,11 +916,6 @@ export class TyDatePicker extends TyComponent {
1396
916
  event.stopPropagation();
1397
917
  this.clearValue();
1398
918
  }
1399
- /**
1400
- * Handle calendar date selection
1401
- *
1402
- * PORTED FROM: handle-calendar-change! in date_picker.cljs
1403
- */
1404
919
  handleCalendarChange(event) {
1405
920
  event.preventDefault();
1406
921
  event.stopPropagation();
@@ -1409,7 +924,6 @@ export class TyDatePicker extends TyComponent {
1409
924
  const dayContext = detail.dayContext;
1410
925
  if (!dayContext)
1411
926
  return;
1412
- // Update state with new date
1413
927
  const newComponents = {
1414
928
  year: dayContext.year,
1415
929
  month: dayContext.month,
@@ -1417,15 +931,12 @@ export class TyDatePicker extends TyComponent {
1417
931
  hour: this._state.hour,
1418
932
  minute: this._state.minute,
1419
933
  };
1420
- // Force sync when calendar date changes
1421
934
  this.updateState(newComponents, true);
1422
935
  this.emitChangeEvent(newComponents, 'selection');
1423
- // Close calendar if time is not required
1424
936
  if (!this._state.withTime) {
1425
937
  this.closeDropdown();
1426
938
  }
1427
939
  else {
1428
- // Auto-focus time input after date selection
1429
940
  requestAnimationFrame(() => {
1430
941
  if (!this.shadowRoot)
1431
942
  return;
@@ -1436,43 +947,26 @@ export class TyDatePicker extends TyComponent {
1436
947
  });
1437
948
  }
1438
949
  }
1439
- /**
1440
- * Handle dialog backdrop clicks
1441
- *
1442
- * PORTED FROM: handle-dialog-click! in date_picker.cljs
1443
- */
1444
950
  handleDialogClick(event) {
1445
951
  if (!this.shadowRoot)
1446
952
  return;
1447
953
  const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1448
954
  const content = this.shadowRoot.querySelector('.calendar-content');
1449
- // Close if clicking on dialog backdrop (not calendar content)
1450
955
  if (event.target === dialog && this._state.open && content && !content.contains(event.target)) {
1451
956
  event.preventDefault();
1452
957
  event.stopPropagation();
1453
958
  this.closeDropdown();
1454
959
  }
1455
960
  }
1456
- /**
1457
- * Handle clicks outside the date picker
1458
- *
1459
- * PORTED FROM: handle-outside-click! in date_picker.cljs
1460
- */
1461
961
  handleOutsideClick(event) {
1462
962
  if (!this.shadowRoot)
1463
963
  return;
1464
964
  const target = event.target;
1465
965
  const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1466
- // Check if click is truly outside everything
1467
966
  if (this._state.open && dialog && !this.contains(target) && !dialog.contains(target)) {
1468
967
  this.closeDropdown();
1469
968
  }
1470
969
  }
1471
- /**
1472
- * Handle Escape key press
1473
- *
1474
- * PORTED FROM: handle-escape-key! in date_picker.cljs
1475
- */
1476
970
  handleEscapeKey(event) {
1477
971
  const keyboardEvent = event;
1478
972
  if (keyboardEvent.key === 'Escape' && this._state.open) {
@@ -1480,22 +974,13 @@ export class TyDatePicker extends TyComponent {
1480
974
  this.closeDropdown();
1481
975
  }
1482
976
  }
1483
- /**
1484
- * Open calendar dropdown
1485
- *
1486
- * PORTED FROM: open-dropdown! in date_picker.cljs
1487
- */
1488
977
  openDropdown() {
1489
978
  if (this._state.open)
1490
979
  return;
1491
980
  if (isMobileTouch())
1492
981
  return;
1493
982
  this.updateState({ open: true });
1494
- // Dispatch open event
1495
- this.dispatchEvent(new CustomEvent('open', {
1496
- bubbles: true,
1497
- composed: true
1498
- }));
983
+ this.dispatchEvent(new CustomEvent('open'));
1499
984
  this.render();
1500
985
  requestAnimationFrame(() => {
1501
986
  if (!this.shadowRoot)
@@ -1510,7 +995,6 @@ export class TyDatePicker extends TyComponent {
1510
995
  const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
1511
996
  if (stubEl)
1512
997
  stubEl.setAttribute('aria-expanded', 'true');
1513
- // Remove focus from any focused elements to prevent the blue outline
1514
998
  const focusedElement = this.shadowRoot.activeElement;
1515
999
  if (focusedElement) {
1516
1000
  focusedElement.blur();
@@ -1518,17 +1002,11 @@ export class TyDatePicker extends TyComponent {
1518
1002
  }
1519
1003
  });
1520
1004
  }
1521
- /**
1522
- * Close calendar dropdown
1523
- *
1524
- * PORTED FROM: close-dropdown! in date_picker.cljs
1525
- */
1526
1005
  closeDropdown() {
1527
1006
  if (!this._state.open)
1528
1007
  return;
1529
1008
  this._closeTimestamp = Date.now();
1530
1009
  const pickerId = `date-picker-${this.id || this.toString()}`;
1531
- // Force sync any staged updates when closing
1532
1010
  this.updateState({ open: false }, true);
1533
1011
  unlockScroll(pickerId);
1534
1012
  if (this.shadowRoot) {
@@ -1537,63 +1015,46 @@ export class TyDatePicker extends TyComponent {
1537
1015
  dialog.classList.remove('position-above', 'position-below', 'open');
1538
1016
  dialog.close();
1539
1017
  }
1540
- // closeDropdown() doesn't call render(), so the stub's aria-expanded
1541
- // (still "true" from open) needs an explicit flip here too.
1542
1018
  const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
1543
1019
  if (stubEl)
1544
1020
  stubEl.setAttribute('aria-expanded', 'false');
1545
1021
  }
1546
- this.dispatchEvent(new CustomEvent('close', {
1547
- bubbles: true,
1548
- composed: true
1549
- }));
1022
+ this.dispatchEvent(new CustomEvent('close'));
1550
1023
  this.render();
1551
1024
  }
1552
- // ==========================================================================
1553
- // Cleanup
1554
- // ==========================================================================
1555
- /**
1556
- * Clean up event listeners and state
1557
- *
1558
- * PORTED FROM: cleanup! in date_picker.cljs
1559
- */
1560
1025
  cleanup() {
1561
- // Remove document listeners
1562
1026
  if (this._clickListener) {
1563
1027
  document.removeEventListener('click', this._clickListener);
1564
1028
  }
1565
1029
  if (this._keydownListener) {
1566
1030
  document.removeEventListener('keydown', this._keydownListener);
1567
1031
  }
1568
- // Remove dialog listener
1569
1032
  if (this._dialogClickListener && this.shadowRoot) {
1570
1033
  const dialog = this.shadowRoot.querySelector('.calendar-dialog');
1571
1034
  if (dialog) {
1572
1035
  dialog.removeEventListener('click', this._dialogClickListener);
1573
1036
  }
1574
1037
  }
1575
- // Unlock scroll if open
1576
1038
  if (this._state.open) {
1577
1039
  const pickerId = `date-picker-${this.id || this.toString()}`;
1578
1040
  unlockScroll(pickerId);
1579
1041
  }
1580
- // Clear references
1581
1042
  this._clickListener = undefined;
1582
1043
  this._keydownListener = undefined;
1583
1044
  this._dialogClickListener = undefined;
1584
1045
  this._timeInput = undefined;
1585
1046
  }
1586
1047
  }
1587
- // ============================================================================
1588
- // PROPERTY CONFIGURATION - Single source of truth
1589
- // ============================================================================
1590
1048
  TyDatePicker.properties = {
1591
- // String properties
1049
+ placement: {
1050
+ type: 'string',
1051
+ visual: true,
1052
+ default: '',
1053
+ },
1592
1054
  size: {
1593
1055
  type: 'string',
1594
1056
  visual: true,
1595
1057
  default: 'md',
1596
- // Fields come in exactly three sizes; legacy xs/xl map to sm/lg.
1597
1058
  validate: (v) => ['sm', 'md', 'lg'].includes(v),
1598
1059
  coerce: (v) => {
1599
1060
  if (v === 'xs')
@@ -1624,10 +1085,9 @@ TyDatePicker.properties = {
1624
1085
  },
1625
1086
  name: {
1626
1087
  type: 'string',
1627
- visual: false, // Non-visual, just for form field name
1088
+ visual: false,
1628
1089
  default: ''
1629
1090
  },
1630
- // Date bounds (ISO) — passed through to the embedded ty-calendar
1631
1091
  min: {
1632
1092
  type: 'string',
1633
1093
  visual: true,
@@ -1638,7 +1098,6 @@ TyDatePicker.properties = {
1638
1098
  visual: true,
1639
1099
  default: ''
1640
1100
  },
1641
- // Boolean properties
1642
1101
  required: {
1643
1102
  type: 'boolean',
1644
1103
  visual: true,
@@ -1657,7 +1116,6 @@ TyDatePicker.properties = {
1657
1116
  'not-clearable': false
1658
1117
  }
1659
1118
  },
1660
- // Format property (medium complexity)
1661
1119
  format: {
1662
1120
  type: 'string',
1663
1121
  visual: true,
@@ -1672,34 +1130,25 @@ TyDatePicker.properties = {
1672
1130
  return v;
1673
1131
  }
1674
1132
  },
1675
- // Locale property (medium complexity - has observer)
1676
1133
  locale: {
1677
1134
  type: 'string',
1678
1135
  visual: true,
1679
1136
  default: ''
1680
1137
  },
1681
- // Value property (high complexity - UTC parsing, date components)
1682
- // Note: Custom getter/setter will handle the complex logic
1683
1138
  value: {
1684
1139
  type: 'string',
1685
1140
  visual: true,
1686
- formValue: true, // Syncs to form
1687
- emitChange: false, // We emit custom 'change' events manually with full detail
1688
- default: null // null when no date selected
1141
+ formValue: true,
1142
+ emitChange: false,
1143
+ default: null
1689
1144
  },
1690
- // With-time property (high complexity - affects parsing and rendering)
1691
1145
  'with-time': {
1692
1146
  type: 'boolean',
1693
1147
  visual: true,
1694
1148
  default: false
1695
1149
  }
1696
1150
  };
1697
- /**
1698
- * Form-associated custom element
1699
- */
1700
1151
  TyDatePicker.formAssociated = true;
1701
- // Register custom element
1702
1152
  if (!customElements.get('ty-date-picker')) {
1703
1153
  customElements.define('ty-date-picker', TyDatePicker);
1704
1154
  }
1705
- //# sourceMappingURL=date-picker.js.map