@xh/hoist 87.1.1 → 87.2.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 (263) hide show
  1. package/.npmignore +39 -0
  2. package/CHANGELOG.md +92 -20
  3. package/admin/AdminUtils.ts +64 -0
  4. package/admin/App.scss +88 -0
  5. package/admin/columns/UserData.ts +30 -3
  6. package/admin/detail/RestDetailModel.ts +64 -0
  7. package/admin/detail/RestDetailPanel.ts +106 -0
  8. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +27 -94
  9. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +23 -60
  10. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +1 -2
  11. package/admin/tabs/general/alertBanner/AlertBannerPanel.ts +2 -13
  12. package/admin/tabs/general/config/ConfigDetailPanel.ts +72 -0
  13. package/admin/tabs/general/config/ConfigPanel.ts +41 -53
  14. package/admin/tabs/general/config/ConfigPanelModel.ts +3 -0
  15. package/admin/tabs/general/config/ConfigValue.scss +14 -0
  16. package/admin/tabs/general/config/ConfigValue.ts +104 -65
  17. package/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.ts +93 -0
  18. package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +4 -0
  19. package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +2 -0
  20. package/admin/tabs/userData/prefs/UserPreferenceDetailPanel.ts +62 -0
  21. package/admin/tabs/userData/prefs/UserPreferenceModel.ts +10 -3
  22. package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -1
  23. package/admin/tabs/userData/roles/details/RoleDetails.scss +2 -19
  24. package/admin/tabs/userData/roles/details/RoleDetails.ts +13 -13
  25. package/appcontainer/AboutDialogModel.ts +1 -0
  26. package/appcontainer/AppContainerModel.ts +3 -0
  27. package/appcontainer/AppStateModel.ts +1 -0
  28. package/appcontainer/BannerSourceModel.ts +1 -0
  29. package/appcontainer/ChangelogDialogModel.ts +1 -0
  30. package/appcontainer/ColChooserOptionsModel.ts +1 -0
  31. package/appcontainer/ExceptionDialogModel.ts +1 -0
  32. package/appcontainer/FeedbackDialogModel.ts +1 -0
  33. package/appcontainer/ImpersonationBarModel.ts +1 -0
  34. package/appcontainer/MessageSourceModel.ts +1 -0
  35. package/appcontainer/OptionsDialogModel.ts +1 -0
  36. package/appcontainer/PageStateModel.ts +1 -0
  37. package/appcontainer/RouterModel.ts +2 -0
  38. package/appcontainer/SizingModeModel.ts +1 -0
  39. package/appcontainer/ThemeModel.ts +1 -0
  40. package/appcontainer/ToastSourceModel.ts +1 -0
  41. package/appcontainer/UserAgentModel.ts +1 -0
  42. package/appcontainer/ViewportSizeModel.ts +1 -0
  43. package/build/types/admin/AdminUtils.d.ts +29 -0
  44. package/build/types/admin/columns/UserData.d.ts +3 -0
  45. package/build/types/admin/detail/RestDetailModel.d.ts +20 -0
  46. package/build/types/admin/detail/RestDetailPanel.d.ts +34 -0
  47. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +5 -17
  48. package/build/types/admin/tabs/general/config/ConfigDetailPanel.d.ts +7 -0
  49. package/build/types/admin/tabs/general/config/ConfigValue.d.ts +10 -4
  50. package/build/types/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.d.ts +3 -0
  51. package/build/types/admin/tabs/userData/prefs/UserPreferenceDetailPanel.d.ts +3 -0
  52. package/build/types/admin/tabs/userData/prefs/UserPreferenceModel.d.ts +3 -0
  53. package/build/types/appcontainer/AboutDialogModel.d.ts +1 -0
  54. package/build/types/appcontainer/AppContainerModel.d.ts +1 -0
  55. package/build/types/appcontainer/AppStateModel.d.ts +1 -0
  56. package/build/types/appcontainer/BannerSourceModel.d.ts +1 -0
  57. package/build/types/appcontainer/ChangelogDialogModel.d.ts +1 -0
  58. package/build/types/appcontainer/ColChooserOptionsModel.d.ts +1 -0
  59. package/build/types/appcontainer/ExceptionDialogModel.d.ts +1 -0
  60. package/build/types/appcontainer/FeedbackDialogModel.d.ts +1 -0
  61. package/build/types/appcontainer/ImpersonationBarModel.d.ts +1 -0
  62. package/build/types/appcontainer/MessageSourceModel.d.ts +1 -0
  63. package/build/types/appcontainer/OptionsDialogModel.d.ts +1 -0
  64. package/build/types/appcontainer/PageStateModel.d.ts +1 -0
  65. package/build/types/appcontainer/RouterModel.d.ts +1 -0
  66. package/build/types/appcontainer/SizingModeModel.d.ts +1 -0
  67. package/build/types/appcontainer/ThemeModel.d.ts +1 -0
  68. package/build/types/appcontainer/ToastSourceModel.d.ts +1 -0
  69. package/build/types/appcontainer/UserAgentModel.d.ts +1 -0
  70. package/build/types/appcontainer/ViewportSizeModel.d.ts +1 -0
  71. package/build/types/cmp/ag-grid/AgGridModel.d.ts +3 -1
  72. package/build/types/cmp/card/Card.d.ts +2 -2
  73. package/build/types/cmp/chart/ChartModel.d.ts +2 -0
  74. package/build/types/cmp/dataview/DataViewModel.d.ts +2 -0
  75. package/build/types/cmp/daterange/DateRangePickerModel.d.ts +266 -0
  76. package/build/types/cmp/daterange/DateRangePresets.d.ts +26 -0
  77. package/build/types/cmp/daterange/DateRangeUtils.d.ts +96 -0
  78. package/build/types/cmp/daterange/Types.d.ts +152 -0
  79. package/build/types/cmp/daterange/index.d.ts +4 -0
  80. package/build/types/cmp/filter/FilterChooserModel.d.ts +3 -1
  81. package/build/types/cmp/form/FormModel.d.ts +3 -1
  82. package/build/types/cmp/grid/GridModel.d.ts +2 -0
  83. package/build/types/cmp/grid/Types.d.ts +2 -0
  84. package/build/types/cmp/grid/filter/GridFilterModel.d.ts +1 -1
  85. package/build/types/cmp/grouping/GroupingChooserModel.d.ts +3 -1
  86. package/build/types/cmp/input/HoistInputProps.d.ts +49 -2
  87. package/build/types/cmp/tab/TabContainerModel.d.ts +3 -1
  88. package/build/types/cmp/tab/TabModel.d.ts +3 -1
  89. package/build/types/cmp/treemap/TreeMapModel.d.ts +2 -0
  90. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +2 -0
  91. package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +2 -0
  92. package/build/types/core/HoistBase.d.ts +7 -0
  93. package/build/types/core/HoistProps.d.ts +26 -1
  94. package/build/types/core/model/RootRefreshContextModel.d.ts +1 -0
  95. package/build/types/core/types/Telemetry.d.ts +1 -1
  96. package/build/types/data/Store.d.ts +3 -1
  97. package/build/types/data/StoreSelectionModel.d.ts +3 -1
  98. package/build/types/data/cube/Cube.d.ts +5 -2
  99. package/build/types/data/cube/View.d.ts +2 -0
  100. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -0
  101. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -0
  102. package/build/types/desktop/cmp/button/Button.d.ts +2 -2
  103. package/build/types/desktop/cmp/button/ButtonGroup.d.ts +2 -2
  104. package/build/types/desktop/cmp/dash/DashConfig.d.ts +2 -0
  105. package/build/types/desktop/cmp/dash/DashViewModel.d.ts +3 -1
  106. package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +1 -1
  107. package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -1
  108. package/build/types/desktop/cmp/daterange/DateRangePicker.d.ts +66 -0
  109. package/build/types/desktop/cmp/daterange/impl/CustomTab.d.ts +3 -0
  110. package/build/types/desktop/cmp/daterange/impl/DateRangePickerLocalModel.d.ts +121 -0
  111. package/build/types/desktop/cmp/daterange/impl/PeriodTab.d.ts +2 -0
  112. package/build/types/desktop/cmp/daterange/impl/PresetsTab.d.ts +3 -0
  113. package/build/types/desktop/cmp/daterange/impl/RelativeTab.d.ts +3 -0
  114. package/build/types/desktop/cmp/daterange/impl/TabUtils.d.ts +21 -0
  115. package/build/types/desktop/cmp/daterange/index.d.ts +2 -0
  116. package/build/types/desktop/cmp/dock/DockContainerModel.d.ts +3 -1
  117. package/build/types/desktop/cmp/dock/DockViewModel.d.ts +3 -1
  118. package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +4 -1
  119. package/build/types/desktop/cmp/input/IntentInput.d.ts +38 -0
  120. package/build/types/desktop/cmp/input/NumberInput.d.ts +2 -2
  121. package/build/types/desktop/cmp/input/SegmentedControl.d.ts +25 -4
  122. package/build/types/desktop/cmp/input/TextArea.d.ts +2 -2
  123. package/build/types/desktop/cmp/input/TextInput.d.ts +2 -2
  124. package/build/types/desktop/cmp/input/index.d.ts +1 -0
  125. package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +3 -1
  126. package/build/types/desktop/cmp/panel/PanelModel.d.ts +3 -1
  127. package/build/types/desktop/cmp/rest/RestGridModel.d.ts +1 -1
  128. package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
  129. package/build/types/icon/Icon.d.ts +3 -0
  130. package/build/types/inspector/impl/InspectorUtils.d.ts +3 -0
  131. package/build/types/inspector/instances/InstancesModel.d.ts +4 -0
  132. package/build/types/kit/react-day-picker/index.d.ts +3 -3
  133. package/build/types/mobile/cmp/button/Button.d.ts +2 -2
  134. package/build/types/mobile/cmp/input/NumberInput.d.ts +2 -2
  135. package/build/types/mobile/cmp/input/SegmentedControl.d.ts +18 -4
  136. package/build/types/mobile/cmp/input/TextArea.d.ts +2 -2
  137. package/build/types/mobile/cmp/input/TextInput.d.ts +2 -2
  138. package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +3 -1
  139. package/build/types/mobile/cmp/navigator/PageModel.d.ts +3 -1
  140. package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
  141. package/build/types/svc/InspectorService.d.ts +1 -0
  142. package/build/types/utils/js/LangUtils.d.ts +24 -5
  143. package/cmp/README.md +1 -0
  144. package/cmp/ag-grid/AgGridModel.ts +5 -0
  145. package/cmp/badge/Badge.ts +5 -3
  146. package/cmp/card/Card.ts +4 -3
  147. package/cmp/chart/ChartModel.ts +11 -1
  148. package/cmp/dataview/DataViewModel.ts +7 -1
  149. package/cmp/daterange/DateRangePickerModel.ts +628 -0
  150. package/cmp/daterange/DateRangePresets.ts +274 -0
  151. package/cmp/daterange/DateRangeUtils.ts +557 -0
  152. package/cmp/daterange/README.md +434 -0
  153. package/cmp/daterange/Types.ts +203 -0
  154. package/cmp/daterange/index.ts +10 -0
  155. package/cmp/filter/FilterChooserModel.ts +7 -4
  156. package/cmp/form/FormModel.ts +6 -0
  157. package/cmp/form/README.md +1 -0
  158. package/cmp/grid/GridModel.ts +24 -4
  159. package/cmp/grid/Types.ts +3 -0
  160. package/cmp/grid/filter/GridFilterModel.ts +3 -1
  161. package/cmp/grouping/GroupingChooserModel.ts +6 -1
  162. package/cmp/input/HoistInputProps.ts +50 -2
  163. package/cmp/input/README.md +42 -0
  164. package/cmp/layout/Box.ts +3 -2
  165. package/cmp/tab/TabContainerModel.ts +9 -1
  166. package/cmp/tab/TabModel.ts +5 -0
  167. package/cmp/treemap/SplitTreeMapModel.ts +12 -2
  168. package/cmp/treemap/TreeMapModel.ts +6 -1
  169. package/cmp/viewmanager/ViewManagerModel.ts +6 -1
  170. package/cmp/zoneGrid/ZoneGridModel.ts +7 -0
  171. package/core/HoistBase.ts +11 -0
  172. package/core/HoistProps.ts +27 -0
  173. package/core/README.md +46 -0
  174. package/core/impl/InstallServices.ts +5 -1
  175. package/core/model/RootRefreshContextModel.ts +2 -0
  176. package/core/types/Telemetry.ts +1 -1
  177. package/data/README.md +3 -1
  178. package/data/Store.ts +7 -0
  179. package/data/StoreSelectionModel.ts +5 -1
  180. package/data/cube/Cube.ts +12 -3
  181. package/data/cube/View.ts +5 -1
  182. package/desktop/README.md +2 -0
  183. package/desktop/appcontainer/LoginPanel.ts +2 -0
  184. package/desktop/cmp/button/Button.scss +71 -42
  185. package/desktop/cmp/button/Button.ts +10 -1
  186. package/desktop/cmp/button/ButtonGroup.ts +4 -1
  187. package/desktop/cmp/button/grid/ColChooserButton.ts +9 -1
  188. package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -0
  189. package/desktop/cmp/dash/DashConfig.ts +3 -0
  190. package/desktop/cmp/dash/DashViewModel.ts +14 -1
  191. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +3 -1
  192. package/desktop/cmp/dash/container/DashContainerModel.ts +3 -1
  193. package/desktop/cmp/daterange/DateRangePicker.scss +577 -0
  194. package/desktop/cmp/daterange/DateRangePicker.ts +357 -0
  195. package/desktop/cmp/daterange/impl/CustomTab.ts +165 -0
  196. package/desktop/cmp/daterange/impl/DateRangePickerLocalModel.ts +482 -0
  197. package/desktop/cmp/daterange/impl/PeriodTab.ts +138 -0
  198. package/desktop/cmp/daterange/impl/PresetsTab.ts +58 -0
  199. package/desktop/cmp/daterange/impl/RelativeTab.ts +108 -0
  200. package/desktop/cmp/daterange/impl/TabUtils.ts +34 -0
  201. package/desktop/cmp/daterange/index.ts +8 -0
  202. package/desktop/cmp/dock/DockContainerModel.ts +6 -1
  203. package/desktop/cmp/dock/DockViewModel.ts +6 -1
  204. package/desktop/cmp/filechooser/FileChooser.ts +4 -0
  205. package/desktop/cmp/form/FormField.ts +1 -0
  206. package/desktop/cmp/grouping/GroupingChooser.ts +8 -2
  207. package/desktop/cmp/input/Checkbox.ts +1 -0
  208. package/desktop/cmp/input/CodeInput.ts +1 -0
  209. package/desktop/cmp/input/DateInput.scss +1 -1
  210. package/desktop/cmp/input/DateInput.ts +1 -0
  211. package/desktop/cmp/input/IntentInput.scss +202 -0
  212. package/desktop/cmp/input/IntentInput.ts +213 -0
  213. package/desktop/cmp/input/NumberInput.ts +11 -2
  214. package/desktop/cmp/input/Picker.ts +1 -0
  215. package/desktop/cmp/input/RadioInput.ts +2 -1
  216. package/desktop/cmp/input/SegmentedControl.scss +45 -2
  217. package/desktop/cmp/input/SegmentedControl.ts +66 -11
  218. package/desktop/cmp/input/Select.ts +9 -7
  219. package/desktop/cmp/input/Slider.ts +2 -0
  220. package/desktop/cmp/input/SwitchInput.ts +1 -0
  221. package/desktop/cmp/input/TextArea.ts +11 -2
  222. package/desktop/cmp/input/TextInput.ts +11 -2
  223. package/desktop/cmp/input/index.ts +1 -0
  224. package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +5 -0
  225. package/desktop/cmp/panel/Panel.ts +8 -5
  226. package/desktop/cmp/panel/PanelModel.ts +8 -0
  227. package/desktop/cmp/panel/impl/ResizeContainer.ts +2 -1
  228. package/desktop/cmp/rest/RestGridModel.ts +4 -0
  229. package/desktop/cmp/tab/impl/TabContainer.ts +2 -0
  230. package/docs/README.md +2 -0
  231. package/docs/build-and-publish.md +23 -7
  232. package/docs/doc-registry.json +8 -0
  233. package/docs/version-compatibility.md +2 -0
  234. package/icon/Icon.ts +9 -0
  235. package/icon/index.ts +24 -0
  236. package/inspector/README.md +8 -2
  237. package/inspector/impl/InspectorUtils.ts +12 -0
  238. package/inspector/instances/DiagnosticsModel.ts +3 -2
  239. package/inspector/instances/InstancesModel.ts +81 -14
  240. package/inspector/instances/InstancesPanel.ts +6 -0
  241. package/kit/react-day-picker/index.ts +3 -3
  242. package/mobile/appcontainer/LoginPanel.ts +2 -0
  243. package/mobile/cmp/button/Button.ts +16 -3
  244. package/mobile/cmp/form/FormField.ts +1 -0
  245. package/mobile/cmp/input/Checkbox.ts +1 -0
  246. package/mobile/cmp/input/DateInput.ts +1 -0
  247. package/mobile/cmp/input/Label.ts +13 -8
  248. package/mobile/cmp/input/NumberInput.ts +11 -2
  249. package/mobile/cmp/input/SearchInput.ts +1 -0
  250. package/mobile/cmp/input/SegmentedControl.scss +36 -6
  251. package/mobile/cmp/input/SegmentedControl.ts +37 -11
  252. package/mobile/cmp/input/Select.ts +2 -0
  253. package/mobile/cmp/input/SwitchInput.ts +1 -0
  254. package/mobile/cmp/input/TextArea.ts +11 -2
  255. package/mobile/cmp/input/TextInput.ts +11 -2
  256. package/mobile/cmp/navigator/NavigatorModel.ts +6 -1
  257. package/mobile/cmp/navigator/PageModel.ts +6 -1
  258. package/mobile/cmp/tab/impl/TabContainer.ts +9 -1
  259. package/package.json +19 -19
  260. package/styles/vars.scss +32 -2
  261. package/svc/InspectorService.ts +2 -0
  262. package/svc/TraceService.ts +5 -2
  263. package/utils/js/LangUtils.ts +37 -5
