playbook_ui 15.6.0.pre.rc.4 → 15.7.0.pre.alpha.PLAY2678emojimask13284

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/_playbook.scss +1 -1
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +3 -2
  4. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +4 -0
  5. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +95 -0
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_colors_rails.html.erb +43 -0
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_colors_rails.md +1 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_control_rails.html.erb +11 -5
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_control_rails.md +7 -1
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background.jsx +54 -0
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background.md +9 -0
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_multi.jsx +80 -0
  13. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_multi.md +3 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +4 -1
  15. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +3 -1
  16. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +2 -2
  17. data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +57 -0
  18. data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.tsx +6 -0
  19. data/app/pb_kits/playbook/pb_card/docs/_card_header.md +1 -1
  20. data/app/pb_kits/playbook/pb_card/docs/_card_highlight.md +1 -1
  21. data/app/pb_kits/playbook/pb_circle_chart/_circle_chart.tsx +6 -0
  22. data/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap +2 -2
  23. data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleIcon.tsx +10 -8
  24. data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_icons.jsx +0 -1
  25. data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_state.jsx +0 -3
  26. data/app/pb_kits/playbook/pb_contact/_contact.tsx +51 -24
  27. data/app/pb_kits/playbook/pb_contact/contact.html.erb +53 -19
  28. data/app/pb_kits/playbook/pb_contact/contact.rb +11 -1
  29. data/app/pb_kits/playbook/pb_contact/contact.test.js +76 -0
  30. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled.html.erb +33 -0
  31. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled.jsx +46 -0
  32. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled_rails.md +2 -0
  33. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled_react.md +2 -0
  34. data/app/pb_kits/playbook/pb_contact/docs/example.yml +2 -0
  35. data/app/pb_kits/playbook/pb_contact/docs/index.js +1 -0
  36. data/app/pb_kits/playbook/pb_date_picker/date_picker.test.js +24 -0
  37. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +197 -7
  38. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_range_pattern_rails.html.erb +23 -14
  39. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_range_pattern_rails.md +1 -1
  40. data/app/pb_kits/playbook/pb_dialog/_dialog.tsx +2 -1
  41. data/app/pb_kits/playbook/pb_dialog/dialog.html.erb +1 -1
  42. data/app/pb_kits/playbook/pb_dialog/dialog.rb +1 -0
  43. data/app/pb_kits/playbook/pb_dialog/dialog.test.jsx +14 -0
  44. data/app/pb_kits/playbook/pb_dialog/dialog_header.html.erb +5 -4
  45. data/app/pb_kits/playbook/pb_dialog/dialog_header.rb +2 -0
  46. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.html.erb +24 -0
  47. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.jsx +60 -0
  48. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.md +3 -0
  49. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible.html.erb +71 -0
  50. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible.jsx +57 -0
  51. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible_rails.md +1 -0
  52. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible_react.md +1 -0
  53. data/app/pb_kits/playbook/pb_dialog/docs/example.yml +4 -0
  54. data/app/pb_kits/playbook/pb_dialog/docs/index.js +3 -1
  55. data/app/pb_kits/playbook/pb_distribution_bar/docs/_distribution_bar_custom_colors.md +1 -1
  56. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +316 -15
  57. data/app/pb_kits/playbook/pb_draggable/context/types.ts +1 -1
  58. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_rails.html.erb +7 -5
  59. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_default_dates.html.erb +19 -0
  60. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_rails.html.erb +12 -0
  61. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_rails.md +26 -0
  62. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_range_end_rails.html.erb +19 -0
  63. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_range_end_rails.md +1 -0
  64. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_default_rails.html.erb +30 -0
  65. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_default_rails.md +3 -0
  66. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_rails.html.erb +29 -0
  67. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_rails.md +13 -0
  68. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_custom_display_rails.html.erb +3 -1
  69. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +5 -0
  70. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +4 -0
  71. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +39 -5
  72. data/app/pb_kits/playbook/pb_dropdown/index.js +171 -3
  73. data/app/pb_kits/playbook/pb_dropdown/quickpick_helper.rb +75 -0
  74. data/app/pb_kits/playbook/pb_filter/Filter/FilterBackground.tsx +3 -3
  75. data/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb +3 -3
  76. data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +2 -1
  77. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +14 -0
  78. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.md +3 -0
  79. data/app/pb_kits/playbook/pb_form/docs/example.yml +1 -0
  80. data/app/pb_kits/playbook/pb_gauge/_gauge.tsx +6 -0
  81. data/app/pb_kits/playbook/pb_line_graph/_line_graph.tsx +6 -0
  82. data/app/pb_kits/playbook/pb_popover/docs/_popover_append_to.html.erb +2 -2
  83. data/app/pb_kits/playbook/pb_popover/docs/_popover_append_to.jsx +3 -2
  84. data/app/pb_kits/playbook/pb_radio/docs/_radio_error.md +1 -1
  85. data/app/pb_kits/playbook/pb_select/_select.tsx +8 -3
  86. data/app/pb_kits/playbook/pb_select/docs/_select_error.md +1 -1
  87. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.html.erb +16 -0
  88. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.jsx +30 -0
  89. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.md +1 -0
  90. data/app/pb_kits/playbook/pb_select/docs/example.yml +2 -0
  91. data/app/pb_kits/playbook/pb_select/docs/index.js +1 -0
  92. data/app/pb_kits/playbook/pb_select/select.html.erb +2 -2
  93. data/app/pb_kits/playbook/pb_select/select.rb +3 -1
  94. data/app/pb_kits/playbook/pb_select/select.test.js +23 -0
  95. data/app/pb_kits/playbook/pb_table/_table.tsx +187 -33
  96. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.jsx +134 -0
  97. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.md +34 -0
  98. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.html.erb +101 -0
  99. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +33 -0
  100. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.jsx +180 -0
  101. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.md +3 -0
  102. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination_rails.html.erb +122 -0
  103. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination_rails.md +3 -0
  104. data/app/pb_kits/playbook/pb_table/docs/example.yml +4 -0
  105. data/app/pb_kits/playbook/pb_table/docs/index.js +2 -0
  106. data/app/pb_kits/playbook/pb_table/table.html.erb +68 -12
  107. data/app/pb_kits/playbook/pb_table/table.rb +22 -3
  108. data/app/pb_kits/playbook/pb_table/table.test.js +143 -0
  109. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +56 -6
  110. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.html.erb +7 -0
  111. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.jsx +24 -0
  112. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.md +2 -0
  113. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_error.md +1 -1
  114. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.html.erb +6 -0
  115. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.jsx +25 -0
  116. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.md +3 -0
  117. data/app/pb_kits/playbook/pb_text_input/docs/example.yml +5 -0
  118. data/app/pb_kits/playbook/pb_text_input/docs/index.js +2 -0
  119. data/app/pb_kits/playbook/pb_text_input/index.js +49 -8
  120. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +6 -0
  121. data/app/pb_kits/playbook/pb_text_input/text_input.rb +7 -1
  122. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +69 -0
  123. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +38 -2
  124. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.html.erb +5 -0
  125. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.jsx +24 -0
  126. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.md +1 -0
  127. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_error.md +1 -1
  128. data/app/pb_kits/playbook/pb_textarea/docs/example.yml +2 -0
  129. data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
  130. data/app/pb_kits/playbook/pb_textarea/index.ts +62 -5
  131. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +1 -0
  132. data/app/pb_kits/playbook/pb_textarea/textarea.rb +8 -0
  133. data/app/pb_kits/playbook/pb_textarea/textarea.test.js +57 -2
  134. data/app/pb_kits/playbook/pb_time_picker/_time_picker.scss +296 -0
  135. data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +822 -0
  136. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.html.erb +2 -0
  137. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.jsx +16 -0
  138. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.md +1 -0
  139. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.html.erb +1 -0
  140. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.jsx +13 -0
  141. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.md +1 -0
  142. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.html.erb +4 -0
  143. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.jsx +29 -0
  144. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.md +1 -0
  145. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.html.erb +13 -0
  146. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.jsx +23 -0
  147. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.html.erb +5 -0
  148. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.jsx +15 -0
  149. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_input_options.html.erb +14 -0
  150. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.html.erb +2 -0
  151. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.jsx +15 -0
  152. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.html.erb +42 -0
  153. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.jsx +52 -0
  154. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.md +1 -0
  155. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +45 -0
  156. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.md +1 -0
  157. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.html.erb +3 -0
  158. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.jsx +21 -0
  159. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.md +1 -0
  160. data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +24 -0
  161. data/app/pb_kits/playbook/pb_time_picker/docs/index.js +9 -0
  162. data/app/pb_kits/playbook/pb_time_picker/index.ts +40 -0
  163. data/app/pb_kits/playbook/pb_time_picker/time_picker.html.erb +1 -0
  164. data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +80 -0
  165. data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +114 -0
  166. data/app/pb_kits/playbook/pb_time_picker/time_picker_helper.ts +662 -0
  167. data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -0
  168. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.html.erb +60 -0
  169. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.jsx +118 -0
  170. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.md +1 -0
  171. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_date.md +1 -1
  172. data/app/pb_kits/playbook/pb_timeline/docs/example.yml +2 -0
  173. data/app/pb_kits/playbook/pb_timeline/docs/index.js +1 -0
  174. data/app/pb_kits/playbook/pb_timeline/item.html.erb +1 -1
  175. data/app/pb_kits/playbook/pb_timeline/item.rb +2 -0
  176. data/app/pb_kits/playbook/pb_timeline/label.html.erb +2 -1
  177. data/app/pb_kits/playbook/pb_timeline/label.rb +2 -0
  178. data/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx +3 -0
  179. data/app/pb_kits/playbook/pb_timeline/timeline.test.js +51 -0
  180. data/app/pb_kits/playbook/tokens/_colors.scss +2 -1
  181. data/app/pb_kits/playbook/utilities/deprecated.ts +73 -0
  182. data/app/pb_kits/playbook/utilities/emojiMask.ts +42 -0
  183. data/app/pb_kits/playbook/utilities/globalProps.ts +1 -0
  184. data/dist/chunks/_typeahead-CSCNg6cp.js +6 -0
  185. data/dist/chunks/lib-DxCgrqqG.js +29 -0
  186. data/dist/chunks/vendor.js +3 -3
  187. data/dist/menu.yml +16 -9
  188. data/dist/playbook-rails-react-bindings.js +1 -1
  189. data/dist/playbook-rails.js +1 -1
  190. data/dist/playbook.css +1 -1
  191. data/lib/playbook/forms/builder/collection_select_field.rb +9 -1
  192. data/lib/playbook/forms/builder/form_field_builder.rb +15 -2
  193. data/lib/playbook/forms/builder/select_field.rb +9 -1
  194. data/lib/playbook/forms/builder/time_picker_field.rb +24 -0
  195. data/lib/playbook/forms/builder/time_zone_select_field.rb +9 -1
  196. data/lib/playbook/forms/builder.rb +1 -0
  197. data/lib/playbook/pb_doc_helper.rb +3 -0
  198. data/lib/playbook/pb_kit_helper.rb +35 -0
  199. data/lib/playbook/version.rb +2 -2
  200. metadata +92 -4
  201. data/dist/chunks/_typeahead-BXM7QUuy.js +0 -6
  202. data/dist/chunks/lib-CgpqUb6l.js +0 -29
