tyrell-components 1.0.0-TC7

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