@keenthemes/ktui 1.0.28 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/README.md +27 -0
  2. package/dist/ktui.js +8780 -6199
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +2744 -1367
  6. package/lib/cjs/components/alert/alert.js +1025 -0
  7. package/lib/cjs/components/alert/alert.js.map +1 -0
  8. package/lib/cjs/components/alert/index.js +20 -0
  9. package/lib/cjs/components/alert/index.js.map +1 -0
  10. package/lib/cjs/components/alert/templates.js +120 -0
  11. package/lib/cjs/components/alert/templates.js.map +1 -0
  12. package/lib/cjs/components/alert/types.js +7 -0
  13. package/lib/cjs/components/alert/types.js.map +1 -0
  14. package/lib/cjs/components/datepicker/config/config.js +42 -0
  15. package/lib/cjs/components/datepicker/config/config.js.map +1 -0
  16. package/lib/cjs/components/datepicker/config/index.js +24 -0
  17. package/lib/cjs/components/datepicker/config/index.js.map +1 -0
  18. package/lib/cjs/components/datepicker/config/interfaces.js +7 -0
  19. package/lib/cjs/components/datepicker/config/interfaces.js.map +1 -0
  20. package/lib/cjs/components/datepicker/config/types.js +7 -0
  21. package/lib/cjs/components/datepicker/config/types.js.map +1 -0
  22. package/lib/cjs/components/datepicker/core/event-manager.js +135 -0
  23. package/lib/cjs/components/datepicker/core/event-manager.js.map +1 -0
  24. package/lib/cjs/components/datepicker/core/focus-manager.js +167 -0
  25. package/lib/cjs/components/datepicker/core/focus-manager.js.map +1 -0
  26. package/lib/cjs/components/datepicker/core/helpers.js +219 -0
  27. package/lib/cjs/components/datepicker/core/helpers.js.map +1 -0
  28. package/lib/cjs/components/datepicker/core/index.js +25 -0
  29. package/lib/cjs/components/datepicker/core/index.js.map +1 -0
  30. package/lib/cjs/components/datepicker/core/unified-state-manager.js +394 -0
  31. package/lib/cjs/components/datepicker/core/unified-state-manager.js.map +1 -0
  32. package/lib/cjs/components/datepicker/datepicker.js +2066 -763
  33. package/lib/cjs/components/datepicker/datepicker.js.map +1 -1
  34. package/lib/cjs/components/datepicker/index.js +19 -8
  35. package/lib/cjs/components/datepicker/index.js.map +1 -1
  36. package/lib/cjs/components/datepicker/ui/index.js +23 -0
  37. package/lib/cjs/components/datepicker/ui/index.js.map +1 -0
  38. package/lib/cjs/components/datepicker/ui/input/dropdown.js +489 -0
  39. package/lib/cjs/components/datepicker/ui/input/dropdown.js.map +1 -0
  40. package/lib/cjs/components/datepicker/ui/input/index.js +23 -0
  41. package/lib/cjs/components/datepicker/ui/input/index.js.map +1 -0
  42. package/lib/cjs/components/datepicker/ui/input/segmented-input.js +640 -0
  43. package/lib/cjs/components/datepicker/ui/input/segmented-input.js.map +1 -0
  44. package/lib/cjs/components/datepicker/ui/renderers/calendar.js +446 -0
  45. package/lib/cjs/components/datepicker/ui/renderers/calendar.js.map +1 -0
  46. package/lib/cjs/components/datepicker/ui/renderers/footer.js +42 -0
  47. package/lib/cjs/components/datepicker/ui/renderers/footer.js.map +1 -0
  48. package/lib/cjs/components/datepicker/ui/renderers/header.js +32 -0
  49. package/lib/cjs/components/datepicker/ui/renderers/header.js.map +1 -0
  50. package/lib/cjs/components/datepicker/ui/renderers/index.js +25 -0
  51. package/lib/cjs/components/datepicker/ui/renderers/index.js.map +1 -0
  52. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js +384 -0
  53. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js.map +1 -0
  54. package/lib/cjs/components/datepicker/ui/templates/index.js +22 -0
  55. package/lib/cjs/components/datepicker/ui/templates/index.js.map +1 -0
  56. package/lib/cjs/components/datepicker/ui/templates/templates.js +253 -0
  57. package/lib/cjs/components/datepicker/ui/templates/templates.js.map +1 -0
  58. package/lib/cjs/components/datepicker/utils/date-formatters.js +88 -0
  59. package/lib/cjs/components/datepicker/utils/date-formatters.js.map +1 -0
  60. package/lib/cjs/components/datepicker/utils/date-utils.js +194 -0
  61. package/lib/cjs/components/datepicker/utils/date-utils.js.map +1 -0
  62. package/lib/cjs/components/datepicker/utils/index.js +24 -0
  63. package/lib/cjs/components/datepicker/utils/index.js.map +1 -0
  64. package/lib/cjs/components/datepicker/utils/time-utils.js +213 -0
  65. package/lib/cjs/components/datepicker/utils/time-utils.js.map +1 -0
  66. package/lib/cjs/index.js +6 -1
  67. package/lib/cjs/index.js.map +1 -1
  68. package/lib/esm/components/alert/alert.js +1022 -0
  69. package/lib/esm/components/alert/alert.js.map +1 -0
  70. package/lib/esm/components/alert/index.js +4 -0
  71. package/lib/esm/components/alert/index.js.map +1 -0
  72. package/lib/esm/components/alert/templates.js +112 -0
  73. package/lib/esm/components/alert/templates.js.map +1 -0
  74. package/lib/esm/components/alert/types.js +6 -0
  75. package/lib/esm/components/alert/types.js.map +1 -0
  76. package/lib/esm/components/datepicker/config/config.js +39 -0
  77. package/lib/esm/components/datepicker/config/config.js.map +1 -0
  78. package/lib/esm/components/datepicker/config/index.js +8 -0
  79. package/lib/esm/components/datepicker/config/index.js.map +1 -0
  80. package/lib/esm/components/datepicker/config/interfaces.js +6 -0
  81. package/lib/esm/components/datepicker/config/interfaces.js.map +1 -0
  82. package/lib/esm/components/datepicker/config/types.js +6 -0
  83. package/lib/esm/components/datepicker/config/types.js.map +1 -0
  84. package/lib/esm/components/datepicker/core/event-manager.js +133 -0
  85. package/lib/esm/components/datepicker/core/event-manager.js.map +1 -0
  86. package/lib/esm/components/datepicker/core/focus-manager.js +164 -0
  87. package/lib/esm/components/datepicker/core/focus-manager.js.map +1 -0
  88. package/lib/esm/components/datepicker/core/helpers.js +211 -0
  89. package/lib/esm/components/datepicker/core/helpers.js.map +1 -0
  90. package/lib/esm/components/datepicker/core/index.js +9 -0
  91. package/lib/esm/components/datepicker/core/index.js.map +1 -0
  92. package/lib/esm/components/datepicker/core/unified-state-manager.js +391 -0
  93. package/lib/esm/components/datepicker/core/unified-state-manager.js.map +1 -0
  94. package/lib/esm/components/datepicker/datepicker.js +2065 -763
  95. package/lib/esm/components/datepicker/datepicker.js.map +1 -1
  96. package/lib/esm/components/datepicker/index.js +6 -8
  97. package/lib/esm/components/datepicker/index.js.map +1 -1
  98. package/lib/esm/components/datepicker/ui/index.js +7 -0
  99. package/lib/esm/components/datepicker/ui/index.js.map +1 -0
  100. package/lib/esm/components/datepicker/ui/input/dropdown.js +486 -0
  101. package/lib/esm/components/datepicker/ui/input/dropdown.js.map +1 -0
  102. package/lib/esm/components/datepicker/ui/input/index.js +7 -0
  103. package/lib/esm/components/datepicker/ui/input/index.js.map +1 -0
  104. package/lib/esm/components/datepicker/ui/input/segmented-input.js +637 -0
  105. package/lib/esm/components/datepicker/ui/input/segmented-input.js.map +1 -0
  106. package/lib/esm/components/datepicker/ui/renderers/calendar.js +443 -0
  107. package/lib/esm/components/datepicker/ui/renderers/calendar.js.map +1 -0
  108. package/lib/esm/components/datepicker/ui/renderers/footer.js +39 -0
  109. package/lib/esm/components/datepicker/ui/renderers/footer.js.map +1 -0
  110. package/lib/esm/components/datepicker/ui/renderers/header.js +29 -0
  111. package/lib/esm/components/datepicker/ui/renderers/header.js.map +1 -0
  112. package/lib/esm/components/datepicker/ui/renderers/index.js +9 -0
  113. package/lib/esm/components/datepicker/ui/renderers/index.js.map +1 -0
  114. package/lib/esm/components/datepicker/ui/renderers/time-picker.js +381 -0
  115. package/lib/esm/components/datepicker/ui/renderers/time-picker.js.map +1 -0
  116. package/lib/esm/components/datepicker/ui/templates/index.js +6 -0
  117. package/lib/esm/components/datepicker/ui/templates/index.js.map +1 -0
  118. package/lib/esm/components/datepicker/ui/templates/templates.js +242 -0
  119. package/lib/esm/components/datepicker/ui/templates/templates.js.map +1 -0
  120. package/lib/esm/components/datepicker/utils/date-formatters.js +83 -0
  121. package/lib/esm/components/datepicker/utils/date-formatters.js.map +1 -0
  122. package/lib/esm/components/datepicker/utils/date-utils.js +184 -0
  123. package/lib/esm/components/datepicker/utils/date-utils.js.map +1 -0
  124. package/lib/esm/components/datepicker/utils/index.js +8 -0
  125. package/lib/esm/components/datepicker/utils/index.js.map +1 -0
  126. package/lib/esm/components/datepicker/utils/time-utils.js +201 -0
  127. package/lib/esm/components/datepicker/utils/time-utils.js.map +1 -0
  128. package/lib/esm/index.js +4 -0
  129. package/lib/esm/index.js.map +1 -1
  130. package/package.json +22 -3
  131. package/src/components/alert/alert.css +429 -188
  132. package/src/components/alert/alert.ts +990 -0
  133. package/src/components/alert/index.ts +4 -0
  134. package/src/components/alert/templates.ts +110 -0
  135. package/src/components/alert/tests/accessibility/aria-roles.test.ts +19 -0
  136. package/src/components/alert/tests/accessibility/focus-management.test.ts +19 -0
  137. package/src/components/alert/tests/accessibility/keyboard-nav.test.ts +22 -0
  138. package/src/components/alert/tests/actions/confirm-cancel.test.ts +122 -0
  139. package/src/components/alert/tests/actions/input-field.test.ts +180 -0
  140. package/src/components/alert/tests/alert.basic.test.ts +126 -0
  141. package/src/components/alert/tests/alert.config.test.ts +75 -0
  142. package/src/components/alert/tests/alert.templates.test.ts +17 -0
  143. package/src/components/alert/tests/config/attribute-config.test.ts +94 -0
  144. package/src/components/alert/tests/config/json-config.test.ts +119 -0
  145. package/src/components/alert/tests/config/merging.test.ts +89 -0
  146. package/src/components/alert/tests/dismissal/auto-dismiss.test.ts +96 -0
  147. package/src/components/alert/tests/dismissal/escape-key-dismiss.test.ts +105 -0
  148. package/src/components/alert/tests/dismissal/manual-dismiss.test.ts +90 -0
  149. package/src/components/alert/tests/dismissal/outside-click-dismiss.test.ts +91 -0
  150. package/src/components/alert/tests/edge-cases/invalid-config.test.ts +19 -0
  151. package/src/components/alert/tests/edge-cases/multiple-alerts.test.ts +19 -0
  152. package/src/components/alert/tests/rendering/custom-content.test.ts +81 -0
  153. package/src/components/alert/tests/rendering/info-alert.test.ts +84 -0
  154. package/src/components/alert/tests/rendering/success-alert.test.ts +100 -0
  155. package/src/components/alert/tests/templates/default-templates.test.ts +16 -0
  156. package/src/components/alert/tests/templates/user-templates.test.ts +16 -0
  157. package/src/components/alert/types.ts +145 -0
  158. package/src/components/datepicker/__tests__/datepicker-events.test.ts +356 -0
  159. package/src/components/datepicker/__tests__/datepicker-init.test.ts +343 -0
  160. package/src/components/datepicker/__tests__/datepicker-integration.test.ts +435 -0
  161. package/src/components/datepicker/__tests__/datepicker-timezone.test.ts +220 -0
  162. package/src/components/datepicker/__tests__/segmented-input-focus.test.ts +380 -0
  163. package/src/components/datepicker/__tests__/selective-state-updates.test.ts +400 -0
  164. package/src/components/datepicker/__tests__/state-manager.test.ts +421 -0
  165. package/src/components/datepicker/__tests__/time-preservation.test.ts +387 -0
  166. package/src/components/datepicker/config/config.ts +40 -0
  167. package/src/components/datepicker/config/index.ts +8 -0
  168. package/src/components/datepicker/config/interfaces.ts +82 -0
  169. package/src/components/datepicker/config/types.ts +188 -0
  170. package/src/components/datepicker/core/event-manager.ts +159 -0
  171. package/src/components/datepicker/core/focus-manager.ts +201 -0
  172. package/src/components/datepicker/core/helpers.ts +231 -0
  173. package/src/components/datepicker/core/index.ts +9 -0
  174. package/src/components/datepicker/core/unified-state-manager.ts +459 -0
  175. package/src/components/datepicker/datepicker.css +429 -1
  176. package/src/components/datepicker/datepicker.ts +2538 -1277
  177. package/src/components/datepicker/index.ts +6 -8
  178. package/src/components/datepicker/ui/index.ts +7 -0
  179. package/src/components/datepicker/ui/input/dropdown.ts +552 -0
  180. package/src/components/datepicker/ui/input/index.ts +7 -0
  181. package/src/components/datepicker/ui/input/segmented-input.ts +638 -0
  182. package/src/components/datepicker/ui/renderers/__tests__/calendar-optimizations.test.ts +611 -0
  183. package/src/components/datepicker/ui/renderers/calendar.ts +530 -0
  184. package/src/components/datepicker/ui/renderers/footer.ts +43 -0
  185. package/src/components/datepicker/ui/renderers/header.ts +33 -0
  186. package/src/components/datepicker/ui/renderers/index.ts +9 -0
  187. package/src/components/datepicker/ui/renderers/time-picker.ts +438 -0
  188. package/src/components/datepicker/ui/templates/index.ts +6 -0
  189. package/src/components/datepicker/ui/templates/templates.ts +306 -0
  190. package/src/components/datepicker/utils/__tests__/date-formatters.test.ts +160 -0
  191. package/src/components/datepicker/utils/__tests__/date-utils-keys.test.ts +86 -0
  192. package/src/components/datepicker/utils/__tests__/date-utils-timezone.test.ts +215 -0
  193. package/src/components/datepicker/utils/date-formatters.ts +85 -0
  194. package/src/components/datepicker/utils/date-utils.ts +172 -0
  195. package/src/components/datepicker/utils/index.ts +8 -0
  196. package/src/components/datepicker/utils/time-utils.ts +221 -0
  197. package/src/index.ts +7 -1
  198. package/CONTRIBUTING.md +0 -101
  199. package/examples/datatable/checkbox-events-test.html +0 -400
  200. package/examples/datatable/credentials-test.html +0 -423
  201. package/examples/datatable/remote-checkbox-test.html +0 -365
  202. package/examples/datatable/sorting-test.html +0 -258
  203. package/examples/image-input/file-upload-example.html +0 -189
  204. package/examples/modal/persistent.html +0 -205
  205. package/examples/modal/remote-select-dropdown.html +0 -166
  206. package/examples/modal/select-dropdown-container.html +0 -129
  207. package/examples/select/avatar.html +0 -47
  208. package/examples/select/basic-usage.html +0 -39
  209. package/examples/select/country.html +0 -43
  210. package/examples/select/dark-mode.html +0 -93
  211. package/examples/select/description.html +0 -53
  212. package/examples/select/disable-option.html +0 -37
  213. package/examples/select/disable-select.html +0 -35
  214. package/examples/select/dropdowncontainer.html +0 -111
  215. package/examples/select/dynamic-methods.html +0 -273
  216. package/examples/select/formdata-remote.html +0 -161
  217. package/examples/select/global-config.html +0 -81
  218. package/examples/select/icon-multiple.html +0 -50
  219. package/examples/select/icon.html +0 -48
  220. package/examples/select/max-selection.html +0 -38
  221. package/examples/select/modal-container.html +0 -128
  222. package/examples/select/modal-positioning-test.html +0 -338
  223. package/examples/select/modal.html +0 -80
  224. package/examples/select/multiple.html +0 -40
  225. package/examples/select/native-selected.html +0 -64
  226. package/examples/select/placeholder.html +0 -40
  227. package/examples/select/remote-data-preselected.html +0 -283
  228. package/examples/select/remote-data.html +0 -38
  229. package/examples/select/search.html +0 -57
  230. package/examples/select/sizes.html +0 -94
  231. package/examples/select/tags-enhanced.html +0 -86
  232. package/examples/select/tags-icons.html +0 -57
  233. package/examples/select/template-customization.html +0 -61
  234. package/examples/sticky/README.md +0 -158
  235. package/examples/sticky/debug-sticky.html +0 -144
  236. package/examples/sticky/test-runner.html +0 -175
  237. package/examples/sticky/test-sticky-logic.js +0 -369
  238. package/examples/sticky/test-sticky-positioning.html +0 -386
  239. package/examples/toast/example.html +0 -479
  240. package/lib/cjs/components/datepicker/calendar.js +0 -1061
  241. package/lib/cjs/components/datepicker/calendar.js.map +0 -1
  242. package/lib/cjs/components/datepicker/config.js +0 -332
  243. package/lib/cjs/components/datepicker/config.js.map +0 -1
  244. package/lib/cjs/components/datepicker/dropdown.js +0 -635
  245. package/lib/cjs/components/datepicker/dropdown.js.map +0 -1
  246. package/lib/cjs/components/datepicker/events.js +0 -129
  247. package/lib/cjs/components/datepicker/events.js.map +0 -1
  248. package/lib/cjs/components/datepicker/keyboard.js +0 -536
  249. package/lib/cjs/components/datepicker/keyboard.js.map +0 -1
  250. package/lib/cjs/components/datepicker/locales.js +0 -78
  251. package/lib/cjs/components/datepicker/locales.js.map +0 -1
  252. package/lib/cjs/components/datepicker/templates.js +0 -403
  253. package/lib/cjs/components/datepicker/templates.js.map +0 -1
  254. package/lib/cjs/components/datepicker/types.js +0 -23
  255. package/lib/cjs/components/datepicker/types.js.map +0 -1
  256. package/lib/cjs/components/datepicker/utils.js +0 -524
  257. package/lib/cjs/components/datepicker/utils.js.map +0 -1
  258. package/lib/esm/components/datepicker/calendar.js +0 -1058
  259. package/lib/esm/components/datepicker/calendar.js.map +0 -1
  260. package/lib/esm/components/datepicker/config.js +0 -329
  261. package/lib/esm/components/datepicker/config.js.map +0 -1
  262. package/lib/esm/components/datepicker/dropdown.js +0 -632
  263. package/lib/esm/components/datepicker/dropdown.js.map +0 -1
  264. package/lib/esm/components/datepicker/events.js +0 -126
  265. package/lib/esm/components/datepicker/events.js.map +0 -1
  266. package/lib/esm/components/datepicker/keyboard.js +0 -533
  267. package/lib/esm/components/datepicker/keyboard.js.map +0 -1
  268. package/lib/esm/components/datepicker/locales.js +0 -74
  269. package/lib/esm/components/datepicker/locales.js.map +0 -1
  270. package/lib/esm/components/datepicker/templates.js +0 -390
  271. package/lib/esm/components/datepicker/templates.js.map +0 -1
  272. package/lib/esm/components/datepicker/types.js +0 -20
  273. package/lib/esm/components/datepicker/types.js.map +0 -1
  274. package/lib/esm/components/datepicker/utils.js +0 -508
  275. package/lib/esm/components/datepicker/utils.js.map +0 -1
  276. package/prettier.config.js +0 -9
  277. package/src/components/datepicker/calendar.ts +0 -1397
  278. package/src/components/datepicker/config.ts +0 -368
  279. package/src/components/datepicker/dropdown.ts +0 -757
  280. package/src/components/datepicker/events.ts +0 -149
  281. package/src/components/datepicker/keyboard.ts +0 -646
  282. package/src/components/datepicker/locales.ts +0 -80
  283. package/src/components/datepicker/templates.ts +0 -792
  284. package/src/components/datepicker/types.ts +0 -154
  285. package/src/components/datepicker/utils.ts +0 -631
  286. package/src/components/select/variants.css +0 -4
  287. package/tsconfig.json +0 -17
  288. package/webpack.config.js +0 -118
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ formatDateToLocalString,
4
+ normalizeDateToLocalMidnight,
5
+ isSameLocalDay,
6
+ parseLocalDate
7
+ } from '../date-utils';
8
+
9
+ describe('Date Utils - Timezone Handling', () => {
10
+ describe('formatDateToLocalString', () => {
11
+ it('should format date as YYYY-MM-DD using local timezone', () => {
12
+ const date = new Date(2024, 0, 15, 14, 30, 45); // Jan 15, 2024 2:30:45 PM
13
+ const formatted = formatDateToLocalString(date);
14
+ expect(formatted).toBe('2024-01-15');
15
+ });
16
+
17
+ it('should use local timezone components, not UTC', () => {
18
+ // Create a date that would shift if converted to UTC
19
+ // In PST (UTC-8), Jan 15 11:00 PM becomes Jan 16 7:00 AM UTC
20
+ const date = new Date(2024, 0, 15, 23, 0, 0); // Jan 15, 2024 11:00 PM local
21
+ const formatted = formatDateToLocalString(date);
22
+ // Should still be Jan 15 in local timezone, not shifted to Jan 16
23
+ expect(formatted).toBe('2024-01-15');
24
+ });
25
+
26
+ it('should pad single-digit months and days with zeros', () => {
27
+ const date = new Date(2024, 0, 5); // Jan 5, 2024
28
+ const formatted = formatDateToLocalString(date);
29
+ expect(formatted).toBe('2024-01-05');
30
+ });
31
+
32
+ it('should handle year boundaries correctly', () => {
33
+ const date1 = new Date(2023, 11, 31); // Dec 31, 2023
34
+ const date2 = new Date(2024, 0, 1); // Jan 1, 2024
35
+ expect(formatDateToLocalString(date1)).toBe('2023-12-31');
36
+ expect(formatDateToLocalString(date2)).toBe('2024-01-01');
37
+ });
38
+
39
+ it('should handle leap years correctly', () => {
40
+ const date = new Date(2024, 1, 29); // Feb 29, 2024 (leap year)
41
+ const formatted = formatDateToLocalString(date);
42
+ expect(formatted).toBe('2024-02-29');
43
+ });
44
+ });
45
+
46
+ describe('normalizeDateToLocalMidnight', () => {
47
+ it('should set time to local midnight (00:00:00)', () => {
48
+ const date = new Date(2024, 0, 15, 14, 30, 45, 123);
49
+ const normalized = normalizeDateToLocalMidnight(date);
50
+ expect(normalized.getHours()).toBe(0);
51
+ expect(normalized.getMinutes()).toBe(0);
52
+ expect(normalized.getSeconds()).toBe(0);
53
+ expect(normalized.getMilliseconds()).toBe(0);
54
+ });
55
+
56
+ it('should preserve date components (year, month, day)', () => {
57
+ const date = new Date(2024, 0, 15, 14, 30, 45);
58
+ const normalized = normalizeDateToLocalMidnight(date);
59
+ expect(normalized.getFullYear()).toBe(2024);
60
+ expect(normalized.getMonth()).toBe(0);
61
+ expect(normalized.getDate()).toBe(15);
62
+ });
63
+
64
+ it('should return a new Date object, not modify the original', () => {
65
+ const date = new Date(2024, 0, 15, 14, 30, 45);
66
+ const normalized = normalizeDateToLocalMidnight(date);
67
+ expect(normalized).not.toBe(date);
68
+ expect(date.getHours()).toBe(14); // Original unchanged
69
+ expect(normalized.getHours()).toBe(0); // New one normalized
70
+ });
71
+
72
+ it('should handle dates already at midnight', () => {
73
+ const date = new Date(2024, 0, 15, 0, 0, 0, 0);
74
+ const normalized = normalizeDateToLocalMidnight(date);
75
+ expect(normalized.getHours()).toBe(0);
76
+ expect(normalized.getDate()).toBe(15);
77
+ });
78
+ });
79
+
80
+ describe('isSameLocalDay', () => {
81
+ it('should return true for dates on the same calendar day', () => {
82
+ const date1 = new Date(2024, 0, 15, 10, 0, 0);
83
+ const date2 = new Date(2024, 0, 15, 20, 0, 0);
84
+ expect(isSameLocalDay(date1, date2)).toBe(true);
85
+ });
86
+
87
+ it('should return false for dates on different days', () => {
88
+ const date1 = new Date(2024, 0, 15, 10, 0, 0);
89
+ const date2 = new Date(2024, 0, 16, 10, 0, 0);
90
+ expect(isSameLocalDay(date1, date2)).toBe(false);
91
+ });
92
+
93
+ it('should return false for dates in different months', () => {
94
+ const date1 = new Date(2024, 0, 15, 10, 0, 0);
95
+ const date2 = new Date(2024, 1, 15, 10, 0, 0);
96
+ expect(isSameLocalDay(date1, date2)).toBe(false);
97
+ });
98
+
99
+ it('should return false for dates in different years', () => {
100
+ const date1 = new Date(2024, 0, 15, 10, 0, 0);
101
+ const date2 = new Date(2025, 0, 15, 10, 0, 0);
102
+ expect(isSameLocalDay(date1, date2)).toBe(false);
103
+ });
104
+
105
+ it('should ignore time components when comparing', () => {
106
+ const date1 = new Date(2024, 0, 15, 0, 0, 0, 0);
107
+ const date2 = new Date(2024, 0, 15, 23, 59, 59, 999);
108
+ expect(isSameLocalDay(date1, date2)).toBe(true);
109
+ });
110
+
111
+ it('should use local timezone for comparison', () => {
112
+ // Create dates that might differ in UTC but are same in local timezone
113
+ const date1 = new Date(2024, 0, 15, 23, 0, 0); // Jan 15 11 PM local
114
+ const date2 = new Date(2024, 0, 15, 0, 0, 0); // Jan 15 midnight local
115
+ expect(isSameLocalDay(date1, date2)).toBe(true);
116
+ });
117
+ });
118
+
119
+ describe('parseLocalDate', () => {
120
+ it('should parse YYYY-MM-DD string to local date at midnight', () => {
121
+ const parsed = parseLocalDate('2024-01-15');
122
+ expect(parsed.getFullYear()).toBe(2024);
123
+ expect(parsed.getMonth()).toBe(0); // January is 0
124
+ expect(parsed.getDate()).toBe(15);
125
+ expect(parsed.getHours()).toBe(0);
126
+ expect(parsed.getMinutes()).toBe(0);
127
+ expect(parsed.getSeconds()).toBe(0);
128
+ });
129
+
130
+ it('should create date in local timezone, not UTC', () => {
131
+ const parsed = parseLocalDate('2024-01-15');
132
+ // The date should represent Jan 15 at local midnight
133
+ // Not Jan 15 at UTC midnight (which could be different day in some timezones)
134
+ expect(parsed.getDate()).toBe(15);
135
+ expect(parsed.getMonth()).toBe(0);
136
+ });
137
+
138
+ it('should handle single-digit months and days', () => {
139
+ const parsed = parseLocalDate('2024-01-05');
140
+ expect(parsed.getMonth()).toBe(0);
141
+ expect(parsed.getDate()).toBe(5);
142
+ });
143
+
144
+ it('should handle year boundaries', () => {
145
+ const date1 = parseLocalDate('2023-12-31');
146
+ const date2 = parseLocalDate('2024-01-01');
147
+ expect(date1.getFullYear()).toBe(2023);
148
+ expect(date1.getMonth()).toBe(11);
149
+ expect(date1.getDate()).toBe(31);
150
+ expect(date2.getFullYear()).toBe(2024);
151
+ expect(date2.getMonth()).toBe(0);
152
+ expect(date2.getDate()).toBe(1);
153
+ });
154
+
155
+ it('should handle leap years', () => {
156
+ const parsed = parseLocalDate('2024-02-29');
157
+ expect(parsed.getFullYear()).toBe(2024);
158
+ expect(parsed.getMonth()).toBe(1);
159
+ expect(parsed.getDate()).toBe(29);
160
+ });
161
+
162
+ it('should round-trip correctly with formatDateToLocalString', () => {
163
+ const originalDate = new Date(2024, 0, 15, 14, 30, 45);
164
+ const formatted = formatDateToLocalString(originalDate);
165
+ const parsed = parseLocalDate(formatted);
166
+ expect(parsed.getFullYear()).toBe(originalDate.getFullYear());
167
+ expect(parsed.getMonth()).toBe(originalDate.getMonth());
168
+ expect(parsed.getDate()).toBe(originalDate.getDate());
169
+ });
170
+ });
171
+
172
+ describe('Timezone Consistency', () => {
173
+ it('should maintain date consistency across format and parse operations', () => {
174
+ const testDates = [
175
+ new Date(2024, 0, 1), // Jan 1
176
+ new Date(2024, 5, 15), // Jun 15
177
+ new Date(2024, 11, 31), // Dec 31
178
+ new Date(2024, 1, 29), // Feb 29 (leap year)
179
+ ];
180
+
181
+ testDates.forEach(date => {
182
+ const formatted = formatDateToLocalString(date);
183
+ const parsed = parseLocalDate(formatted);
184
+ expect(parsed.getFullYear()).toBe(date.getFullYear());
185
+ expect(parsed.getMonth()).toBe(date.getMonth());
186
+ expect(parsed.getDate()).toBe(date.getDate());
187
+ });
188
+ });
189
+
190
+ it('should prevent date shifts from UTC conversion', () => {
191
+ // Create a date late in the day that could shift when converted to UTC
192
+ const lateEvening = new Date(2024, 0, 15, 23, 30, 0);
193
+ const formatted = formatDateToLocalString(lateEvening);
194
+
195
+ // Should remain Jan 15, not shift to Jan 16
196
+ expect(formatted).toBe('2024-01-15');
197
+
198
+ // Parse it back and verify it's still the same day
199
+ const parsed = parseLocalDate(formatted);
200
+ expect(parsed.getDate()).toBe(15);
201
+ expect(parsed.getMonth()).toBe(0);
202
+ });
203
+
204
+ it('should handle dates at midnight correctly', () => {
205
+ const midnight = new Date(2024, 0, 15, 0, 0, 0, 0);
206
+ const formatted = formatDateToLocalString(midnight);
207
+ expect(formatted).toBe('2024-01-15');
208
+
209
+ const parsed = parseLocalDate(formatted);
210
+ expect(parsed.getDate()).toBe(15);
211
+ expect(parsed.getHours()).toBe(0);
212
+ });
213
+ });
214
+ });
215
+
@@ -0,0 +1,85 @@
1
+ /*
2
+ * date-formatters.ts - Date formatting and comparison utilities for KTDatepicker
3
+ * Provides pure utility functions for date formatting, comparison, and normalization.
4
+ */
5
+
6
+ /**
7
+ * Formats a Date object according to the provided format string.
8
+ *
9
+ * Supported tokens:
10
+ * yyyy - 4-digit year
11
+ * yy - 2-digit year
12
+ * MM - 2-digit month (01-12)
13
+ * M - 1/2-digit month (1-12)
14
+ * dd - 2-digit day (01-31)
15
+ * d - 1/2-digit day (1-31)
16
+ * HH - 2-digit hour (00-23)
17
+ * H - 1/2-digit hour (0-23)
18
+ * mm - 2-digit minute (00-59)
19
+ * m - 1/2-digit minute (0-59)
20
+ * ss - 2-digit second (00-59)
21
+ * s - 1/2-digit second (0-59)
22
+ * a - AM/PM indicator
23
+ *
24
+ * @param date Date object to format
25
+ * @param format Format string with tokens
26
+ * @returns Formatted date string, or empty string if date is invalid
27
+ *
28
+ * @example
29
+ * formatDate(new Date(2024, 0, 15), 'yyyy-MM-dd') // Returns "2024-01-15"
30
+ * formatDate(new Date(2024, 0, 15, 14, 30), 'HH:mm') // Returns "14:30"
31
+ */
32
+ export function formatDate(date: Date, format: string): string {
33
+ if (!(date instanceof Date) || isNaN(date.getTime())) return '';
34
+ return format
35
+ .replace(/yyyy/g, date.getFullYear().toString())
36
+ .replace(/yy/g, date.getFullYear().toString().slice(-2))
37
+ .replace(/MM/g, String(date.getMonth() + 1).padStart(2, '0'))
38
+ .replace(/M(?![a-zA-Z])/g, String(date.getMonth() + 1))
39
+ .replace(/dd/g, String(date.getDate()).padStart(2, '0'))
40
+ .replace(/d(?![a-zA-Z])/g, String(date.getDate()))
41
+ .replace(/HH/g, String(date.getHours()).padStart(2, '0'))
42
+ .replace(/H(?![a-zA-Z])/g, String(date.getHours()))
43
+ .replace(/mm/g, String(date.getMinutes()).padStart(2, '0'))
44
+ .replace(/m(?![a-zA-Z])/g, String(date.getMinutes()))
45
+ .replace(/ss/g, String(date.getSeconds()).padStart(2, '0'))
46
+ .replace(/s(?![a-zA-Z])/g, String(date.getSeconds()))
47
+ .replace(/a/g, date.getHours() >= 12 ? 'PM' : 'AM');
48
+ }
49
+
50
+ /**
51
+ * Checks if two dates represent the same calendar day.
52
+ * Compares year, month, and day components, ignoring time.
53
+ *
54
+ * @param a First date to compare
55
+ * @param b Second date to compare
56
+ * @returns True if both dates represent the same day, false otherwise
57
+ *
58
+ * @example
59
+ * isSameDay(new Date(2024, 0, 15, 10, 30), new Date(2024, 0, 15, 14, 45)) // Returns true
60
+ * isSameDay(new Date(2024, 0, 15), new Date(2024, 0, 16)) // Returns false
61
+ */
62
+ export function isSameDay(a: Date, b: Date): boolean {
63
+ return a.getFullYear() === b.getFullYear() &&
64
+ a.getMonth() === b.getMonth() &&
65
+ a.getDate() === b.getDate();
66
+ }
67
+
68
+ /**
69
+ * Normalizes a date to local midnight (00:00:00) in the local timezone.
70
+ * Useful for date-only comparisons where time components should be ignored.
71
+ *
72
+ * @param date Date object to normalize
73
+ * @returns New Date object set to local midnight (00:00:00)
74
+ *
75
+ * @example
76
+ * const date = new Date(2024, 0, 15, 14, 30, 45);
77
+ * const normalized = normalizeDateToMidnight(date);
78
+ * // normalized represents 2024-01-15 00:00:00 in local timezone
79
+ */
80
+ export function normalizeDateToMidnight(date: Date): Date {
81
+ const normalized = new Date(date);
82
+ normalized.setHours(0, 0, 0, 0);
83
+ return normalized;
84
+ }
85
+
@@ -0,0 +1,172 @@
1
+ /*
2
+ * date-utils.ts - Shared date parsing/formatting utilities for KTDatepicker
3
+ * Exports parseDateFromFormat for use in datepicker and segmented input.
4
+ */
5
+
6
+ /**
7
+ * Parses a date string according to a format string (supports yyyy, yy, MM, M, dd, d).
8
+ * @param str Date string
9
+ * @param format Format string
10
+ * @returns Date object or null if parsing fails
11
+ */
12
+ export function parseDateFromFormat(str: string, format: string): Date | null {
13
+ if (!str || !format) return null;
14
+ // Supported tokens: yyyy, yy, MM, M, dd, d
15
+ const tokenRegex = /(yyyy|yy|MM|M|dd|d)/g;
16
+ const tokens: string[] = [];
17
+ let regexStr = '';
18
+ let lastIndex = 0;
19
+ let match;
20
+ while ((match = tokenRegex.exec(format)) !== null) {
21
+ // Add any literal text between tokens
22
+ if (match.index > lastIndex) {
23
+ regexStr += format.slice(lastIndex, match.index).replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
24
+ }
25
+ tokens.push(match[0]);
26
+ switch (match[0]) {
27
+ case 'yyyy': regexStr += '(\\d{4})'; break;
28
+ case 'yy': regexStr += '(\\d{2})'; break;
29
+ case 'MM': regexStr += '(\\d{2})'; break;
30
+ case 'M': regexStr += '(\\d{1,2})'; break;
31
+ case 'dd': regexStr += '(\\d{2})'; break;
32
+ case 'd': regexStr += '(\\d{1,2})'; break;
33
+ }
34
+ lastIndex = match.index + match[0].length;
35
+ }
36
+ // Add any trailing literal text
37
+ if (lastIndex < format.length) {
38
+ regexStr += format.slice(lastIndex).replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
39
+ }
40
+ const regex = new RegExp('^' + regexStr + '$');
41
+ const matchResult = regex.exec(str);
42
+ if (!matchResult) return null;
43
+ let year = 1970, month = 1, day = 1;
44
+ let matchIdx = 1;
45
+ for (const token of tokens) {
46
+ const val = matchResult[matchIdx++] || '';
47
+ switch (token) {
48
+ case 'yyyy': year = parseInt(val, 10); break;
49
+ case 'yy': year = 2000 + parseInt(val, 10); break;
50
+ case 'MM':
51
+ case 'M': month = parseInt(val, 10); break;
52
+ case 'dd':
53
+ case 'd': day = parseInt(val, 10); break;
54
+ }
55
+ }
56
+ return new Date(year, month - 1, day);
57
+ }
58
+
59
+ /**
60
+ * Extracts segment order from a format string for use in segmented input.
61
+ * @param format Format string (e.g., 'dd/MM/yyyy', 'yyyy-MM-dd')
62
+ * @returns Array of segment types in the order they appear in the format
63
+ */
64
+ export function getSegmentOrderFromFormat(format: string): Array<'day' | 'month' | 'year'> {
65
+ if (!format) return ['month', 'day', 'year']; // Default fallback
66
+
67
+ const segments: Array<'day' | 'month' | 'year'> = [];
68
+ const tokenRegex = /(yyyy|yy|MM|M|dd|d)/g;
69
+ let match;
70
+
71
+ while ((match = tokenRegex.exec(format)) !== null) {
72
+ switch (match[0]) {
73
+ case 'yyyy':
74
+ case 'yy':
75
+ segments.push('year');
76
+ break;
77
+ case 'MM':
78
+ case 'M':
79
+ segments.push('month');
80
+ break;
81
+ case 'dd':
82
+ case 'd':
83
+ segments.push('day');
84
+ break;
85
+ }
86
+ }
87
+
88
+ // Remove duplicates while preserving order
89
+ const uniqueSegments = segments.filter((segment, index) => segments.indexOf(segment) === index);
90
+
91
+ // Ensure we have all required segments, add missing ones at the end
92
+ const requiredSegments: Array<'day' | 'month' | 'year'> = ['day', 'month', 'year'];
93
+ requiredSegments.forEach(segment => {
94
+ if (!uniqueSegments.includes(segment)) {
95
+ uniqueSegments.push(segment);
96
+ }
97
+ });
98
+
99
+ return uniqueSegments;
100
+ }
101
+
102
+ /**
103
+ * Formats a date to a local timezone string in YYYY-MM-DD format.
104
+ * Uses local timezone components to prevent date shifts from UTC conversion.
105
+ * @param date Date object to format
106
+ * @returns Date string in YYYY-MM-DD format using local timezone
107
+ */
108
+ export function formatDateToLocalString(date: Date): string {
109
+ const year = date.getFullYear();
110
+ const month = String(date.getMonth() + 1).padStart(2, '0');
111
+ const day = String(date.getDate()).padStart(2, '0');
112
+ return `${year}-${month}-${day}`;
113
+ }
114
+
115
+ /**
116
+ * Normalizes a date to local midnight (00:00:00) in the local timezone.
117
+ * Useful for date-only comparisons where time components should be ignored.
118
+ * @param date Date object to normalize
119
+ * @returns New Date object set to local midnight
120
+ */
121
+ export function normalizeDateToLocalMidnight(date: Date): Date {
122
+ const normalized = new Date(date);
123
+ normalized.setHours(0, 0, 0, 0);
124
+ return normalized;
125
+ }
126
+
127
+ /**
128
+ * Checks if two dates represent the same calendar day in local timezone.
129
+ * Compares year, month, and day components, ignoring time.
130
+ * @param a First date
131
+ * @param b Second date
132
+ * @returns True if both dates represent the same day in local timezone
133
+ */
134
+ export function isSameLocalDay(a: Date, b: Date): boolean {
135
+ return a.getFullYear() === b.getFullYear() &&
136
+ a.getMonth() === b.getMonth() &&
137
+ a.getDate() === b.getDate();
138
+ }
139
+
140
+ /**
141
+ * Parses a date string (YYYY-MM-DD format) into a Date object at local midnight.
142
+ * Creates a local timezone date to avoid timezone-related date shifts.
143
+ * @param dateStr Date string in YYYY-MM-DD format
144
+ * @returns Date object representing the date at local midnight
145
+ */
146
+ export function parseLocalDate(dateStr: string): Date {
147
+ // Parse YYYY-MM-DD string and create local date at midnight
148
+ const [year, month, day] = dateStr.split('-').map(Number);
149
+ return new Date(year, month - 1, day, 0, 0, 0, 0);
150
+ }
151
+
152
+ /**
153
+ * Converts a date to a numeric key for O(1) date comparisons.
154
+ * Format: year * 10000 + month * 100 + day
155
+ * This enables efficient Set/Map lookups for date matching.
156
+ * @param date Date object to convert
157
+ * @returns Numeric key representing the date (e.g., 20241225 for Dec 25, 2024)
158
+ */
159
+ export function getDateKey(date: Date): number {
160
+ return date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();
161
+ }
162
+
163
+ /**
164
+ * Checks if two dates have the same date key (same calendar day).
165
+ * Optimized version using numeric keys for O(1) comparison.
166
+ * @param a First date
167
+ * @param b Second date
168
+ * @returns True if both dates have the same date key
169
+ */
170
+ export function isSameDayByKey(a: Date, b: Date): boolean {
171
+ return getDateKey(a) === getDateKey(b);
172
+ }
@@ -0,0 +1,8 @@
1
+ /*
2
+ * utils/index.ts - Utility module exports
3
+ * Provides date and time utility functions for KTDatepicker
4
+ */
5
+
6
+ export * from './date-utils';
7
+ export * from './date-formatters';
8
+ export * from './time-utils';
@@ -0,0 +1,221 @@
1
+ /*
2
+ * time-utils.ts - Time utilities for KTDatepicker
3
+ * Provides time parsing, formatting, validation, and granularity handling.
4
+ * Follows HeroUI best practices for time picker functionality.
5
+ */
6
+
7
+ import { TimeState } from '../config/types';
8
+
9
+ /**
10
+ * Parse time string to TimeState object
11
+ * @param timeStr Time string in format 'HH:MM' or 'HH:MM:SS'
12
+ * @returns TimeState object or null if invalid
13
+ */
14
+ export function parseTimeString(timeStr: string): TimeState | null {
15
+ if (!timeStr || typeof timeStr !== 'string') return null;
16
+
17
+ const timeRegex = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/;
18
+ const match = timeStr.match(timeRegex);
19
+
20
+ if (!match) return null;
21
+
22
+ const hour = parseInt(match[1], 10);
23
+ const minute = parseInt(match[2], 10);
24
+ const second = match[3] ? parseInt(match[3], 10) : 0;
25
+
26
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
27
+ return null;
28
+ }
29
+
30
+ return { hour, minute, second };
31
+ }
32
+
33
+ /**
34
+ * Format TimeState to string
35
+ * @param time TimeState object
36
+ * @param granularity Granularity level
37
+ * @param format Time format ('12h' or '24h')
38
+ * @returns Formatted time string
39
+ */
40
+ export function formatTime(time: TimeState, granularity: 'second' | 'minute' | 'hour' = 'minute', format: '12h' | '24h' = '24h'): string {
41
+ if (!time) return '';
42
+
43
+ let { hour, minute, second } = time;
44
+ let ampm = '';
45
+
46
+ // Handle 12-hour format
47
+ if (format === '12h') {
48
+ ampm = hour >= 12 ? 'PM' : 'AM';
49
+ hour = hour % 12;
50
+ hour = hour === 0 ? 12 : hour;
51
+ }
52
+
53
+ // Format based on granularity
54
+ switch (granularity) {
55
+ case 'hour':
56
+ return format === '12h' ? `${hour} ${ampm}` : `${hour.toString().padStart(2, '0')}`;
57
+ case 'minute':
58
+ return format === '12h'
59
+ ? `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')} ${ampm}`
60
+ : `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
61
+ case 'second':
62
+ return format === '12h'
63
+ ? `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')} ${ampm}`
64
+ : `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
65
+ default:
66
+ return format === '12h'
67
+ ? `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')} ${ampm}`
68
+ : `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validate time against constraints
74
+ * @param time TimeState to validate
75
+ * @param minTime Minimum time constraint
76
+ * @param maxTime Maximum time constraint
77
+ * @returns Validation result
78
+ */
79
+ export function validateTime(time: TimeState, minTime?: string, maxTime?: string): { isValid: boolean; error?: string } {
80
+ if (!time) {
81
+ return { isValid: false, error: 'Time is required' };
82
+ }
83
+
84
+ const { hour, minute, second } = time;
85
+
86
+ // Basic range validation
87
+ if (hour < 0 || hour > 23) {
88
+ return { isValid: false, error: 'Hour must be between 0 and 23' };
89
+ }
90
+ if (minute < 0 || minute > 59) {
91
+ return { isValid: false, error: 'Minute must be between 0 and 59' };
92
+ }
93
+ if (second < 0 || second > 59) {
94
+ return { isValid: false, error: 'Second must be between 0 and 59' };
95
+ }
96
+
97
+ // Min/Max time validation
98
+ if (minTime) {
99
+ const minTimeState = parseTimeString(minTime);
100
+ if (minTimeState && isTimeBefore(time, minTimeState)) {
101
+ return { isValid: false, error: `Time must be after ${minTime}` };
102
+ }
103
+ }
104
+
105
+ if (maxTime) {
106
+ const maxTimeState = parseTimeString(maxTime);
107
+ if (maxTimeState && isTimeAfter(time, maxTimeState)) {
108
+ return { isValid: false, error: `Time must be before ${maxTime}` };
109
+ }
110
+ }
111
+
112
+ return { isValid: true };
113
+ }
114
+
115
+ /**
116
+ * Check if time1 is before time2
117
+ * @param time1 First time
118
+ * @param time2 Second time
119
+ * @returns True if time1 is before time2
120
+ */
121
+ export function isTimeBefore(time1: TimeState, time2: TimeState): boolean {
122
+ const totalSeconds1 = time1.hour * 3600 + time1.minute * 60 + time1.second;
123
+ const totalSeconds2 = time2.hour * 3600 + time2.minute * 60 + time2.second;
124
+ return totalSeconds1 < totalSeconds2;
125
+ }
126
+
127
+ /**
128
+ * Check if time1 is after time2
129
+ * @param time1 First time
130
+ * @param time2 Second time
131
+ * @returns True if time1 is after time2
132
+ */
133
+ export function isTimeAfter(time1: TimeState, time2: TimeState): boolean {
134
+ const totalSeconds1 = time1.hour * 3600 + time1.minute * 60 + time1.second;
135
+ const totalSeconds2 = time2.hour * 3600 + time2.minute * 60 + time2.second;
136
+ return totalSeconds1 > totalSeconds2;
137
+ }
138
+
139
+ /**
140
+ * Get time segments based on granularity
141
+ * @param granularity Time granularity
142
+ * @returns Array of segment types
143
+ */
144
+ export function getTimeSegments(granularity: 'second' | 'minute' | 'hour'): Array<'hour' | 'minute' | 'second' | 'ampm'> {
145
+ switch (granularity) {
146
+ case 'hour':
147
+ return ['hour'];
148
+ case 'minute':
149
+ return ['hour', 'minute'];
150
+ case 'second':
151
+ return ['hour', 'minute', 'second'];
152
+ default:
153
+ return ['hour', 'minute'];
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Convert Date object to TimeState
159
+ * @param date Date object
160
+ * @returns TimeState object
161
+ */
162
+ export function dateToTimeState(date: Date): TimeState {
163
+ return {
164
+ hour: date.getHours(),
165
+ minute: date.getMinutes(),
166
+ second: date.getSeconds()
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Convert TimeState to Date object (using current date)
172
+ * @param time TimeState object
173
+ * @returns Date object with time applied
174
+ */
175
+ export function timeStateToDate(time: TimeState): Date {
176
+ const date = new Date();
177
+ date.setHours(time.hour, time.minute, time.second, 0);
178
+ return date;
179
+ }
180
+
181
+ /**
182
+ * Apply time to existing date
183
+ * @param date Date object
184
+ * @param time TimeState object
185
+ * @returns New Date object with time applied
186
+ */
187
+ export function applyTimeToDate(date: Date, time: TimeState): Date {
188
+ const newDate = new Date(date);
189
+ newDate.setHours(time.hour, time.minute, time.second, 0);
190
+ return newDate;
191
+ }
192
+
193
+ /**
194
+ * Get time step options based on granularity and step
195
+ * @param granularity Time granularity
196
+ * @param step Step increment in minutes
197
+ * @returns Array of available time values
198
+ */
199
+ export function getTimeStepOptions(granularity: 'second' | 'minute' | 'hour', step: number = 1): number[] {
200
+ const options: number[] = [];
201
+
202
+ switch (granularity) {
203
+ case 'hour':
204
+ for (let i = 0; i < 24; i += Math.max(1, Math.floor(step / 60))) {
205
+ options.push(i);
206
+ }
207
+ break;
208
+ case 'minute':
209
+ for (let i = 0; i < 60; i += step) {
210
+ options.push(i);
211
+ }
212
+ break;
213
+ case 'second':
214
+ for (let i = 0; i < 60; i += Math.max(1, Math.floor(step * 60))) {
215
+ options.push(i);
216
+ }
217
+ break;
218
+ }
219
+
220
+ return options;
221
+ }