@@ -0,0 +1,662 @@
1
+ // Time Picker Helper Functions
2
+ // Utility functions for parsing, validating, and formatting time values
3
+
4
+ export type ParsedTime = {
5
+ hour: number
6
+ minute: number
7
+ meridiem: 'AM' | 'PM'
8
+ }
9
+
10
+ export type TimeFormat = 'AMPM' | '24hour'
11
+
12
+ export type HourConstraints = {
13
+ maxHour: number
14
+ minHour: number
15
+ }
16
+
17
+ export type HourInputResult = {
18
+ value: string
19
+ hour: number | null
20
+ isValid: boolean
21
+ capped: boolean
22
+ }
23
+
24
+ export type MinuteInputResult = {
25
+ value: string
26
+ minute: number | null
27
+ isValid: boolean
28
+ capped: boolean
29
+ }
30
+
31
+ export type FormatConversionResult = {
32
+ hour: number
33
+ meridiem: 'AM' | 'PM'
34
+ changed: boolean
35
+ }
36
+
37
+ // ===========================================================
38
+ // | Time Parsing Utilities |
39
+ // ===========================================================
40
+
41
+ /**
42
+ * Parse a time string to minutes since midnight for comparison
43
+ * @param timeStr - Time string in "HH:MM" format (24-hour)
44
+ * @returns Minutes since midnight, or null if invalid
45
+ */
46
+ export const parseTimeToMinutes = (timeStr: string | undefined): number | null => {
47
+ if (!timeStr) return null
48
+ const match = timeStr.match(/(\d{1,2}):(\d{2})/)
49
+ if (match) {
50
+ const h = parseInt(match[1], 10)
51
+ const m = parseInt(match[2], 10)
52
+ return h * 60 + m
53
+ }
54
+ return null
55
+ }
56
+
57
+ /**
58
+ * Parse a time string into hour, minute, and meridiem components
59
+ * @param timeStr - Time string in "HH:MM" (24-hour) or "H:MM AM/PM" (12-hour) format
60
+ * @param timeFormat - The target time format ('AMPM' or '24hour')
61
+ * @returns Parsed time object with hour, minute, and meridiem
62
+ */
63
+ export const parseTime = (
64
+ timeStr: string | undefined,
65
+ timeFormat: TimeFormat = 'AMPM'
66
+ ): ParsedTime => {
67
+ if (!timeStr) {
68
+ // Default to 12:00 PM when no time is provided
69
+ return {
70
+ hour: 12,
71
+ minute: 0,
72
+ meridiem: 'PM',
73
+ }
74
+ }
75
+
76
+ const match = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)?/i)
77
+ if (match) {
78
+ let hour = parseInt(match[1], 10)
79
+ const minute = parseInt(match[2], 10)
80
+ let meridiem = match[3]?.toUpperCase() as 'AM' | 'PM'
81
+
82
+ if (timeFormat === '24hour') {
83
+ // For 24-hour format, store hour as-is (0-23) but keep meridiem for internal state
84
+ // If meridiem is provided, convert to 24-hour first
85
+ if (meridiem) {
86
+ if (meridiem === 'PM' && hour !== 12) hour += 12
87
+ if (meridiem === 'AM' && hour === 12) hour = 0
88
+ }
89
+ if (hour > 23) hour = 23
90
+ if (hour < 0) hour = 0
91
+ meridiem = hour < 12 ? 'AM' : 'PM'
92
+ return { hour, minute, meridiem }
93
+ }
94
+
95
+ // 12-hour format (AMPM)
96
+ // If no meridiem specified, infer from hour (assuming 24-hour format input)
97
+ if (!meridiem) {
98
+ // Input is likely 24-hour format, convert to 12-hour
99
+ if (hour > 12) {
100
+ hour = hour % 12
101
+ meridiem = 'PM'
102
+ } else if (hour === 12) {
103
+ meridiem = 'PM'
104
+ } else if (hour === 0) {
105
+ hour = 12
106
+ meridiem = 'AM'
107
+ } else {
108
+ meridiem = 'AM'
109
+ }
110
+ }
111
+
112
+ return { hour, minute, meridiem }
113
+ }
114
+
115
+ return { hour: 12, minute: 0, meridiem: 'PM' }
116
+ }
117
+
118
+ // ===========================================================
119
+ // | Time Validation Utilities |
120
+ // ===========================================================
121
+
122
+ /**
123
+ * Check if a given time is within the allowed min/max range
124
+ * @param h - Hour value
125
+ * @param m - Minute value
126
+ * @param mer - Meridiem ('AM' or 'PM'), required for AMPM format
127
+ * @param timeFormat - The time format being used
128
+ * @param minTimeMinutes - Minimum time in minutes since midnight
129
+ * @param maxTimeMinutes - Maximum time in minutes since midnight
130
+ * @returns True if time is within range
131
+ */
132
+ export const isTimeInRange = (
133
+ h: number,
134
+ m: number,
135
+ mer: 'AM' | 'PM' | undefined,
136
+ timeFormat: TimeFormat,
137
+ minTimeMinutes: number | null,
138
+ maxTimeMinutes: number | null
139
+ ): boolean => {
140
+ let hour24 = h
141
+ // Convert to 24-hour if we have meridiem and are in AMPM mode
142
+ if (timeFormat === 'AMPM' && mer) {
143
+ if (mer === 'PM' && h !== 12) hour24 = h + 12
144
+ if (mer === 'AM' && h === 12) hour24 = 0
145
+ }
146
+ const timeInMinutes = hour24 * 60 + m
147
+
148
+ if (minTimeMinutes !== null && timeInMinutes < minTimeMinutes) return false
149
+ if (maxTimeMinutes !== null && timeInMinutes > maxTimeMinutes) return false
150
+ return true
151
+ }
152
+
153
+ /**
154
+ * Check if a specific hour is completely disabled (all minutes are out of range)
155
+ * @param h - Hour value
156
+ * @param mer - Meridiem ('AM' or 'PM')
157
+ * @param timeFormat - The time format being used
158
+ * @param minTimeMinutes - Minimum time in minutes since midnight
159
+ * @param maxTimeMinutes - Maximum time in minutes since midnight
160
+ * @returns True if the entire hour is disabled
161
+ */
162
+ export const isHourDisabled = (
163
+ h: number,
164
+ mer: 'AM' | 'PM' | undefined,
165
+ timeFormat: TimeFormat,
166
+ minTimeMinutes: number | null,
167
+ maxTimeMinutes: number | null
168
+ ): boolean => {
169
+ // Check if any minute in this hour is valid
170
+ for (let m = 0; m < 60; m++) {
171
+ if (isTimeInRange(h, m, mer, timeFormat, minTimeMinutes, maxTimeMinutes)) return false
172
+ }
173
+ return true
174
+ }
175
+
176
+ /**
177
+ * Check if ANY AM time is valid in the range (for disabling AM toggle)
178
+ * @param minTimeMinutes - Minimum time in minutes since midnight
179
+ * @param maxTimeMinutes - Maximum time in minutes since midnight
180
+ * @returns True if any AM time is valid
181
+ */
182
+ export const isAnyAMTimeValid = (
183
+ minTimeMinutes: number | null,
184
+ maxTimeMinutes: number | null
185
+ ): boolean => {
186
+ if (minTimeMinutes === null && maxTimeMinutes === null) return true
187
+ // AM times are 00:00 to 11:59 (0-719 minutes)
188
+ const amStart = 0
189
+ const amEnd = 719
190
+ const rangeStart = minTimeMinutes ?? 0
191
+ const rangeEnd = maxTimeMinutes ?? 1439
192
+ // Check if AM range overlaps with valid range
193
+ return amStart <= rangeEnd && amEnd >= rangeStart
194
+ }
195
+
196
+ /**
197
+ * Check if ANY PM time is valid in the range (for disabling PM toggle)
198
+ * @param minTimeMinutes - Minimum time in minutes since midnight
199
+ * @param maxTimeMinutes - Maximum time in minutes since midnight
200
+ * @returns True if any PM time is valid
201
+ */
202
+ export const isAnyPMTimeValid = (
203
+ minTimeMinutes: number | null,
204
+ maxTimeMinutes: number | null
205
+ ): boolean => {
206
+ if (minTimeMinutes === null && maxTimeMinutes === null) return true
207
+ // PM times are 12:00 to 23:59 (720-1439 minutes)
208
+ const pmStart = 720
209
+ const pmEnd = 1439
210
+ const rangeStart = minTimeMinutes ?? 0
211
+ const rangeEnd = maxTimeMinutes ?? 1439
212
+ // Check if PM range overlaps with valid range
213
+ return pmStart <= rangeEnd && pmEnd >= rangeStart
214
+ }
215
+
216
+ // ===========================================================
217
+ // | Time Formatting Utilities |
218
+ // ===========================================================
219
+
220
+ /**
221
+ * Format time for display in the input field
222
+ * @param h - Hour value
223
+ * @param m - Minute value
224
+ * @param mer - Meridiem ('AM' or 'PM')
225
+ * @param timeFormat - The time format to use for display
226
+ * @returns Formatted time string
227
+ */
228
+ export const getDisplayTime = (
229
+ h: number,
230
+ m: number,
231
+ mer: 'AM' | 'PM',
232
+ timeFormat: TimeFormat
233
+ ): string => {
234
+ if (timeFormat === '24hour') {
235
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`
236
+ }
237
+ return `${h}:${m.toString().padStart(2, '0')} ${mer}`
238
+ }
239
+
240
+ /**
241
+ * Convert time to 24-hour format string for onChange/onClose callbacks
242
+ * @param h - Hour value
243
+ * @param m - Minute value
244
+ * @param mer - Meridiem ('AM' or 'PM')
245
+ * @param timeFormat - The current time format
246
+ * @returns Time string in "HH:MM" 24-hour format
247
+ */
248
+ export const get24HourTime = (
249
+ h: number,
250
+ m: number,
251
+ mer: 'AM' | 'PM',
252
+ timeFormat: TimeFormat
253
+ ): string => {
254
+ if (timeFormat === '24hour') {
255
+ // Already in 24-hour format
256
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`
257
+ }
258
+ // Convert from 12-hour to 24-hour
259
+ let hour24 = h
260
+ if (mer === 'PM' && h !== 12) hour24 = h + 12
261
+ if (mer === 'AM' && h === 12) hour24 = 0
262
+ return `${hour24.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`
263
+ }
264
+
265
+ /**
266
+ * Format a 24-hour time string to display format based on timeFormat
267
+ * @param time24 - Time string in "HH:MM" 24-hour format
268
+ * @param timeFormat - The target time format for display
269
+ * @returns Formatted time string
270
+ */
271
+ export const formatTimeForDisplay = (
272
+ time24: string,
273
+ timeFormat: TimeFormat
274
+ ): string => {
275
+ const match = time24.match(/(\d{1,2}):(\d{2})/)
276
+ if (!match) return time24
277
+
278
+ let h = parseInt(match[1], 10)
279
+ const m = parseInt(match[2], 10)
280
+ const mStr = m.toString().padStart(2, '0')
281
+
282
+ if (timeFormat === '24hour') {
283
+ return `${h}:${mStr}`
284
+ }
285
+
286
+ // 12-hour format
287
+ const meridiem = h >= 12 ? 'PM' : 'AM'
288
+ if (h === 0) h = 12
289
+ else if (h > 12) h = h - 12
290
+ return `${h}:${mStr}${meridiem}`
291
+ }
292
+
293
+ /**
294
+ * Generate the time range error message in the correct format
295
+ * @param minTime - Minimum time in "HH:MM" format
296
+ * @param maxTime - Maximum time in "HH:MM" format
297
+ * @param timeFormat - The time format for display
298
+ * @returns Error message string
299
+ */
300
+ export const getTimeRangeErrorMessage = (
301
+ minTime: string | undefined,
302
+ maxTime: string | undefined,
303
+ timeFormat: TimeFormat
304
+ ): string => {
305
+ const minFormatted = minTime ? formatTimeForDisplay(minTime, timeFormat) : null
306
+ const maxFormatted = maxTime ? formatTimeForDisplay(maxTime, timeFormat) : null
307
+
308
+ if (minFormatted && maxFormatted) {
309
+ return `Select a time between ${minFormatted} and ${maxFormatted}.`
310
+ } else if (minFormatted) {
311
+ return `Select a time after ${minFormatted}.`
312
+ } else if (maxFormatted) {
313
+ return `Select a time before ${maxFormatted}.`
314
+ }
315
+ return 'Selected time is out of range.'
316
+ }
317
+
318
+ // ===========================================================
319
+ // | Timezone Utilities |
320
+ // ===========================================================
321
+
322
+ /**
323
+ * Get the current timezone text for display
324
+ * @returns Timezone string like "EST (Eastern Standard Time)"
325
+ */
326
+ export const getTimezoneText = (): string => {
327
+ const date = new Date()
328
+ const tzAbbr = date
329
+ .toLocaleDateString('en-US', {
330
+ day: '2-digit',
331
+ timeZoneName: 'short',
332
+ })
333
+ .slice(4)
334
+ const tzText = date
335
+ .toLocaleDateString('en-US', {
336
+ day: '2-digit',
337
+ timeZoneName: 'long',
338
+ })
339
+ .slice(4)
340
+ return `${tzAbbr} (${tzText})`
341
+ }
342
+
343
+ // ===========================================================
344
+ // | Hour/Minute Input Handling |
345
+ // ===========================================================
346
+
347
+ /**
348
+ * Get the hour constraints based on time format
349
+ * @param timeFormat - The time format being used
350
+ * @returns Object with maxHour and minHour
351
+ */
352
+ export const getHourConstraints = (timeFormat: TimeFormat): HourConstraints => {
353
+ return {
354
+ maxHour: timeFormat === '24hour' ? 23 : 12,
355
+ minHour: timeFormat === '24hour' ? 0 : 1,
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Validate and process hour input value
361
+ * @param rawValue - The raw input value
362
+ * @param timeFormat - The time format being used
363
+ * @returns Result object with processed value, hour number, and validation status
364
+ */
365
+ export const processHourInput = (
366
+ rawValue: string,
367
+ timeFormat: TimeFormat
368
+ ): HourInputResult => {
369
+ // Allow empty for deletion
370
+ if (rawValue === '') {
371
+ return { value: '', hour: null, isValid: true, capped: false }
372
+ }
373
+
374
+ // Only allow digits
375
+ if (!/^\d+$/.test(rawValue)) {
376
+ return { value: rawValue, hour: null, isValid: false, capped: false }
377
+ }
378
+
379
+ // Limit to 2 characters max
380
+ if (rawValue.length > 2) {
381
+ return { value: rawValue, hour: null, isValid: false, capped: false }
382
+ }
383
+
384
+ // Parse the numeric value
385
+ const numValue = parseInt(rawValue, 10)
386
+ if (isNaN(numValue)) {
387
+ return { value: rawValue, hour: null, isValid: false, capped: false }
388
+ }
389
+
390
+ const { maxHour, minHour } = getHourConstraints(timeFormat)
391
+
392
+ // Cap at max if exceeded
393
+ if (numValue > maxHour) {
394
+ return { value: maxHour.toString(), hour: maxHour, isValid: true, capped: true }
395
+ }
396
+
397
+ // Return valid result - preserve raw value for display (leading zeros)
398
+ return {
399
+ value: rawValue,
400
+ hour: numValue >= minHour ? numValue : null,
401
+ isValid: true,
402
+ capped: false,
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Normalize hour input value on blur
408
+ * @param inputValue - The current input value
409
+ * @param currentHour - The current hour state
410
+ * @param timeFormat - The time format being used
411
+ * @returns Object with normalized hour and display value
412
+ */
413
+ export const normalizeHourOnBlur = (
414
+ inputValue: string,
415
+ currentHour: number,
416
+ timeFormat: TimeFormat
417
+ ): { hour: number; displayValue: string } => {
418
+ if (inputValue === '' || isNaN(parseInt(inputValue, 10))) {
419
+ return { hour: currentHour, displayValue: currentHour.toString() }
420
+ }
421
+
422
+ let numValue = parseInt(inputValue, 10)
423
+ const { maxHour, minHour } = getHourConstraints(timeFormat)
424
+
425
+ if (numValue > maxHour) numValue = maxHour
426
+ if (numValue < minHour) numValue = minHour
427
+
428
+ return { hour: numValue, displayValue: numValue.toString() }
429
+ }
430
+
431
+ /**
432
+ * Validate and process minute input value
433
+ * @param rawValue - The raw input value
434
+ * @returns Result object with processed value, minute number, and validation status
435
+ */
436
+ export const processMinuteInput = (rawValue: string): MinuteInputResult => {
437
+ // Allow empty for deletion
438
+ if (rawValue === '') {
439
+ return { value: '', minute: null, isValid: true, capped: false }
440
+ }
441
+
442
+ // Only allow digits
443
+ if (!/^\d+$/.test(rawValue)) {
444
+ return { value: rawValue, minute: null, isValid: false, capped: false }
445
+ }
446
+
447
+ // Limit to 2 characters max
448
+ if (rawValue.length > 2) {
449
+ return { value: rawValue, minute: null, isValid: false, capped: false }
450
+ }
451
+
452
+ // Parse the numeric value
453
+ const numValue = parseInt(rawValue, 10)
454
+ if (isNaN(numValue)) {
455
+ return { value: rawValue, minute: null, isValid: false, capped: false }
456
+ }
457
+
458
+ // Cap at 59 if exceeded
459
+ if (numValue > 59) {
460
+ return { value: '59', minute: 59, isValid: true, capped: true }
461
+ }
462
+
463
+ return {
464
+ value: rawValue,
465
+ minute: numValue >= 0 ? numValue : null,
466
+ isValid: true,
467
+ capped: false,
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Normalize minute input value on blur
473
+ * @param inputValue - The current input value
474
+ * @param currentMinute - The current minute state
475
+ * @returns Object with normalized minute and display value
476
+ */
477
+ export const normalizeMinuteOnBlur = (
478
+ inputValue: string,
479
+ currentMinute: number
480
+ ): { minute: number; displayValue: string } => {
481
+ if (inputValue === '' || isNaN(parseInt(inputValue, 10))) {
482
+ return { minute: currentMinute, displayValue: currentMinute.toString().padStart(2, '0') }
483
+ }
484
+
485
+ let numValue = parseInt(inputValue, 10)
486
+ if (numValue > 59) numValue = 59
487
+ if (numValue < 0) numValue = 0
488
+
489
+ return { minute: numValue, displayValue: numValue.toString().padStart(2, '0') }
490
+ }
491
+
492
+ // ===========================================================
493
+ // | Hour Format Conversion |
494
+ // ===========================================================
495
+
496
+ /**
497
+ * Convert hour from 12-hour to 24-hour format
498
+ * @param hour - Hour in 12-hour format (1-12)
499
+ * @param meridiem - 'AM' or 'PM'
500
+ * @returns Hour in 24-hour format (0-23)
501
+ */
502
+ export const to24Hour = (hour: number, meridiem: 'AM' | 'PM'): number => {
503
+ let hour24 = hour
504
+ if (meridiem === 'PM' && hour !== 12) hour24 = hour + 12
505
+ if (meridiem === 'AM' && hour === 12) hour24 = 0
506
+ return Math.max(0, Math.min(23, hour24))
507
+ }
508
+
509
+ /**
510
+ * Convert hour from 24-hour to 12-hour format
511
+ * @param hour24 - Hour in 24-hour format (0-23)
512
+ * @returns Object with hour (1-12) and meridiem ('AM' or 'PM')
513
+ */
514
+ export const to12Hour = (hour24: number): { hour: number; meridiem: 'AM' | 'PM' } => {
515
+ let hour = hour24
516
+ let meridiem: 'AM' | 'PM' = 'AM'
517
+
518
+ if (hour24 > 12) {
519
+ hour = hour24 % 12
520
+ meridiem = 'PM'
521
+ } else if (hour24 === 12) {
522
+ meridiem = 'PM'
523
+ } else if (hour24 === 0) {
524
+ hour = 12
525
+ meridiem = 'AM'
526
+ }
527
+
528
+ return { hour, meridiem }
529
+ }
530
+
531
+ /**
532
+ * Convert hour when switching from 12-hour to 24-hour format
533
+ * @param hour - Current hour value
534
+ * @param meridiem - Current meridiem
535
+ * @returns Conversion result with new hour and whether it changed
536
+ */
537
+ export const convertTo24HourFormat = (
538
+ hour: number,
539
+ meridiem: 'AM' | 'PM'
540
+ ): FormatConversionResult => {
541
+ let hour24 = hour
542
+ if (meridiem === 'PM' && hour !== 12) hour24 = hour + 12
543
+ if (meridiem === 'AM' && hour === 12) hour24 = 0
544
+ if (hour24 > 23) hour24 = 23
545
+ if (hour24 < 0) hour24 = 0
546
+
547
+ return {
548
+ hour: hour24,
549
+ meridiem: hour24 < 12 ? 'AM' : 'PM',
550
+ changed: hour24 !== hour,
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Convert hour when switching from 24-hour to 12-hour format
556
+ * @param hour - Current hour value (0-23)
557
+ * @param currentMeridiem - Current meridiem value
558
+ * @returns Conversion result with new hour, meridiem, and whether it changed
559
+ */
560
+ export const convertTo12HourFormat = (
561
+ hour: number,
562
+ currentMeridiem: 'AM' | 'PM'
563
+ ): FormatConversionResult => {
564
+ let hour12 = hour
565
+ let newMeridiem = currentMeridiem
566
+ let shouldUpdateMeridiem = false
567
+
568
+ if (hour12 > 12) {
569
+ hour12 = hour12 % 12
570
+ newMeridiem = 'PM'
571
+ shouldUpdateMeridiem = true
572
+ } else if (hour12 === 0) {
573
+ hour12 = 12
574
+ newMeridiem = 'AM'
575
+ shouldUpdateMeridiem = true
576
+ } else if (hour12 === 12) {
577
+ // 12 in 24-hour format is 12 PM
578
+ newMeridiem = 'PM'
579
+ shouldUpdateMeridiem = true
580
+ }
581
+
582
+ // Clamp to valid 12-hour range
583
+ if (hour12 > 12) hour12 = 12
584
+ if (hour12 < 1) hour12 = 1
585
+
586
+ return {
587
+ hour: hour12,
588
+ meridiem: shouldUpdateMeridiem ? newMeridiem : currentMeridiem,
589
+ changed: hour12 !== hour || (shouldUpdateMeridiem && newMeridiem !== currentMeridiem),
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Generate hour options based on time format
595
+ * @param timeFormat - 'AMPM' for 12-hour (1-12), '24hour' for 24-hour (0-23)
596
+ * @returns Array of valid hour numbers
597
+ */
598
+ export const generateHourOptions = (timeFormat: TimeFormat): number[] => {
599
+ if (timeFormat === '24hour') {
600
+ return Array.from({ length: 24 }, (_, i) => i) // 0-23
601
+ }
602
+ return Array.from({ length: 12 }, (_, i) => i + 1) // 1-12
603
+ }
604
+
605
+ /**
606
+ * Generate minute options (00-59)
607
+ * @returns Array of minute numbers 0-59
608
+ */
609
+ export const generateMinuteOptions = (): number[] => {
610
+ return Array.from({ length: 60 }, (_, i) => i) // 0-59
611
+ }
612
+
613
+ /**
614
+ * Determine the valid initial meridiem based on min/max time constraints
615
+ * If the parsed meridiem is invalid, returns a valid alternative
616
+ * @param parsedMeridiem - The initially parsed meridiem
617
+ * @param minTimeMinutes - Minimum time in minutes (or null)
618
+ * @param maxTimeMinutes - Maximum time in minutes (or null)
619
+ * @returns The valid meridiem to use
620
+ */
621
+ export const getValidInitialMeridiem = (
622
+ parsedMeridiem: 'AM' | 'PM',
623
+ minTimeMinutes: number | null,
624
+ maxTimeMinutes: number | null
625
+ ): 'AM' | 'PM' => {
626
+ const amValid = isAnyAMTimeValid(minTimeMinutes, maxTimeMinutes)
627
+ const pmValid = isAnyPMTimeValid(minTimeMinutes, maxTimeMinutes)
628
+
629
+ if (parsedMeridiem === 'AM' && amValid) return 'AM'
630
+ if (parsedMeridiem === 'PM' && pmValid) return 'PM'
631
+
632
+ if (!pmValid && amValid) return 'AM'
633
+ if (!amValid && pmValid) return 'PM'
634
+
635
+ return parsedMeridiem
636
+ }
637
+
638
+ export default {
639
+ parseTimeToMinutes,
640
+ parseTime,
641
+ isTimeInRange,
642
+ isHourDisabled,
643
+ isAnyAMTimeValid,
644
+ isAnyPMTimeValid,
645
+ getDisplayTime,
646
+ get24HourTime,
647
+ formatTimeForDisplay,
648
+ getTimeRangeErrorMessage,
649
+ getTimezoneText,
650
+ getHourConstraints,
651
+ processHourInput,
652
+ normalizeHourOnBlur,
653
+ processMinuteInput,
654
+ normalizeMinuteOnBlur,
655
+ to24Hour,
656
+ to12Hour,
657
+ convertTo24HourFormat,
658
+ convertTo12HourFormat,
659
+ generateHourOptions,
660
+ generateMinuteOptions,
661
+ getValidInitialMeridiem,
662
+ }
@@ -18,6 +18,7 @@ type ItemProps = {
18
18
  icon?: string,
19
19
  iconColor?: 'default' | 'royal' | 'blue' | 'purple' | 'teal' | 'red' | 'yellow' | 'green',
20
20
  lineStyle?: 'solid' | 'dotted',
21
+ showCurrentYear?: boolean,
21
22
  } & GlobalProps
22
23
 
23
24
  function isElementOfType<P>(
@@ -35,6 +36,7 @@ const TimelineItem = ({
35
36
  icon = 'user',
36
37
  iconColor = 'default',
37
38
  lineStyle = 'solid',
39
+ showCurrentYear = false,
38
40
  ...props
39
41
  }: ItemProps): React.ReactElement => {
40
42
  const timelineItemCss = buildCss('pb_timeline_item_kit', lineStyle)
@@ -73,6 +75,7 @@ const TimelineItem = ({
73
75
  <DateStacked
74
76
  align="center"
75
77
  date={date}
78
+ showCurrentYear={showCurrentYear}
76
79
  size="sm"
77
80
  />
78
81
  )}