@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,638 @@
1
+ /*
2
+ * segmented-input.ts - Modular segmented input for KTDatepicker (2025+)
3
+ * Each date/time part is rendered as a focusable, editable segment.
4
+ *
5
+ * Features:
6
+ * - Segments: day, month, year, (optionally hour, minute, second, AM/PM)
7
+ * - Keyboard navigation: Tab, Shift+Tab, arrow keys, Enter
8
+ * - Direct typing/editing of segments with focus preservation
9
+ * - ARIA roles and accessibility for all segments
10
+ * - Emits change events on value update (debounced)
11
+ * - Integrates with KTDatepicker for value sync
12
+ *
13
+ * Keyboard Navigation:
14
+ * - Tab/Shift+Tab: Move between segments
15
+ * - Arrow Left/Right: Move between segments
16
+ * - Arrow Up/Down: Increment/decrement segment value
17
+ * - Enter: Move to next segment (wraps from last to first)
18
+ * - Number keys: Direct input with validation (optimized for performance)
19
+ *
20
+ * Performance Optimizations:
21
+ * - No DOM re-rendering during number typing
22
+ * - Focus preserved during rapid input
23
+ * - Debounced onChange events (150ms delay)
24
+ * - Caret position maintained during editing
25
+ */
26
+
27
+ import { getTemplateStrings } from '../templates/templates';
28
+ import { KTDatepickerConfig } from '../../config/types';
29
+
30
+ /**
31
+ * SegmentedInputOptions defines the configuration for the segmented input.
32
+ */
33
+ export interface SegmentedInputOptions {
34
+ value: Date;
35
+ format?: string; // e.g. 'MM/DD/YYYY', 'YYYY-MM-DD', etc.
36
+ min?: Date;
37
+ max?: Date;
38
+ disabled?: boolean;
39
+ required?: boolean;
40
+ readOnly?: boolean;
41
+ locale?: string;
42
+ onChange?: (value: Date) => void;
43
+ segments?: Array<'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'ampm'>;
44
+ timeFormat?: '12h' | '24h'; // Time format for display
45
+ }
46
+
47
+ /**
48
+ * SegmentedInput - renders a segmented date/time input.
49
+ * @param container - HTMLElement to render into
50
+ * @param options - SegmentedInputOptions
51
+ * @returns cleanup function
52
+ */
53
+ export function SegmentedInput(container: HTMLElement, options: SegmentedInputOptions) {
54
+ // --- Internal state ---
55
+ let currentValue = new Date(options.value);
56
+ const segments = options.segments || ['month', 'day', 'year'];
57
+ const locale = options.locale || 'default';
58
+
59
+ // Global flag to track arrow navigation across all segmented inputs
60
+ if (!(window as any).__ktui_segmented_input_arrow_navigation) {
61
+ (window as any).__ktui_segmented_input_arrow_navigation = false;
62
+ }
63
+
64
+ // --- Get templates ---
65
+ // Use a minimal config to get templates; in real usage, pass full config if available
66
+ const templates = getTemplateStrings({} as KTDatepickerConfig);
67
+ const segmentTpl = templates.dateSegment as string | ((data: any) => string) | undefined;
68
+ const separatorTpl = templates.segmentSeparator as string | ((data: any) => string) | undefined;
69
+
70
+ // --- Utility: get separator between segments ---
71
+ function getSeparatorBetweenSegments(segment1: string, segment2: string, format: string | undefined): string {
72
+ // Time segments use ":" as separator
73
+ const timeSegments = ['hour', 'minute', 'second'];
74
+ if (timeSegments.includes(segment1) && timeSegments.includes(segment2)) {
75
+ return ':';
76
+ }
77
+
78
+ // Space between date and time
79
+ const dateSegments = ['day', 'month', 'year'];
80
+ if (dateSegments.includes(segment1) && timeSegments.includes(segment2)) {
81
+ return ' ';
82
+ }
83
+
84
+ // AM/PM has space before it
85
+ if (segment2 === 'ampm') {
86
+ return ' ';
87
+ }
88
+
89
+ // Second to AM/PM has space
90
+ if (segment1 === 'second' && segment2 === 'ampm') {
91
+ return ' ';
92
+ }
93
+
94
+ // Date segments use separator from format
95
+ if (dateSegments.includes(segment1) && dateSegments.includes(segment2)) {
96
+ return getSeparatorFromFormat(format);
97
+ }
98
+
99
+ // Default fallback
100
+ return '';
101
+ }
102
+
103
+ // --- Utility: get separator from format ---
104
+ function getSeparatorFromFormat(format: string | undefined): string {
105
+ if (!format) return '/'; // Default fallback
106
+
107
+ // Find the first non-date token character to use as separator
108
+ const tokenRegex = /(yyyy|yy|MM|M|dd|d)/g;
109
+ let lastIndex = 0;
110
+ let match;
111
+
112
+ while ((match = tokenRegex.exec(format)) !== null) {
113
+ if (match.index > lastIndex) {
114
+ // Found a separator character
115
+ const separator = format.slice(lastIndex, match.index);
116
+ if (separator && separator.length > 0) {
117
+ return separator;
118
+ }
119
+ }
120
+ lastIndex = match.index + match[0].length;
121
+ }
122
+
123
+ // Check for separator after the last token
124
+ if (lastIndex < format.length) {
125
+ const separator = format.slice(lastIndex);
126
+ if (separator && separator.length > 0) {
127
+ return separator;
128
+ }
129
+ }
130
+
131
+ return '/'; // Default fallback
132
+ }
133
+
134
+ // --- Utility: check if segment should be padded based on format ---
135
+ function shouldPadSegment(segment: string, format: string | undefined): boolean {
136
+ if (!format) return true; // Default to padded for backward compatibility
137
+
138
+ const tokenRegex = /(yyyy|yy|MM|M|dd|d|HH|H|mm|m|ss|s)/g;
139
+ let match;
140
+
141
+ while ((match = tokenRegex.exec(format)) !== null) {
142
+ switch (match[0]) {
143
+ case 'dd': if (segment === 'day') return true; break;
144
+ case 'd': if (segment === 'day') return false; break;
145
+ case 'MM': if (segment === 'month') return true; break;
146
+ case 'M': if (segment === 'month') return false; break;
147
+ case 'yyyy': if (segment === 'year') return true; break; // 4-digit year
148
+ case 'yy': if (segment === 'year') return false; break; // 2-digit year
149
+ case 'HH':
150
+ case 'H': if (segment === 'hour') return match[0] === 'HH'; break;
151
+ case 'mm':
152
+ case 'm': if (segment === 'minute') return match[0] === 'mm'; break;
153
+ case 'ss':
154
+ case 's': if (segment === 'second') return match[0] === 'ss'; break;
155
+ }
156
+ }
157
+
158
+ return true; // Default fallback
159
+ }
160
+
161
+ // --- Utility: get segment value as string ---
162
+ function getSegmentValue(segment: string, date: Date): string {
163
+ const shouldPad = shouldPadSegment(segment, options.format);
164
+
165
+ switch (segment) {
166
+ case 'day':
167
+ const day = date.getDate();
168
+ return shouldPad ? day.toString().padStart(2, '0') : day.toString();
169
+ case 'month':
170
+ const month = date.getMonth() + 1;
171
+ return shouldPad ? month.toString().padStart(2, '0') : month.toString();
172
+ case 'year':
173
+ const year = date.getFullYear();
174
+ return shouldPad ? year.toString() : year.toString().slice(-2); // yy format shows last 2 digits
175
+ case 'hour':
176
+ // For 12-hour format, convert to 1-12 range
177
+ let hourValue: number;
178
+ if (options.timeFormat === '12h') {
179
+ const hour24 = date.getHours();
180
+ hourValue = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24;
181
+ } else {
182
+ hourValue = date.getHours();
183
+ }
184
+ return shouldPad ? hourValue.toString().padStart(2, '0') : hourValue.toString();
185
+ case 'minute':
186
+ const minute = date.getMinutes();
187
+ return shouldPad ? minute.toString().padStart(2, '0') : minute.toString();
188
+ case 'second':
189
+ const second = date.getSeconds();
190
+ return shouldPad ? second.toString().padStart(2, '0') : second.toString();
191
+ case 'ampm': return date.getHours() < 12 ? 'AM' : 'PM';
192
+ default: return '';
193
+ }
194
+ }
195
+
196
+ // --- Utility: set segment value ---
197
+ function setSegmentValue(segment: string, value: string, date: Date): Date {
198
+ const d = new Date(date);
199
+ switch (segment) {
200
+ case 'day': d.setDate(Number(value)); break;
201
+ case 'month': d.setMonth(Number(value) - 1); break;
202
+ case 'year': d.setFullYear(Number(value)); break;
203
+ case 'hour':
204
+ // Handle 12-hour vs 24-hour format
205
+ let hourValue = Number(value);
206
+ if (options.timeFormat === '12h') {
207
+ // In 12-hour mode, convert 12-hour input to 24-hour
208
+ const currentHour = d.getHours();
209
+ const isPM = currentHour >= 12;
210
+ if (hourValue === 12) {
211
+ hourValue = isPM ? 12 : 0; // 12 AM = 0, 12 PM = 12
212
+ } else if (isPM) {
213
+ hourValue += 12; // PM hours: add 12
214
+ }
215
+ }
216
+ // Preserve existing minutes and seconds when setting hour
217
+ const currentMinutes = d.getMinutes();
218
+ const currentSecondsForHour = d.getSeconds();
219
+ d.setHours(hourValue, currentMinutes, currentSecondsForHour);
220
+ break;
221
+ case 'minute':
222
+ // Preserve existing seconds when setting minute
223
+ const currentSecondsForMinute = d.getSeconds();
224
+ d.setMinutes(Number(value), currentSecondsForMinute);
225
+ break;
226
+ case 'second': d.setSeconds(Number(value)); break;
227
+ case 'ampm':
228
+ if (value === 'AM' && d.getHours() >= 12) {
229
+ d.setHours(d.getHours() - 12);
230
+ } else if (value === 'PM' && d.getHours() < 12) {
231
+ d.setHours(d.getHours() + 12);
232
+ }
233
+ break;
234
+ }
235
+ return d;
236
+ }
237
+
238
+ // --- Utility: get min/max for a segment ---
239
+ function getSegmentMin(segment: string, date: Date): number | undefined {
240
+ switch (segment) {
241
+ case 'day':
242
+ // Use actual month/year for max days
243
+ return 1;
244
+ case 'month':
245
+ return 1;
246
+ case 'year':
247
+ return options.min ? options.min.getFullYear() : undefined;
248
+ case 'hour':
249
+ return 0;
250
+ case 'minute':
251
+ case 'second':
252
+ return 0;
253
+ case 'ampm':
254
+ return 0; // 0 = AM, 1 = PM
255
+ default:
256
+ return undefined;
257
+ }
258
+ }
259
+ function getSegmentMax(segment: string, date: Date): number | undefined {
260
+ switch (segment) {
261
+ case 'day':
262
+ // Use actual month/year for max days
263
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
264
+ case 'month':
265
+ return 12;
266
+ case 'year':
267
+ return options.max ? options.max.getFullYear() : undefined;
268
+ case 'hour':
269
+ return 23;
270
+ case 'minute':
271
+ case 'second':
272
+ return 59;
273
+ case 'ampm':
274
+ return 1; // 0 = AM, 1 = PM
275
+ default:
276
+ return undefined;
277
+ }
278
+ }
279
+
280
+ // --- Track focused segment index ---
281
+ let focusedIdx = 0;
282
+ // --- Track caret position (offset) ---
283
+ let caretOffset: number | null = null;
284
+ // --- Track if this is the first render ---
285
+ let isInitialRender = true;
286
+ // --- Track if we're in the middle of Arrow Up/Down navigation ---
287
+ let isArrowNavigation = false;
288
+
289
+ // --- Focus a segment by index and restore caret position ---
290
+ /**
291
+ * Restores focus to the segment at the given index and restores caret position if available.
292
+ * @param idx - Index of the segment to focus
293
+ * @param caret - Caret offset to restore (null for end)
294
+ */
295
+ function restoreFocus(idx: number, caret: number | null = null) {
296
+ try {
297
+ const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
298
+ if (segs[idx] && segs[idx].offsetParent !== null) { // Check if element is in DOM
299
+ segs.forEach((el, i) => el.setAttribute('tabindex', i === idx ? '0' : '-1'));
300
+ segs[idx].focus();
301
+ // Restore caret position (at end if null)
302
+ if (segs[idx].isContentEditable) {
303
+ const range = document.createRange();
304
+ range.selectNodeContents(segs[idx]);
305
+ range.collapse(false); // place at end
306
+ if (caret !== null && segs[idx].firstChild) {
307
+ range.setStart(segs[idx].firstChild, Math.min(caret, segs[idx].textContent?.length || 0));
308
+ range.collapse(true);
309
+ }
310
+ const sel = window.getSelection();
311
+ if (sel) {
312
+ sel.removeAllRanges();
313
+ sel.addRange(range);
314
+ }
315
+ }
316
+ }
317
+ } catch (error) {
318
+ // Fallback: focus first available segment
319
+ const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
320
+ if (segs[0]) segs[0].focus();
321
+ }
322
+ }
323
+
324
+ // --- Render segments using templates ---
325
+ function render() {
326
+ // Capture caret position before DOM update
327
+ const prevSegs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
328
+ if (prevSegs[focusedIdx] && document.activeElement === prevSegs[focusedIdx]) {
329
+ const sel = window.getSelection();
330
+ if (sel && sel.anchorNode === prevSegs[focusedIdx].firstChild) {
331
+ caretOffset = sel.anchorOffset;
332
+ } else {
333
+ caretOffset = null;
334
+ }
335
+ } else {
336
+ caretOffset = null;
337
+ }
338
+ container.innerHTML = '';
339
+ container.setAttribute('role', 'group');
340
+ container.setAttribute('aria-label', 'Date input');
341
+ container.tabIndex = -1;
342
+
343
+ // Build segments HTML using templates
344
+ let segmentsHtml = '';
345
+ segments.forEach((segment, idx) => {
346
+ const segmentValue = getSegmentValue(segment, currentValue);
347
+ const segmentData = {
348
+ segmentType: segment,
349
+ segmentValue,
350
+ ariaLabel: segment.charAt(0).toUpperCase() + segment.slice(1),
351
+ ariaValueNow: segmentValue,
352
+ ariaValueText: segmentValue,
353
+ ariaValueMin: getSegmentMin(segment, currentValue)?.toString() ?? '',
354
+ ariaValueMax: getSegmentMax(segment, currentValue)?.toString() ?? '',
355
+ tabindex: idx === focusedIdx ? '0' : '-1',
356
+ contenteditable: (!options.disabled && !options.readOnly).toString(),
357
+ };
358
+
359
+ let segmentHtml = '';
360
+ if (typeof segmentTpl === 'function') {
361
+ segmentHtml = segmentTpl(segmentData);
362
+ } else if (typeof segmentTpl === 'string') {
363
+ segmentHtml = segmentTpl
364
+ .replace(/{{segmentType}}/g, segmentData.segmentType)
365
+ .replace(/{{segmentValue}}/g, segmentData.segmentValue)
366
+ .replace(/{{ariaLabel}}/g, segmentData.ariaLabel)
367
+ .replace(/{{ariaValueNow}}/g, segmentData.ariaValueNow)
368
+ .replace(/{{ariaValueText}}/g, segmentData.ariaValueText)
369
+ .replace(/{{ariaValueMin}}/g, segmentData.ariaValueMin)
370
+ .replace(/{{ariaValueMax}}/g, segmentData.ariaValueMax)
371
+ .replace(/{{tabindex}}/g, segmentData.tabindex)
372
+ .replace(/{{contenteditable}}/g, segmentData.contenteditable)
373
+ .replace(/{{class}}/g, ''); // Replace class placeholder with empty string
374
+ } else {
375
+ segmentHtml = '';
376
+ }
377
+
378
+ segmentsHtml += segmentHtml;
379
+
380
+ if (idx < segments.length - 1) {
381
+ // Get appropriate separator between these segments
382
+ const nextSegment = segments[idx + 1];
383
+ const separator = getSeparatorBetweenSegments(segment, nextSegment, options.format);
384
+
385
+ let sepHtml = '';
386
+ if (typeof separatorTpl === 'function') {
387
+ sepHtml = separatorTpl({ separator });
388
+ } else if (typeof separatorTpl === 'string') {
389
+ sepHtml = separatorTpl
390
+ .replace(/{{separator}}/g, separator)
391
+ .replace(/{{class}}/g, ''); // Replace class placeholder with empty string
392
+ } else {
393
+ sepHtml = '';
394
+ }
395
+ segmentsHtml += sepHtml;
396
+ }
397
+ });
398
+
399
+ // Render segments directly into container (container already has the correct structure from template)
400
+ container.innerHTML = segmentsHtml;
401
+
402
+ // Verify template rendering was successful
403
+ const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
404
+
405
+ if (segs.length === 0) {
406
+ throw new Error('Segmented input template rendering failed');
407
+ }
408
+
409
+ // Re-bind events to all segments
410
+
411
+ segs.forEach((span, idx) => {
412
+ span.addEventListener('keydown', (e) => {
413
+ if (options.disabled || options.readOnly) return;
414
+ // Wrapping navigation
415
+ if (e.key === 'ArrowRight' || (e.key === 'Tab' && !e.shiftKey)) {
416
+ e.preventDefault();
417
+ isArrowNavigation = true; // Set flag to prevent blur onChange
418
+ focusedIdx = (idx + 1) % segments.length;
419
+ caretOffset = null;
420
+ // Update tabindex directly instead of full re-render
421
+ const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
422
+ segs.forEach((el, i) => el.setAttribute('tabindex', i === focusedIdx ? '0' : '-1'));
423
+ restoreFocus(focusedIdx, caretOffset);
424
+ // Reset flag after a short delay to allow focus to be restored
425
+ setTimeout(() => {
426
+ isArrowNavigation = false;
427
+ }, 10);
428
+ } else if (e.key === 'ArrowLeft' || (e.key === 'Tab' && e.shiftKey)) {
429
+ e.preventDefault();
430
+ isArrowNavigation = true; // Set flag to prevent blur onChange
431
+ focusedIdx = (idx - 1 + segments.length) % segments.length;
432
+ caretOffset = null;
433
+ // Update tabindex directly instead of full re-render
434
+ const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
435
+ segs.forEach((el, i) => el.setAttribute('tabindex', i === focusedIdx ? '0' : '-1'));
436
+ restoreFocus(focusedIdx, caretOffset);
437
+ // Reset flag after a short delay to allow focus to be restored
438
+ setTimeout(() => {
439
+ isArrowNavigation = false;
440
+ }, 10);
441
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
442
+ // Increment/decrement value
443
+ e.preventDefault();
444
+ e.stopPropagation(); // Prevent bubbling to main datepicker
445
+ isArrowNavigation = true; // Set flag to prevent blur onChange
446
+
447
+ let newValue: string;
448
+
449
+ if (segments[idx] === 'ampm') {
450
+ // Handle AM/PM toggle
451
+ const currentAmPm = span.textContent || 'AM';
452
+ if (e.key === 'ArrowUp') {
453
+ newValue = currentAmPm === 'AM' ? 'PM' : 'AM';
454
+ } else {
455
+ newValue = currentAmPm === 'AM' ? 'PM' : 'AM';
456
+ }
457
+ } else {
458
+ // Handle numeric segments
459
+ const min = getSegmentMin(segments[idx], currentValue) ?? 0;
460
+ const max = getSegmentMax(segments[idx], currentValue) ?? 9999;
461
+ let current = Number(getSegmentValue(segments[idx], currentValue)) || min;
462
+ if (e.key === 'ArrowUp') {
463
+ current = Math.min(max, current + 1);
464
+ } else if (e.key === 'ArrowDown') {
465
+ current = Math.max(min, current - 1);
466
+ }
467
+ newValue = current.toString();
468
+ const shouldPad = shouldPadSegment(segments[idx], options.format);
469
+ if (segments[idx] === 'year') {
470
+ if (shouldPad) {
471
+ newValue = newValue.padStart(4, '0');
472
+ }
473
+ } else if (shouldPad) {
474
+ newValue = newValue.padStart(2, '0');
475
+ }
476
+ }
477
+
478
+ span.textContent = newValue;
479
+ currentValue = setSegmentValue(segments[idx], newValue, currentValue);
480
+
481
+ // Set global flag to prevent unified observer from overriding UI
482
+ (window as any).__ktui_segmented_input_arrow_navigation = true;
483
+
484
+ // Call onChange immediately for Arrow Up/Down to update the main datepicker
485
+ if (options.onChange) {
486
+ options.onChange(currentValue);
487
+ }
488
+
489
+ // Clear flag after onChange callback
490
+ setTimeout(() => {
491
+ (window as any).__ktui_segmented_input_arrow_navigation = false;
492
+ }, 50);
493
+ // Preserve caret position at end of content
494
+ if (span.isContentEditable) {
495
+ const range = document.createRange();
496
+ range.selectNodeContents(span);
497
+ range.collapse(false); // place at end
498
+ const sel = window.getSelection();
499
+ if (sel) {
500
+ sel.removeAllRanges();
501
+ sel.addRange(range);
502
+ }
503
+ }
504
+ // Reset flag after a short delay to allow focus to be restored
505
+ setTimeout(() => {
506
+ isArrowNavigation = false;
507
+ }, 10);
508
+ } else if (e.key === 'Enter') {
509
+ // Move to next segment on Enter
510
+ e.preventDefault();
511
+ e.stopPropagation(); // Prevent bubbling to main datepicker
512
+ isArrowNavigation = true; // Set flag to prevent blur onChange
513
+ focusedIdx = (idx + 1) % segments.length;
514
+ caretOffset = null;
515
+ // Update tabindex directly instead of full re-render
516
+ const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
517
+ segs.forEach((el, i) => el.setAttribute('tabindex', i === focusedIdx ? '0' : '-1'));
518
+ restoreFocus(focusedIdx, caretOffset);
519
+ // Reset flag after a short delay to allow focus to be restored
520
+ setTimeout(() => {
521
+ isArrowNavigation = false;
522
+ }, 10);
523
+ } else if (/^[0-9]$/.test(e.key)) {
524
+ // Direct typing, enforce min/max - optimized to avoid focus loss
525
+ e.preventDefault();
526
+ let newValue: string;
527
+ const currentText = span.textContent || '';
528
+
529
+ if (segments[idx] === 'year') {
530
+ // For year: shift left and append new digit (e.g., "2024" + "6" = "0246", then becomes "2026" after validation)
531
+ // If already 4 digits, shift left by removing first digit and appending new one
532
+ if (currentText.length === 4) {
533
+ newValue = (currentText.slice(1) + e.key);
534
+ } else {
535
+ newValue = (currentText + e.key).slice(-4);
536
+ }
537
+ } else {
538
+ // For day/month: shift left and append new digit (e.g., "12" + "5" = "25")
539
+ // If already 2 digits, shift left by removing first digit and appending new one
540
+ if (currentText.length === 2) {
541
+ newValue = (currentText.slice(1) + e.key);
542
+ } else {
543
+ newValue = (currentText + e.key).slice(-2);
544
+ }
545
+ }
546
+
547
+ const min = getSegmentMin(segments[idx], currentValue) ?? 0;
548
+ const max = getSegmentMax(segments[idx], currentValue) ?? (segments[idx] === 'year' ? 9999 : 99);
549
+ let num = Math.max(min, Math.min(max, Number(newValue)));
550
+ if (isNaN(num)) num = min;
551
+
552
+ // Update content directly without re-rendering to preserve focus
553
+ const shouldPad = shouldPadSegment(segments[idx], options.format);
554
+ if (segments[idx] === 'year') {
555
+ span.textContent = shouldPad ? num.toString().padStart(4, '0') : num.toString();
556
+ } else {
557
+ span.textContent = shouldPad ? num.toString().padStart(2, '0') : num.toString();
558
+ }
559
+
560
+ // Update internal value
561
+ currentValue = setSegmentValue(segments[idx], span.textContent || '', currentValue);
562
+
563
+ // Debounce onChange to avoid excessive updates during rapid typing
564
+ if (options.onChange) {
565
+ clearTimeout((span as any)._onChangeTimeout);
566
+ (span as any)._onChangeTimeout = setTimeout(() => {
567
+ options.onChange!(currentValue);
568
+ }, 150); // 150ms debounce
569
+ }
570
+
571
+ // Preserve caret position at end of content
572
+ if (span.isContentEditable) {
573
+ const range = document.createRange();
574
+ range.selectNodeContents(span);
575
+ range.collapse(false); // place at end
576
+ const sel = window.getSelection();
577
+ if (sel) {
578
+ sel.removeAllRanges();
579
+ sel.addRange(range);
580
+ }
581
+ }
582
+ }
583
+ });
584
+ // Focus/blur styling (no classes, just ARIA/tabindex)
585
+ span.addEventListener('focus', () => {
586
+ span.setAttribute('tabindex', '0');
587
+ focusedIdx = idx;
588
+ });
589
+ span.addEventListener('blur', () => {
590
+ span.setAttribute('tabindex', '-1');
591
+ // Clear any pending debounced onChange calls
592
+ if ((span as any)._onChangeTimeout) {
593
+ clearTimeout((span as any)._onChangeTimeout);
594
+ (span as any)._onChangeTimeout = null;
595
+ }
596
+ // Call onChange when user finishes editing to update the main datepicker
597
+ // But not during Arrow Up/Down navigation and only if value actually changed
598
+ if (options.onChange && !isArrowNavigation) {
599
+ const updatedValue = setSegmentValue(segments[idx], span.textContent || '', currentValue);
600
+ // Only call onChange if the value has actually changed
601
+ if (updatedValue.getTime() !== currentValue.getTime()) {
602
+ options.onChange(updatedValue);
603
+ }
604
+ }
605
+ });
606
+ // Mouse click focuses segment (optimized to avoid unnecessary re-rendering)
607
+ span.addEventListener('mousedown', (e) => {
608
+ e.preventDefault();
609
+ focusedIdx = idx;
610
+ caretOffset = null;
611
+ // Only update tabindex and focus, no full re-render
612
+ const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
613
+ segs.forEach((el, i) => el.setAttribute('tabindex', i === focusedIdx ? '0' : '-1'));
614
+ restoreFocus(focusedIdx, caretOffset);
615
+ });
616
+ });
617
+ // After rendering, restore focus to the correct segment and caret (skip on initial render)
618
+ if (!isInitialRender) {
619
+ restoreFocus(focusedIdx, caretOffset);
620
+ }
621
+ isInitialRender = false;
622
+ }
623
+
624
+ // --- Initial render ---
625
+ render();
626
+
627
+ // --- Cleanup function ---
628
+ return () => {
629
+ // Clear any pending debounced onChange calls
630
+ const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
631
+ segs.forEach(span => {
632
+ if ((span as any)._onChangeTimeout) {
633
+ clearTimeout((span as any)._onChangeTimeout);
634
+ }
635
+ });
636
+ container.innerHTML = '';
637
+ };
638
+ }