@@ -0,0 +1,357 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import {DateRangePickerModel} from '@xh/hoist/cmp/daterange';
8
+ import {
9
+ DateRangePickerLocalModel,
10
+ type DateRangePickerTabSpec
11
+ } from './impl/DateRangePickerLocalModel';
12
+ import {box, div, filler, fragment, hbox, span, vbox} from '@xh/hoist/cmp/layout';
13
+ import {tabContainer} from '@xh/hoist/cmp/tab';
14
+ import {
15
+ hoistCmp,
16
+ type HoistProps,
17
+ type Intent,
18
+ type LayoutProps,
19
+ type TestSupportProps,
20
+ useLocalModel,
21
+ uses
22
+ } from '@xh/hoist/core';
23
+ import {button, type ButtonProps} from '@xh/hoist/desktop/cmp/button';
24
+ import '@xh/hoist/desktop/register';
25
+ import {Icon} from '@xh/hoist/icon';
26
+ import {popover} from '@xh/hoist/kit/blueprint';
27
+ import type {PopoverPosition} from '@blueprintjs/core';
28
+ import {elemWithin, getTestId} from '@xh/hoist/utils/js';
29
+ import {composeRefs, splitLayoutProps, useOnResize} from '@xh/hoist/utils/react';
30
+ import classNames from 'classnames';
31
+ import type {KeyboardEvent, ReactNode} from 'react';
32
+ import {customTab} from './impl/CustomTab';
33
+ import {periodTab} from './impl/PeriodTab';
34
+ import {presetsTab} from './impl/PresetsTab';
35
+ import {relativeTab} from './impl/RelativeTab';
36
+ import './DateRangePicker.scss';
37
+
38
+ export interface DateRangePickerProps
39
+ extends HoistProps<DateRangePickerModel>, LayoutProps, TestSupportProps {
40
+ /**
41
+ * Props forwarded to the trigger button - use to customize its `icon`, `intent`, `minimal`,
42
+ * `outlined`, `tooltip`, or any other {@link ButtonProps}.
43
+ */
44
+ buttonProps?: Partial<ButtonProps>;
45
+
46
+ /**
47
+ * Text rendered in the popover footer, or null to suppress. Default is a note on the date that
48
+ * periods are relative to. Not shown by a single-tab picker, whose footer carries the resolved
49
+ * dates instead.
50
+ */
51
+ footerNote?: ReactNode;
52
+
53
+ /**
54
+ * Intent for the popover's selection accent - the highlight on selected presets, months,
55
+ * and calendar days, and the Apply button. Also applied to the trigger button unless
56
+ * overridden via `buttonProps`. Default: the primary intent for the accent, none for the
57
+ * trigger.
58
+ */
59
+ intent?: Intent;
60
+
61
+ /**
62
+ * Placement of the popover relative to the trigger. Default 'bottom-left', which flips to
63
+ * 'top-left' when there is no room below - e.g. from a bottom toolbar - and holds the popover's
64
+ * left edge to the trigger as the trigger's label changes width.
65
+ */
66
+ popoverPosition?: PopoverPosition;
67
+
68
+ /**
69
+ * True (default) to show the resolved dates beside the period label on the trigger. The dates
70
+ * are also dropped automatically when a stretched trigger (e.g. `flex: 1` within a narrow
71
+ * dashboard widget) measures too narrow to fit them - either way, they remain available via
72
+ * the trigger's tooltip.
73
+ */
74
+ showRange?: boolean;
75
+
76
+ /**
77
+ * True to flank the trigger with previous/next buttons that move the applied range by its own
78
+ * length - see {@link DateRangePickerModel.stepRange}. Default false.
79
+ */
80
+ showStepButtons?: boolean;
81
+
82
+ /**
83
+ * True (default) to style trigger button background and borders to match inputs. Set to
84
+ * false to render an outlined button instead.
85
+ */
86
+ styleButtonAsInput?: boolean;
87
+ }
88
+
89
+ /**
90
+ * A dropdown control for selecting a period of time - one compact trigger that can express
91
+ * presets (e.g. MTD, Prev 30 Days), relative lookbacks, calendar months, quarters and years, and
92
+ * custom ranges of dates.
93
+ *
94
+ * The trigger shows the applied period's label with its resolved dates, and opens a popover with a
95
+ * tab for each of those selection shapes. The backing {@link DateRangePickerModel}'s `tabs` config
96
+ * selects which tabs show. Preset and period picks commit on click. Relative and custom picks are
97
+ * drafts until Apply, held in a local model that never touches the applied value.
98
+ *
99
+ * App code constructs and persists the {@link DateRangePickerModel}, which owns the applied value
100
+ * and the ranges and filters it resolves to - see that class for the full API.
101
+ *
102
+ * @see DateRangePickerModel
103
+ */
104
+ export const [DateRangePicker, dateRangePicker] = hoistCmp.withFactory<DateRangePickerProps>({
105
+ displayName: 'DateRangePicker',
106
+ model: uses(DateRangePickerModel),
107
+ className: 'xh-date-range-picker',
108
+
109
+ render(
110
+ {
111
+ model,
112
+ className,
113
+ buttonProps,
114
+ footerNote,
115
+ intent,
116
+ popoverPosition = 'bottom-left',
117
+ showRange = true,
118
+ showStepButtons = false,
119
+ styleButtonAsInput = true,
120
+ testId,
121
+ ...props
122
+ },
123
+ ref
124
+ ) {
125
+ const impl = useLocalModel(() => new DateRangePickerLocalModel(testId, TAB_SPECS)),
126
+ [layoutProps] = splitLayoutProps(props),
127
+ {singleTab} = impl,
128
+ // A stretched trigger adapts to its measured width - a content-sized toolbar trigger
129
+ // never truncates, so it is not measured at all.
130
+ {flex, width} = layoutProps,
131
+ stretched =
132
+ (flex != null && flex !== 0 && flex !== '0' && flex !== 'none') || width != null,
133
+ compact = stretched && impl.measuredWidth != null && impl.measuredWidth < 280,
134
+ resizeRef = useOnResize(rect => (impl.measuredWidth = rect.width), {debounce: 100}),
135
+ viewRef = stretched ? composeRefs(impl.viewRef, resizeRef) : impl.viewRef;
136
+
137
+ impl.intent = intent;
138
+
139
+ return box({
140
+ className: classNames(className, compact && 'xh-date-range-picker--compact'),
141
+ ref: ref ? composeRefs(ref, viewRef) : viewRef,
142
+ ...layoutProps,
143
+ items: [
144
+ stepButton({
145
+ omit: !showStepButtons,
146
+ model: impl,
147
+ steps: -1,
148
+ styleButtonAsInput,
149
+ testId
150
+ }),
151
+ popover({
152
+ isOpen: impl.isOpen,
153
+ position: popoverPosition,
154
+ minimal: false,
155
+ popoverClassName: classNames(
156
+ 'xh-date-range-picker-popover',
157
+ singleTab && 'xh-date-range-picker-popover--single',
158
+ intent && `xh-date-range-picker-popover--intent-${intent}`
159
+ ),
160
+ item: trigger({
161
+ model: impl,
162
+ testId,
163
+ intent,
164
+ showRange: showRange && !compact,
165
+ styleButtonAsInput,
166
+ buttonProps
167
+ }),
168
+ content: popoverContent({model: impl, testId, intent, footerNote}),
169
+ onInteraction: (willOpen, e) => {
170
+ if (!willOpen && impl.isOpen) {
171
+ // Clicks on this instance's own trigger toggle via its onClick - don't
172
+ // double-close. Anything else, including this picker's own step
173
+ // buttons, closes the popover so it never shows drafts seeded from a
174
+ // stale value.
175
+ const target = e?.target as HTMLElement,
176
+ onTrigger =
177
+ target && elemWithin(target, 'xh-date-range-picker__trigger');
178
+ if (!onTrigger) impl.close();
179
+ }
180
+ }
181
+ }),
182
+ stepButton({
183
+ omit: !showStepButtons,
184
+ model: impl,
185
+ steps: 1,
186
+ styleButtonAsInput,
187
+ testId
188
+ })
189
+ ]
190
+ });
191
+ }
192
+ });
193
+
194
+ //------------------
195
+ // Step buttons
196
+ //------------------
197
+ const stepButton = hoistCmp.factory<DateRangePickerLocalModel>(
198
+ ({model, steps, styleButtonAsInput, testId}) => {
199
+ const {parentModel} = model,
200
+ back = steps < 0;
201
+ return button({
202
+ className: 'xh-date-range-picker__step-btn',
203
+ icon: back ? Icon.angleLeft() : Icon.angleRight(),
204
+ ...(styleButtonAsInput ? {minimal: true} : {outlined: true}),
205
+ title: back ? 'Previous period' : 'Next period',
206
+ disabled: back ? !parentModel.canStepBack : !parentModel.canStepForward,
207
+ testId: getTestId(testId, back ? 'step-back' : 'step-forward'),
208
+ onClick: () => parentModel.stepRange(steps)
209
+ });
210
+ }
211
+ );
212
+
213
+ //------------------
214
+ // Trigger
215
+ //------------------
216
+ const trigger = hoistCmp.factory<DateRangePickerLocalModel>(
217
+ ({model, testId, intent, showRange, styleButtonAsInput, buttonProps}) => {
218
+ const {parentModel} = model,
219
+ {label, rangeLabel, labelNeedsDates} = parentModel,
220
+ // Without its dates, `Custom` or `As Of` says nothing - show the dates in their place.
221
+ text = !showRange && labelNeedsDates ? rangeLabel : label,
222
+ base = 'xh-date-range-picker__trigger';
223
+
224
+ return button({
225
+ // Input mode relies on the `--as-input` class for its chrome, so renders minimal.
226
+ ...(styleButtonAsInput ? {minimal: true} : {outlined: true}),
227
+ intent,
228
+ icon: Icon.calendar(),
229
+ title: `${label} | ${rangeLabel}`,
230
+ ...buttonProps,
231
+ className: classNames(
232
+ base,
233
+ styleButtonAsInput && `${base}--as-input`,
234
+ buttonProps?.className
235
+ ),
236
+ active: model.isOpen,
237
+ testId: getTestId(testId, 'trigger'),
238
+ items: [
239
+ span({className: `${base}-label`, item: text}),
240
+ span({omit: !showRange, className: `${base}-divider`, item: '|'}),
241
+ span({omit: !showRange, className: `${base}-range`, item: rangeLabel})
242
+ ],
243
+ onClick: () => model.toggleOpen(),
244
+ // Arrow keys step the period without opening the popover.
245
+ onKeyDown: (e: KeyboardEvent) => {
246
+ const steps = e.key === 'ArrowLeft' ? -1 : e.key === 'ArrowRight' ? 1 : 0;
247
+ if (steps) {
248
+ e.preventDefault();
249
+ parentModel.stepRange(steps);
250
+ }
251
+ }
252
+ });
253
+ }
254
+ );
255
+
256
+ //------------------
257
+ // Popover shell
258
+ //------------------
259
+ const popoverContent = hoistCmp.factory<DateRangePickerLocalModel>(
260
+ ({model, testId, intent, footerNote}) => {
261
+ return vbox({
262
+ className: 'xh-date-range-picker-popover__content',
263
+ testId: getTestId(testId, 'popover'),
264
+ onKeyDown: e => {
265
+ // Enter applies the active draft - except on buttons, whose Enter is their click.
266
+ const onButton = !!(e.target as HTMLElement).closest('button');
267
+ if (e.key === 'Enter' && model.applyEnabled && !onButton) model.apply();
268
+ },
269
+ items: [
270
+ tabContainer({
271
+ className: 'xh-date-range-picker-popover__body',
272
+ model: model.tabModel,
273
+ // Switcher testId seeds each tab's own - `<testId>-rail-<tabId>`.
274
+ switcher: model.singleTab
275
+ ? false
276
+ : {orientation: 'left', testId: getTestId(testId, 'rail')}
277
+ }),
278
+ footer({model, testId, intent, footerNote})
279
+ ]
280
+ });
281
+ }
282
+ );
283
+
284
+ //------------------
285
+ // Footer
286
+ //------------------
287
+ const footer = hoistCmp.factory<DateRangePickerLocalModel>(
288
+ ({model, testId, intent, footerNote}) => {
289
+ const {parentModel, singleTab} = model,
290
+ // Explicit null suppresses the note; undefined takes the default.
291
+ note = footerNote === undefined ? anchorNote({model}) : footerNote,
292
+ showNote = !singleTab && note != null && note !== false,
293
+ // Apply and Cancel only where there is a draft to apply - presets and periods commit
294
+ // on click, and drafts commit as they change under `commitOnChange`.
295
+ showControls = model.showApplyControls && model.applyEnabled;
296
+
297
+ // A single tab with nothing to apply - or that applies as it changes - needs no buttons.
298
+ if (singleTab && !showControls) {
299
+ return div({
300
+ className: 'xh-date-range-picker-popover__single-footer',
301
+ item: parentModel.rangeLabel
302
+ });
303
+ }
304
+
305
+ if (!showNote && !showControls) return null;
306
+
307
+ return hbox({
308
+ className: 'xh-date-range-picker-popover__footer',
309
+ items: [
310
+ Icon.info({
311
+ omit: !showNote,
312
+ className: 'xh-date-range-picker-popover__footer-icon'
313
+ }),
314
+ div({
315
+ omit: !showNote,
316
+ className: 'xh-date-range-picker-popover__footer-note',
317
+ item: note
318
+ }),
319
+ filler(),
320
+ button({
321
+ omit: !showControls,
322
+ text: 'Cancel',
323
+ testId: getTestId(testId, 'cancel'),
324
+ onClick: () => model.close()
325
+ }),
326
+ button({
327
+ omit: !showControls,
328
+ text: 'Apply',
329
+ outlined: true,
330
+ intent: intent ?? 'primary',
331
+ testId: getTestId(testId, 'apply'),
332
+ onClick: () => model.apply()
333
+ })
334
+ ]
335
+ });
336
+ }
337
+ );
338
+
339
+ /** The default footer note - muted prose, with the date it names set apart as on the trigger. */
340
+ const anchorNote = hoistCmp.factory<DateRangePickerLocalModel>(({model}) => {
341
+ const {prefix, date} = model.anchorNote;
342
+ return fragment(
343
+ prefix,
344
+ span({className: 'xh-date-range-picker-popover__footer-date', item: date})
345
+ );
346
+ });
347
+
348
+ //------------------
349
+ // Tabs
350
+ //------------------
351
+ /** Switcher entry + content for each tab. The model's `tabs` config selects and orders them. */
352
+ const TAB_SPECS: DateRangePickerTabSpec[] = [
353
+ {id: 'presets', title: 'Presets', icon: Icon.bolt(), content: presetsTab},
354
+ {id: 'relative', title: 'Relative', icon: Icon.history(), content: relativeTab},
355
+ {id: 'period', title: 'Months & Years', icon: Icon.calendarDays(), content: periodTab},
356
+ {id: 'custom', title: 'Custom Range', icon: Icon.calendarRange(), content: customTab}
357
+ ];
@@ -0,0 +1,165 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import type {DateRangePickerLocalModel} from './DateRangePickerLocalModel';
8
+ import {div, hbox, span, vbox} from '@xh/hoist/cmp/layout';
9
+ import {hoistCmp, type HoistProps} from '@xh/hoist/core';
10
+ import {button} from '@xh/hoist/desktop/cmp/button';
11
+ import {dateInput} from '@xh/hoist/desktop/cmp/input';
12
+ import {Icon} from '@xh/hoist/icon';
13
+ import type {LocalDate} from '@xh/hoist/utils/datetime';
14
+ import {getTestId} from '@xh/hoist/utils/js';
15
+ import classNames from 'classnames';
16
+ import {isString} from 'lodash';
17
+ import {clickable, dataAttrs} from './TabUtils';
18
+
19
+ /** Custom tab - start and end inputs over a two-month calendar, drafted until applied. */
20
+ export const customTab = hoistCmp.factory<DateRangePickerLocalModel>(({model, testId}) => {
21
+ const {parentModel, minDate, maxDate, leftMonth, rightMonth} = model;
22
+
23
+ const field = (edge: 'start' | 'end') =>
24
+ vbox({
25
+ className: classNames(
26
+ 'xh-date-range-picker-popover__field',
27
+ model.nextEdge === edge && 'xh-date-range-picker-popover__field--armed'
28
+ ),
29
+ items: [
30
+ div({
31
+ className: 'xh-date-range-picker-popover__field-label',
32
+ item: edge === 'start' ? 'Start' : 'End'
33
+ }),
34
+ dateInput({
35
+ value: edge === 'start' ? model.customStart : model.customEnd,
36
+ onCommit: v =>
37
+ edge === 'start' ? model.commitStartInput(v) : model.commitEndInput(v),
38
+ valueType: 'localDate',
39
+ // Inputs need a parseable string format - a function format cannot be typed into.
40
+ formatString: isString(parentModel.dateFormat)
41
+ ? parentModel.dateFormat
42
+ : 'YYYY-MM-DD',
43
+ enablePicker: false,
44
+ rightElement: null,
45
+ width: '100%',
46
+ minDate,
47
+ maxDate,
48
+ testId: getTestId(testId, `${edge}-input`)
49
+ })
50
+ ]
51
+ });
52
+
53
+ return vbox({
54
+ className: 'xh-date-range-picker-popover__tab',
55
+ items: [
56
+ hbox({
57
+ className: 'xh-date-range-picker-popover__custom-fields',
58
+ items: [
59
+ field('start'),
60
+ Icon.arrowRight({className: 'xh-date-range-picker-popover__field-arrow'}),
61
+ field('end')
62
+ ]
63
+ }),
64
+ hbox({
65
+ className: 'xh-date-range-picker-popover__cal-nav',
66
+ items: [
67
+ button({
68
+ className: 'xh-date-range-picker-popover__nav-btn',
69
+ icon: Icon.angleLeft(),
70
+ minimal: true,
71
+ disabled: !model.canCalBack,
72
+ testId: getTestId(testId, 'cal-back'),
73
+ onClick: () => model.stepCalendar(-1)
74
+ }),
75
+ span({
76
+ className: 'xh-date-range-picker-popover__cal-title',
77
+ item: leftMonth.format('MMMM YYYY')
78
+ }),
79
+ span({
80
+ className: 'xh-date-range-picker-popover__cal-title',
81
+ item: rightMonth.format('MMMM YYYY')
82
+ }),
83
+ button({
84
+ className: 'xh-date-range-picker-popover__nav-btn',
85
+ icon: Icon.angleRight(),
86
+ minimal: true,
87
+ disabled: !model.canCalForward,
88
+ testId: getTestId(testId, 'cal-forward'),
89
+ onClick: () => model.stepCalendar(1)
90
+ })
91
+ ]
92
+ }),
93
+ hbox({
94
+ className: 'xh-date-range-picker-popover__cals',
95
+ items: [
96
+ calendarMonth({model, monthStart: leftMonth}),
97
+ calendarMonth({model, monthStart: rightMonth})
98
+ ]
99
+ })
100
+ ]
101
+ });
102
+ });
103
+
104
+ const DOW_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
105
+
106
+ interface CalendarMonthProps extends HoistProps<DateRangePickerLocalModel> {
107
+ monthStart: LocalDate;
108
+ }
109
+
110
+ const calendarMonth = hoistCmp.factory<CalendarMonthProps>(({model, monthStart}) => {
111
+ const {customStart, customEnd} = model;
112
+
113
+ return div({
114
+ className: 'xh-date-range-picker-popover__cal',
115
+ items: [
116
+ div({
117
+ className: 'xh-date-range-picker-popover__cal-dow',
118
+ items: DOW_LABELS.map((it, idx) => span({key: idx, item: it}))
119
+ }),
120
+ ...buildWeeks(monthStart).map((week, wIdx) =>
121
+ div({
122
+ key: wIdx,
123
+ className: 'xh-date-range-picker-popover__cal-week',
124
+ items: week.map((day, dIdx) => {
125
+ if (!day) {
126
+ return div({
127
+ key: dIdx,
128
+ className: 'xh-date-range-picker-popover__cal-cell'
129
+ });
130
+ }
131
+ const disabled = model.isDayDisabled(day),
132
+ edge = day === customStart || day === customEnd,
133
+ inRange = day > customStart && day < customEnd;
134
+ return div({
135
+ key: dIdx,
136
+ className: classNames(
137
+ 'xh-date-range-picker-popover__cal-cell',
138
+ edge && 'xh-date-range-picker-popover__cal-cell--edge',
139
+ inRange && 'xh-date-range-picker-popover__cal-cell--in-range',
140
+ disabled && 'xh-date-range-picker-popover__cal-cell--disabled'
141
+ ),
142
+ ...dataAttrs({'data-date': day.isoString}),
143
+ item: day.format('D'),
144
+ ...clickable(disabled ? null : () => model.pickDay(day))
145
+ });
146
+ })
147
+ })
148
+ )
149
+ ]
150
+ });
151
+ });
152
+
153
+ function buildWeeks(monthStart: LocalDate): (LocalDate | null)[][] {
154
+ const startOffset = monthStart.moment.day(),
155
+ daysInMonth = monthStart.endOfMonth().diff(monthStart, 'days') + 1,
156
+ cells: (LocalDate | null)[] = [];
157
+
158
+ for (let i = 0; i < startOffset; i++) cells.push(null);
159
+ for (let d = 0; d < daysInMonth; d++) cells.push(monthStart.add(d, 'days'));
160
+ while (cells.length % 7 !== 0) cells.push(null);
161
+
162
+ const weeks = [];
163
+ for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
164
+ return weeks;
165
+ }