@xh/hoist 83.1.0 → 84.0.1

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 (250) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/admin/tabs/cluster/instances/logs/levels/LogLevelDialogModel.ts +106 -10
  3. package/admin/tabs/cluster/metrics/MetricsModel.ts +3 -3
  4. package/appcontainer/AppContainerModel.ts +1 -1
  5. package/appcontainer/README.md +20 -0
  6. package/assets.d.ts +34 -0
  7. package/build/types/cmp/ag-grid/AgGrid.d.ts +8 -19
  8. package/build/types/cmp/ag-grid/AgGridModel.d.ts +18 -5
  9. package/build/types/cmp/card/Card.d.ts +9 -4
  10. package/build/types/cmp/card/CardModel.d.ts +15 -2
  11. package/build/types/cmp/chart/Chart.d.ts +2 -2
  12. package/build/types/cmp/chart/ChartModel.d.ts +11 -1
  13. package/build/types/cmp/dataview/DataView.d.ts +4 -2
  14. package/build/types/cmp/dataview/DataViewModel.d.ts +16 -4
  15. package/build/types/cmp/filter/FilterChooserModel.d.ts +7 -1
  16. package/build/types/cmp/form/Form.d.ts +2 -1
  17. package/build/types/cmp/form/FormModel.d.ts +12 -0
  18. package/build/types/cmp/form/field/BaseFieldModel.d.ts +7 -0
  19. package/build/types/cmp/form/formfieldset/FormFieldSetModel.d.ts +7 -1
  20. package/build/types/cmp/grid/GridModel.d.ts +16 -1
  21. package/build/types/cmp/grid/GridSorter.d.ts +14 -0
  22. package/build/types/cmp/grid/Types.d.ts +18 -0
  23. package/build/types/cmp/grid/columns/Column.d.ts +40 -2
  24. package/build/types/cmp/grid/columns/ColumnGroup.d.ts +10 -0
  25. package/build/types/cmp/grouping/GroupingChooserModel.d.ts +9 -2
  26. package/build/types/cmp/layout/Box.d.ts +19 -7
  27. package/build/types/cmp/layout/Frame.d.ts +17 -5
  28. package/build/types/cmp/loadingindicator/LoadingIndicator.d.ts +6 -4
  29. package/build/types/cmp/pinpad/PinPadModel.d.ts +6 -1
  30. package/build/types/cmp/spinner/Spinner.d.ts +31 -10
  31. package/build/types/cmp/tab/TabContainerModel.d.ts +11 -0
  32. package/build/types/cmp/tab/TabModel.d.ts +7 -0
  33. package/build/types/cmp/tab/Types.d.ts +4 -0
  34. package/build/types/cmp/treemap/TreeMapModel.d.ts +3 -3
  35. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +9 -0
  36. package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +22 -3
  37. package/build/types/cmp/zoneGrid/impl/ZoneMapperModel.d.ts +6 -0
  38. package/build/types/core/HoistComponent.d.ts +29 -8
  39. package/build/types/core/HoistProps.d.ts +9 -3
  40. package/build/types/core/load/LoadSpec.d.ts +1 -1
  41. package/build/types/core/persist/provider/ViewManagerProvider.d.ts +7 -0
  42. package/build/types/data/Store.d.ts +35 -1
  43. package/build/types/data/StoreSelectionModel.d.ts +18 -2
  44. package/build/types/data/cube/Cube.d.ts +26 -6
  45. package/build/types/data/cube/Query.d.ts +10 -0
  46. package/build/types/data/cube/View.d.ts +21 -2
  47. package/build/types/data/cube/aggregate/Aggregator.d.ts +13 -0
  48. package/build/types/data/cube/aggregate/AverageAggregator.d.ts +1 -0
  49. package/build/types/data/cube/aggregate/AverageStrictAggregator.d.ts +1 -0
  50. package/build/types/data/cube/aggregate/ChildCountAggregator.d.ts +1 -0
  51. package/build/types/data/cube/aggregate/LeafCountAggregator.d.ts +1 -0
  52. package/build/types/data/cube/aggregate/MaxAggregator.d.ts +1 -0
  53. package/build/types/data/cube/aggregate/MinAggregator.d.ts +1 -0
  54. package/build/types/data/cube/aggregate/NullAggregator.d.ts +1 -0
  55. package/build/types/data/cube/aggregate/SingleAggregator.d.ts +1 -0
  56. package/build/types/data/cube/aggregate/SumAggregator.d.ts +1 -0
  57. package/build/types/data/cube/aggregate/SumStrictAggregator.d.ts +1 -0
  58. package/build/types/data/cube/aggregate/UniqueAggregator.d.ts +1 -0
  59. package/build/types/data/filter/BaseFilterFieldSpec.d.ts +9 -0
  60. package/build/types/data/filter/Types.d.ts +12 -0
  61. package/build/types/desktop/cmp/button/AppMenuButton.d.ts +5 -0
  62. package/build/types/desktop/cmp/button/Button.d.ts +5 -1
  63. package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +12 -3
  64. package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +9 -0
  65. package/build/types/desktop/cmp/dock/DockViewModel.d.ts +7 -0
  66. package/build/types/desktop/cmp/filechooser/FileChooserModel.d.ts +8 -0
  67. package/build/types/desktop/cmp/grid/editors/BooleanEditor.d.ts +1 -0
  68. package/build/types/desktop/cmp/grid/editors/DateEditor.d.ts +1 -0
  69. package/build/types/desktop/cmp/grid/editors/NumberEditor.d.ts +1 -0
  70. package/build/types/desktop/cmp/grid/editors/SelectEditor.d.ts +1 -0
  71. package/build/types/desktop/cmp/grid/editors/TextAreaEditor.d.ts +1 -0
  72. package/build/types/desktop/cmp/grid/editors/TextEditor.d.ts +1 -0
  73. package/build/types/desktop/cmp/input/Picker.d.ts +1 -1
  74. package/build/types/desktop/cmp/input/SegmentedControl.d.ts +16 -2
  75. package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +7 -0
  76. package/build/types/desktop/cmp/modalsupport/ModalSupportModel.d.ts +28 -2
  77. package/build/types/desktop/cmp/panel/Panel.d.ts +5 -2
  78. package/build/types/desktop/cmp/panel/PanelModel.d.ts +12 -2
  79. package/build/types/desktop/cmp/rest/RestGrid.d.ts +10 -0
  80. package/build/types/desktop/cmp/rest/RestGridModel.d.ts +9 -1
  81. package/build/types/desktop/cmp/toolbar/Toolbar.d.ts +4 -1
  82. package/build/types/format/FormatDate.d.ts +4 -4
  83. package/build/types/icon/Icon.d.ts +3 -0
  84. package/build/types/kit/blueprint/Wrappers.d.ts +12 -1
  85. package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +8 -0
  86. package/build/types/mobile/cmp/navigator/PageModel.d.ts +7 -0
  87. package/build/types/mobile/cmp/panel/DialogPanel.d.ts +0 -2
  88. package/build/types/security/BaseOAuthClient.d.ts +9 -0
  89. package/build/types/security/authzero/AuthZeroClient.d.ts +6 -0
  90. package/build/types/security/msal/MsalClient.d.ts +6 -0
  91. package/build/types/svc/FetchService.d.ts +10 -7
  92. package/build/types/svc/TraceService.d.ts +17 -2
  93. package/build/types/utils/async/Timer.d.ts +6 -0
  94. package/build/types/utils/js/LangUtils.d.ts +1 -1
  95. package/build/types/utils/js/TestUtils.d.ts +1 -1
  96. package/build/types/utils/react/index.d.ts +0 -1
  97. package/build/types/utils/telemetry/Span.d.ts +12 -2
  98. package/cmp/ag-grid/AgGrid.ts +8 -19
  99. package/cmp/ag-grid/AgGridModel.ts +18 -5
  100. package/cmp/card/Card.ts +9 -4
  101. package/cmp/card/CardModel.ts +15 -2
  102. package/cmp/chart/Chart.ts +2 -2
  103. package/cmp/chart/ChartModel.ts +11 -1
  104. package/cmp/dataview/DataView.ts +4 -2
  105. package/cmp/dataview/DataViewModel.ts +16 -4
  106. package/cmp/filter/FilterChooserModel.ts +7 -1
  107. package/cmp/form/Form.ts +2 -1
  108. package/cmp/form/FormModel.ts +12 -0
  109. package/cmp/form/README.md +13 -0
  110. package/cmp/form/field/BaseFieldModel.ts +7 -0
  111. package/cmp/form/formfieldset/FormFieldSetModel.ts +7 -1
  112. package/cmp/grid/Grid.scss +14 -8
  113. package/cmp/grid/GridModel.ts +16 -1
  114. package/cmp/grid/GridSorter.ts +14 -0
  115. package/cmp/grid/README.md +12 -0
  116. package/cmp/grid/Types.ts +18 -0
  117. package/cmp/grid/columns/Column.ts +40 -2
  118. package/cmp/grid/columns/ColumnGroup.ts +10 -0
  119. package/cmp/grouping/GroupingChooserModel.ts +9 -2
  120. package/cmp/layout/Box.ts +19 -7
  121. package/cmp/layout/Frame.ts +17 -5
  122. package/cmp/layout/README.md +16 -21
  123. package/cmp/loadingindicator/LoadingIndicator.scss +1 -1
  124. package/cmp/loadingindicator/LoadingIndicator.ts +11 -9
  125. package/cmp/pinpad/PinPadModel.ts +6 -1
  126. package/cmp/spinner/Spinner.scss +13 -0
  127. package/cmp/spinner/Spinner.ts +58 -20
  128. package/cmp/tab/TabContainerModel.ts +11 -0
  129. package/cmp/tab/TabModel.ts +7 -0
  130. package/cmp/tab/Types.ts +4 -0
  131. package/cmp/treemap/TreeMapModel.ts +3 -3
  132. package/cmp/viewmanager/ViewManagerModel.ts +9 -0
  133. package/cmp/zoneGrid/ZoneGridModel.ts +22 -3
  134. package/cmp/zoneGrid/impl/ZoneMapperModel.ts +6 -0
  135. package/core/ExceptionHandler.ts +1 -1
  136. package/core/HoistComponent.ts +36 -11
  137. package/core/HoistProps.ts +9 -3
  138. package/core/README.md +68 -6
  139. package/core/impl/InstanceManager.ts +1 -0
  140. package/core/load/LoadSpec.ts +1 -1
  141. package/core/persist/provider/ViewManagerProvider.ts +7 -0
  142. package/data/README.md +48 -124
  143. package/data/Store.ts +35 -1
  144. package/data/StoreSelectionModel.ts +18 -2
  145. package/data/cube/Cube.ts +26 -6
  146. package/data/cube/Query.ts +10 -0
  147. package/data/cube/README.md +236 -0
  148. package/data/cube/View.ts +21 -2
  149. package/data/cube/aggregate/Aggregator.ts +13 -0
  150. package/data/cube/aggregate/AverageAggregator.ts +1 -0
  151. package/data/cube/aggregate/AverageStrictAggregator.ts +1 -0
  152. package/data/cube/aggregate/ChildCountAggregator.ts +1 -0
  153. package/data/cube/aggregate/LeafCountAggregator.ts +1 -0
  154. package/data/cube/aggregate/MaxAggregator.ts +1 -0
  155. package/data/cube/aggregate/MinAggregator.ts +1 -0
  156. package/data/cube/aggregate/NullAggregator.ts +1 -0
  157. package/data/cube/aggregate/SingleAggregator.ts +1 -0
  158. package/data/cube/aggregate/SumAggregator.ts +1 -0
  159. package/data/cube/aggregate/SumStrictAggregator.ts +1 -0
  160. package/data/cube/aggregate/UniqueAggregator.ts +1 -0
  161. package/data/filter/BaseFilterFieldSpec.ts +9 -0
  162. package/data/filter/Types.ts +12 -0
  163. package/desktop/README.md +131 -9
  164. package/desktop/appcontainer/AboutDialog.ts +2 -0
  165. package/desktop/appcontainer/Banner.ts +5 -2
  166. package/desktop/appcontainer/ChangelogDialog.ts +1 -0
  167. package/desktop/appcontainer/ExceptionDialog.ts +4 -0
  168. package/desktop/appcontainer/ExceptionDialogDetails.ts +4 -1
  169. package/desktop/appcontainer/FeedbackDialog.ts +4 -1
  170. package/desktop/appcontainer/ImpersonationBar.ts +4 -0
  171. package/desktop/appcontainer/LockoutPanel.ts +4 -1
  172. package/desktop/appcontainer/LoginPanel.ts +7 -3
  173. package/desktop/appcontainer/Message.ts +9 -3
  174. package/desktop/appcontainer/OptionsDialog.ts +3 -1
  175. package/desktop/appcontainer/VersionBar.ts +1 -0
  176. package/desktop/appcontainer/suspend/IdlePanel.ts +4 -4
  177. package/desktop/appcontainer/suspend/SuspendPanel.ts +3 -0
  178. package/desktop/cmp/button/AppMenuButton.ts +5 -0
  179. package/desktop/cmp/button/Button.ts +14 -4
  180. package/desktop/cmp/dash/README.md +14 -0
  181. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +12 -3
  182. package/desktop/cmp/dash/container/DashContainerModel.ts +9 -0
  183. package/desktop/cmp/dock/DockViewModel.ts +7 -0
  184. package/desktop/cmp/filechooser/FileChooserModel.ts +9 -2
  185. package/desktop/cmp/grid/editors/BooleanEditor.ts +1 -0
  186. package/desktop/cmp/grid/editors/DateEditor.ts +1 -0
  187. package/desktop/cmp/grid/editors/NumberEditor.ts +1 -0
  188. package/desktop/cmp/grid/editors/SelectEditor.ts +1 -0
  189. package/desktop/cmp/grid/editors/TextAreaEditor.ts +1 -0
  190. package/desktop/cmp/grid/editors/TextEditor.ts +1 -0
  191. package/desktop/cmp/input/Picker.ts +2 -2
  192. package/desktop/cmp/input/SegmentedControl.ts +20 -2
  193. package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +7 -0
  194. package/desktop/cmp/modalsupport/ModalSupportModel.ts +31 -2
  195. package/desktop/cmp/panel/Panel.ts +29 -21
  196. package/desktop/cmp/panel/PanelModel.ts +12 -2
  197. package/desktop/cmp/panel/README.md +20 -0
  198. package/desktop/cmp/rest/RestGrid.ts +10 -0
  199. package/desktop/cmp/rest/RestGridModel.ts +9 -1
  200. package/desktop/cmp/toolbar/Toolbar.ts +9 -2
  201. package/desktop/cmp/viewmanager/ViewManager.ts +1 -1
  202. package/docs/README.md +9 -4
  203. package/docs/coding-conventions.md +29 -21
  204. package/docs/doc-registry.json +31 -15
  205. package/docs/planning/docs-roadmap-log.md +11 -0
  206. package/docs/planning/docs-roadmap.md +1 -0
  207. package/docs/upgrade-notes/v84-upgrade-notes.md +136 -0
  208. package/docs/version-compatibility.md +2 -0
  209. package/format/FormatDate.ts +4 -4
  210. package/icon/Icon.ts +9 -0
  211. package/icon/README.md +62 -22
  212. package/icon/index.ts +24 -0
  213. package/kit/README.md +8 -2
  214. package/kit/blueprint/Wrappers.ts +12 -1
  215. package/mcp/README.md +47 -26
  216. package/mcp/cli/ts.ts +39 -4
  217. package/mcp/data/ts-registry.ts +57 -17
  218. package/mcp/tools/typescript.ts +32 -4
  219. package/mobile/appcontainer/AboutDialog.ts +3 -0
  220. package/mobile/appcontainer/Banner.ts +2 -0
  221. package/mobile/appcontainer/ExceptionDialog.ts +4 -0
  222. package/mobile/appcontainer/ExceptionDialogDetails.ts +1 -0
  223. package/mobile/appcontainer/FeedbackDialog.ts +4 -1
  224. package/mobile/appcontainer/ImpersonationBar.ts +2 -0
  225. package/mobile/appcontainer/LockoutPanel.ts +2 -0
  226. package/mobile/appcontainer/LoginPanel.ts +7 -3
  227. package/mobile/appcontainer/Message.ts +9 -3
  228. package/mobile/appcontainer/OptionsDialog.ts +5 -1
  229. package/mobile/appcontainer/VersionBar.ts +1 -0
  230. package/mobile/appcontainer/suspend/IdlePanel.ts +5 -6
  231. package/mobile/appcontainer/suspend/SuspendPanel.ts +3 -0
  232. package/mobile/cmp/navigator/NavigatorModel.ts +8 -0
  233. package/mobile/cmp/navigator/PageModel.ts +7 -0
  234. package/mobile/cmp/panel/DialogPanel.ts +0 -2
  235. package/package.json +11 -11
  236. package/security/BaseOAuthClient.ts +9 -0
  237. package/security/authzero/AuthZeroClient.ts +6 -0
  238. package/security/msal/MsalClient.ts +6 -0
  239. package/styles/vars.scss +14 -0
  240. package/svc/FetchService.ts +25 -15
  241. package/svc/README.md +39 -9
  242. package/svc/TraceService.ts +69 -11
  243. package/utils/README.md +0 -1
  244. package/utils/async/Timer.ts +6 -0
  245. package/utils/js/LangUtils.ts +1 -1
  246. package/utils/js/TestUtils.ts +1 -1
  247. package/utils/react/index.ts +0 -1
  248. package/utils/telemetry/Span.ts +21 -4
  249. package/build/types/utils/react/ClassName.d.ts +0 -14
  250. package/utils/react/ClassName.ts +0 -24
@@ -7,6 +7,7 @@
7
7
 
8
8
  import {Aggregator} from './Aggregator';
9
9
 
10
+ /** Always returns null. Useful for fields that should not be aggregated. */
10
11
  export class NullAggregator extends Aggregator {
11
12
  override aggregate(rows, fieldName) {
12
13
  return null;
@@ -7,6 +7,7 @@
7
7
 
8
8
  import {Aggregator} from './Aggregator';
9
9
 
10
+ /** Returns the value if there is exactly one row, otherwise null. */
10
11
  export class SingleAggregator extends Aggregator {
11
12
  override aggregate(rows, fieldName) {
12
13
  return rows.length === 1 ? rows[0].data[fieldName] : null;
@@ -7,6 +7,7 @@
7
7
 
8
8
  import {Aggregator} from './Aggregator';
9
9
 
10
+ /** Sums numeric values, skipping nulls. */
10
11
  export class SumAggregator extends Aggregator {
11
12
  override aggregate(rows, fieldName) {
12
13
  let ret = null;
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import {Aggregator} from './Aggregator';
8
8
 
9
+ /** Sums numeric values, returning null if any value is null. */
9
10
  export class SumStrictAggregator extends Aggregator {
10
11
  override aggregate(rows, fieldName) {
11
12
  let ret = null;
@@ -7,6 +7,7 @@
7
7
  import {Aggregator} from './Aggregator';
8
8
  import {isEmpty, isEqual} from 'lodash';
9
9
 
10
+ /** Returns the value if all rows share the same value, otherwise null. */
10
11
  export class UniqueAggregator extends Aggregator {
11
12
  override aggregate(rows, fieldName) {
12
13
  if (isEmpty(rows)) return null;
@@ -9,6 +9,15 @@ import {Field, FieldFilter, FieldType, FilterValueSource, genDisplayName} from '
9
9
  import {compact, isArray, isEmpty} from 'lodash';
10
10
  import {FieldFilterOperator} from './Types';
11
11
 
12
+ /**
13
+ * Base configuration for field-level filtering options - defines available operators, value
14
+ * enumeration, and display metadata. Not used directly by applications; extended by
15
+ * {@link GridFilterFieldSpecConfig} (for column-header filters via {@link GridFilterModelConfig})
16
+ * and {@link FilterChooserFieldSpecConfig} (for {@link FilterChooserModel}).
17
+ *
18
+ * @see GridFilterFieldSpec
19
+ * @see FilterChooserFieldSpec
20
+ */
12
21
  export interface BaseFilterFieldSpecConfig {
13
22
  /** Identifying field name to filter on. */
14
23
  field: string;
@@ -13,6 +13,10 @@ export type FilterLike = Filter | FilterSpec | FilterTestFn | FilterLike[];
13
13
 
14
14
  export type FilterSpec = FieldFilterSpec | FunctionFilterSpec | CompoundFilterSpec;
15
15
 
16
+ /**
17
+ * Plain-object form of a {@link FieldFilter} - a single field/operator/value condition.
18
+ * Can be passed anywhere a {@link FilterLike} is accepted.
19
+ */
16
20
  export interface FieldFilterSpec {
17
21
  /** Name of Field to filter or Field instance. */
18
22
  field: string | Field;
@@ -42,6 +46,10 @@ export type FieldFilterOperator =
42
46
  | 'includes'
43
47
  | 'excludes';
44
48
 
49
+ /**
50
+ * Plain-object form of a {@link CompoundFilter} - a group of filters joined by AND/OR.
51
+ * Can be passed anywhere a {@link FilterLike} is accepted.
52
+ */
45
53
  export interface CompoundFilterSpec {
46
54
  /** Collection of Filters or configs to create. */
47
55
  filters: FilterLike[];
@@ -52,6 +60,10 @@ export interface CompoundFilterSpec {
52
60
 
53
61
  export type CompoundFilterOperator = 'AND' | 'OR' | 'and' | 'or';
54
62
 
63
+ /**
64
+ * Plain-object form of a {@link FunctionFilter} - a custom function-based filter.
65
+ * Can be passed anywhere a {@link FilterLike} is accepted.
66
+ */
55
67
  export interface FunctionFilterSpec {
56
68
  /** Key used to identify this FunctionFilter.*/
57
69
  key: string;
package/desktop/README.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Desktop Package
2
2
 
3
+ | Section | Description |
4
+ |---------|-------------|
5
+ | [Overview](#overview) | Desktop platform, Blueprint foundation |
6
+ | [Architecture](#architecture) | Directory structure and key sub-packages |
7
+ | [Relationship to /cmp/](#relationship-to-cmp) | Platform-specific vs. cross-platform components |
8
+ | [AppContainer](#appcontainer) | Desktop app shell and lifecycle |
9
+ | [Component Sub-Packages](#component-sub-packages) | Panel, Toolbar, Button, Grid, Dashboard, Tabs, Inputs, and more |
10
+ | [Dialogs](#dialogs) | Custom modal dialogs via Blueprint Kit export |
11
+ | [Desktop Hooks](#desktop-hooks) | useContextMenu |
12
+ | [Common Patterns](#common-patterns) | Blueprint wrappers, Popover, ContextMenu |
13
+ | [Related Packages](#related-packages) | Links to cross-platform and utility packages |
14
+
3
15
  ## Overview
4
16
 
5
17
  The `/desktop/` package provides Hoist's desktop-specific UI components, incorporating the
@@ -169,20 +181,34 @@ covering toolbars, masks, collapse/resize, persistence, and modal support.
169
181
 
170
182
  ### Toolbar (`/cmp/toolbar/`)
171
183
 
172
- Container for action buttons and controls:
184
+ Horizontal (or vertical) container for action buttons and controls. Toolbars are typically
185
+ placed in a Panel's `tbar` (top) or `bbar` (bottom) slots. Use `filler()` to push items to
186
+ the right side, and `'-'` (shortcut for `toolbarSep()`) for visual dividers.
173
187
 
174
188
  ```typescript
175
- import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
189
+ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
190
+ import {filler} from '@xh/hoist/cmp/layout';
176
191
 
177
- toolbar(
178
- button({text: 'Add', icon: Icon.add()}),
179
- button({text: 'Edit', icon: Icon.edit()}),
180
- toolbarSep(),
181
- filler(),
182
- searchField()
183
- )
192
+ toolbar({
193
+ items: [
194
+ button({text: 'Add', icon: Icon.add()}),
195
+ button({text: 'Edit', icon: Icon.edit()}),
196
+ '-', // separator token — shorthand for toolbarSep()
197
+ filler(),
198
+ searchField()
199
+ ]
200
+ })
184
201
  ```
185
202
 
203
+ Key props:
204
+ - `compact` - reduced height and font size (useful in dense UIs)
205
+ - `vertical` - stack items vertically instead of horizontally
206
+ - `enableOverflowMenu` - collapse items that don't fit into a dropdown menu
207
+ - `collapseFrom` - `'start'` or `'end'` (default) for overflow direction
208
+ - `minVisibleItems` - minimum items to keep visible before overflowing
209
+
210
+ `Toolbar.defaults.compact` can be set at bootstrap for app-wide compact toolbars.
211
+
186
212
  ### Button (`/cmp/button/`)
187
213
 
188
214
  Desktop buttons with Blueprint styling:
@@ -340,6 +366,102 @@ leftRightChooser({model})
340
366
  | `/viewmanager/` | View save/load UI |
341
367
  | `/zoneGrid/` | Zone mapper for ZoneGrid columns |
342
368
 
369
+ ## Dialogs
370
+
371
+ For simple alerts and confirmations, use the built-in `XH.message()` and `XH.confirm()` methods
372
+ (see the [appcontainer README](../appcontainer/README.md#messages)).
373
+
374
+ For custom dialogs with rich content (forms, grids, etc.), use the Blueprint `dialog` element
375
+ factory from `@xh/hoist/kit/blueprint`. This is the standard Hoist pattern for modal dialogs -
376
+ Blueprint's Dialog is re-exported through Kit with transitions disabled for snappy rendering.
377
+
378
+ The typical pattern uses a dedicated model to manage the dialog's open/closed state and content,
379
+ with the parent component always rendering the dialog component alongside its other children.
380
+ The dialog renders as a Blueprint portal overlay when open and returns null when closed, so it
381
+ can be included unconditionally in the parent's item list.
382
+
383
+ ```typescript
384
+ // TaskDialogModel.ts - manages dialog open/closed state and form data
385
+ export class TaskDialogModel extends HoistModel {
386
+ @observable isOpen = false;
387
+
388
+ @managed formModel = new FormModel({
389
+ fields: [{name: 'description', rules: [required]}]
390
+ });
391
+
392
+ @action
393
+ open(initialValues?) {
394
+ this.isOpen = true;
395
+ this.formModel.init(initialValues);
396
+ }
397
+
398
+ @action
399
+ close() { this.isOpen = false; }
400
+ }
401
+
402
+ // TaskDialog.ts - the dialog component, rendered by the parent
403
+ export const taskDialog = hoistCmp.factory({
404
+ model: uses(TaskDialogModel),
405
+ render({model}) {
406
+ if (!model.isOpen) return null;
407
+ return dialog({
408
+ title: 'Edit Task',
409
+ style: {width: 500},
410
+ isOpen: true,
411
+ onClose: () => model.close(),
412
+ item: panel({
413
+ item: form(
414
+ formField({field: 'description', item: textInput()})
415
+ ),
416
+ bbar: toolbar({
417
+ items: [filler(), button({text: 'Save', intent: 'primary'})]
418
+ })
419
+ })
420
+ });
421
+ }
422
+ });
423
+
424
+ // TodoPanel.ts - the parent component that owns and renders the dialog
425
+ export class TodoPanelModel extends HoistModel {
426
+ @managed taskDialogModel = new TaskDialogModel();
427
+ @managed gridModel = new GridModel({...});
428
+ }
429
+
430
+ export const todoPanel = hoistCmp.factory({
431
+ model: creates(TodoPanelModel),
432
+ render({model}) {
433
+ return panel({
434
+ tbar: toolbar({
435
+ items: [
436
+ button({
437
+ text: 'New Task',
438
+ icon: Icon.add(),
439
+ onClick: () => model.taskDialogModel.open()
440
+ })
441
+ ]
442
+ }),
443
+ items: [
444
+ grid(),
445
+ taskDialog() // Always rendered - shows/hides based on isOpen
446
+ ]
447
+ });
448
+ }
449
+ });
450
+ ```
451
+
452
+ Key points:
453
+ - Import `dialog` from `@xh/hoist/kit/blueprint` (not from Blueprint directly) to get
454
+ Hoist's transition-disabled wrapper.
455
+ - The parent model owns the dialog model as a `@managed` property and calls `open()`/`close()`.
456
+ - The parent component renders the dialog factory in its `items` alongside other children -
457
+ the dialog shows/hides itself based on its model's `isOpen` state.
458
+ - Use `onClose` to handle the dialog's close button and outside-click-to-close.
459
+ - Wrap dialog content in a `panel()` when you need toolbars, masks, or standard layout.
460
+
461
+ Note: Hoist does not yet provide its own first-class Dialog component wrapper
462
+ ([#861](https://github.com/xh/hoist-react/issues/861)) - the Blueprint Kit re-export is the
463
+ established pattern used throughout the framework and applications.
464
+
343
465
  ## Desktop Hooks
344
466
 
345
467
  ### useContextMenu
@@ -45,6 +45,7 @@ export const aboutDialog = hoistCmp.factory({
45
45
  button({
46
46
  text: 'Send Client Health Report',
47
47
  icon: Icon.health(),
48
+ testId: 'xh-about-health-report-btn',
48
49
  omit: !XH.clientHealthService.enabled,
49
50
  onClick: async () => {
50
51
  try {
@@ -60,6 +61,7 @@ export const aboutDialog = hoistCmp.factory({
60
61
  text: 'Close',
61
62
  intent: 'primary',
62
63
  outlined: true,
64
+ testId: 'xh-about-close-btn',
63
65
  onClick: onClose
64
66
  })
65
67
  )
@@ -26,9 +26,10 @@ export const banner = hoistCmp.factory({
26
26
  model: uses(BannerModel),
27
27
 
28
28
  render({model}) {
29
- const {icon, message, intent, onClick, className} = model;
29
+ const {icon, message, intent, onClick, className, category} = model;
30
30
 
31
31
  return toolbar({
32
+ testId: `xh-banner-${category}`,
32
33
  className: classNames(
33
34
  'xh-banner',
34
35
  className,
@@ -57,12 +58,13 @@ export const banner = hoistCmp.factory({
57
58
  });
58
59
 
59
60
  const actionButton = hoistCmp.factory<BannerModel>(({model}) => {
60
- const {actionButtonProps} = model;
61
+ const {actionButtonProps, category} = model;
61
62
  if (isEmpty(actionButtonProps)) return null;
62
63
 
63
64
  return button({
64
65
  outlined: true,
65
66
  className: 'xh-banner__action-button',
67
+ testId: `xh-banner-${category}-action-btn`,
66
68
  ...actionButtonProps
67
69
  });
68
70
  });
@@ -74,6 +76,7 @@ const dismissButton = hoistCmp.factory<BannerModel>(({model}) => {
74
76
  return button({
75
77
  icon: Icon.close(),
76
78
  className: 'xh-banner__dismiss-button',
79
+ testId: `xh-banner-${category}-dismiss-btn`,
77
80
  onClick: () => {
78
81
  XH.hideBanner(category);
79
82
  if (isFunction(onClose)) onClose(model);
@@ -44,6 +44,7 @@ const changelogContents = hoistCmp.factory<ChangelogDialogModel>(({model}) => {
44
44
  button({
45
45
  text: 'Close',
46
46
  intent: 'primary',
47
+ testId: 'xh-changelog-close-btn',
47
48
  onClick: () => model.hide()
48
49
  })
49
50
  ]
@@ -62,12 +62,14 @@ const bbar = hoistCmp.factory<ExceptionDialogModel>(({model}) =>
62
62
  omit: !XH.identityService?.isImpersonating,
63
63
  icon: Icon.impersonate(),
64
64
  text: 'End Impersonation',
65
+ testId: 'xh-exception-end-impersonation-btn',
65
66
  onClick: () => XH.identityService.endImpersonateAsync()
66
67
  }),
67
68
  filler(),
68
69
  button({
69
70
  icon: Icon.search(),
70
71
  text: 'Show/Report Details',
72
+ testId: 'xh-exception-details-btn',
71
73
  onClick: () => model.openDetails(),
72
74
  omit: !model.options.showAsError
73
75
  }),
@@ -86,11 +88,13 @@ export const dismissButton = hoistCmp.factory<ExceptionDialogModel>(({model}) =>
86
88
  ? button({
87
89
  icon: Icon.refresh(),
88
90
  text: 'Reload App',
91
+ testId: 'xh-exception-dismiss-btn',
89
92
  autoFocus: true,
90
93
  onClick: () => XH.reloadApp({removeQueryParams: true})
91
94
  })
92
95
  : button({
93
96
  text: 'Close',
97
+ testId: 'xh-exception-dismiss-btn',
94
98
  autoFocus: true,
95
99
  onClick: () => model.close()
96
100
  });
@@ -62,6 +62,7 @@ export const exceptionDialogDetails = hoistCmp.factory<ExceptionDialogModel>(({m
62
62
  placeholder: 'Add message here...',
63
63
  width: '100%',
64
64
  height: 120,
65
+ testId: 'xh-exception-details-message',
65
66
  omit: !clientUserKnown
66
67
  })
67
68
  ]
@@ -71,13 +72,15 @@ export const exceptionDialogDetails = hoistCmp.factory<ExceptionDialogModel>(({m
71
72
  button({
72
73
  icon: Icon.envelope(),
73
74
  text: 'Send',
75
+ testId: 'xh-exception-details-send-btn',
74
76
  disabled: !model.userMessage,
75
77
  onClick: () => model.sendReportAsync(),
76
78
  omit: !clientUserKnown
77
79
  }),
78
80
  clipboardButton({
79
81
  getCopyText: () => errorStr,
80
- successMessage: 'Error details copied to clipboard.'
82
+ successMessage: 'Error details copied to clipboard.',
83
+ testId: 'xh-exception-details-copy-btn'
81
84
  }),
82
85
  dismissButton()
83
86
  ])
@@ -37,12 +37,14 @@ export const feedbackDialog = hoistCmp.factory({
37
37
  height: 250,
38
38
  style: {marginBottom: 2},
39
39
  commitOnChange: true,
40
- bind: 'message'
40
+ bind: 'message',
41
+ testId: 'xh-feedback-message'
41
42
  }),
42
43
  toolbar(
43
44
  filler(),
44
45
  button({
45
46
  text: 'Cancel',
47
+ testId: 'xh-feedback-cancel-btn',
46
48
  onClick: () => model.hide()
47
49
  }),
48
50
  button({
@@ -50,6 +52,7 @@ export const feedbackDialog = hoistCmp.factory({
50
52
  intent: 'success',
51
53
  minimal: false,
52
54
  disabled: !model.message,
55
+ testId: 'xh-feedback-send-btn',
53
56
  onClick: () => model.submitAsync()
54
57
  })
55
58
  )
@@ -41,6 +41,7 @@ export const impersonationBar = hoistCmp.factory({
41
41
 
42
42
  return toolbar({
43
43
  className: 'xh-impersonation-bar',
44
+ testId: 'xh-impersonation-bar',
44
45
  items: [
45
46
  Icon.impersonate(),
46
47
  span(msg),
@@ -49,6 +50,7 @@ export const impersonationBar = hoistCmp.factory({
49
50
  text: 'Important Reminders',
50
51
  icon: Icon.warning(),
51
52
  outlined: true,
53
+ testId: 'xh-impersonation-reminders-btn',
52
54
  onClick: showUseResponsiblyAlert
53
55
  }),
54
56
  hspacer(),
@@ -64,12 +66,14 @@ export const impersonationBar = hoistCmp.factory({
64
66
  maxWidth: 350,
65
67
  menuWidth: 350,
66
68
  flex: 1,
69
+ testId: 'xh-impersonation-target',
67
70
  onCommit: model.onCommit,
68
71
  ref: model.inputRef
69
72
  }),
70
73
  button({
71
74
  text: isImpersonating ? 'Exit Impersonation' : 'Cancel',
72
75
  outlined: true,
76
+ testId: 'xh-impersonation-exit-btn',
73
77
  onClick: model.onClose
74
78
  })
75
79
  ]
@@ -27,6 +27,7 @@ export const lockoutPanel = hoistCmp.factory({
27
27
  filler(),
28
28
  box({
29
29
  className: 'xh-lockout-panel',
30
+ testId: 'xh-lockout-panel',
30
31
  item: unauthorizedMessage()
31
32
  }),
32
33
  filler()
@@ -60,7 +61,8 @@ const unauthorizedMessage = hoistCmp.factory<AppContainerModel>({
60
61
  logoutButton({
61
62
  text: 'Logout',
62
63
  intent: null,
63
- minimal: false
64
+ minimal: false,
65
+ testId: 'xh-lockout-logout-btn'
64
66
  }),
65
67
  hspacer(5),
66
68
  button({
@@ -68,6 +70,7 @@ const unauthorizedMessage = hoistCmp.factory<AppContainerModel>({
68
70
  icon: Icon.impersonate(),
69
71
  text: 'End Impersonation',
70
72
  minimal: false,
73
+ testId: 'xh-lockout-end-impersonation-btn',
71
74
  onClick: () => identityService.endImpersonateAsync()
72
75
  }),
73
76
  filler()
@@ -39,6 +39,7 @@ export const loginPanel = hoistCmp.factory({
39
39
  title: XH.clientAppName,
40
40
  icon: Icon.login(),
41
41
  className: 'xh-login',
42
+ testId: 'xh-login',
42
43
  width: 300,
43
44
  mask: loadObserver,
44
45
  items: [
@@ -51,7 +52,8 @@ export const loginPanel = hoistCmp.factory({
51
52
  autoFocus: true,
52
53
  commitOnChange: true,
53
54
  onKeyDown,
54
- width: null
55
+ width: null,
56
+ testId: 'xh-login-username'
55
57
  }),
56
58
  textInput({
57
59
  bind: 'password',
@@ -60,7 +62,8 @@ export const loginPanel = hoistCmp.factory({
60
62
  type: 'password',
61
63
  commitOnChange: true,
62
64
  onKeyDown,
63
- width: null
65
+ width: null,
66
+ testId: 'xh-login-password'
64
67
  })
65
68
  ),
66
69
  div({
@@ -81,7 +84,8 @@ export const loginPanel = hoistCmp.factory({
81
84
  intent: 'primary',
82
85
  icon: Icon.login(),
83
86
  disabled: !isValid || loginInProgress,
84
- onClick: () => model.submitAsync()
87
+ onClick: () => model.submitAsync(),
88
+ testId: 'xh-login-btn'
85
89
  })
86
90
  ]
87
91
  })
@@ -52,6 +52,7 @@ const inputsCmp = hoistCmp.factory<MessageModel>(({model}) => {
52
52
  formField({
53
53
  field: 'value',
54
54
  label: null,
55
+ testId: 'xh-message-value',
55
56
  item: withDefault(
56
57
  input.item,
57
58
  textInput({
@@ -70,6 +71,7 @@ const inputsCmp = hoistCmp.factory<MessageModel>(({model}) => {
70
71
  formField({
71
72
  label: extraConfirmLabel,
72
73
  field: 'extraConfirm',
74
+ testId: 'xh-message-extra-confirm',
73
75
  item: textInput({
74
76
  autoFocus: true,
75
77
  selectOnFocus: true,
@@ -95,7 +97,7 @@ const bbar = hoistCmp.factory<MessageModel>(({model}) => {
95
97
  ret = [];
96
98
 
97
99
  if (cancelProps) {
98
- ret.push(button(cancelProps));
100
+ ret.push(button({testId: 'xh-message-cancel-btn', ...cancelProps}));
99
101
  }
100
102
 
101
103
  if (cancelAlign === 'left') {
@@ -108,8 +110,12 @@ const bbar = hoistCmp.factory<MessageModel>(({model}) => {
108
110
  // Merge in formModel.isValid here in render stage to get reactivity.
109
111
  ret.push(
110
112
  formModel
111
- ? button({...confirmProps, disabled: !formModel.isValid})
112
- : button(confirmProps)
113
+ ? button({
114
+ testId: 'xh-message-confirm-btn',
115
+ ...confirmProps,
116
+ disabled: !formModel.isValid
117
+ })
118
+ : button({testId: 'xh-message-confirm-btn', ...confirmProps})
113
119
  );
114
120
  }
115
121
 
@@ -51,10 +51,11 @@ export const optionsDialog = hoistCmp.factory({
51
51
  })
52
52
  ),
53
53
  bbar: [
54
- restoreDefaultsButton(),
54
+ restoreDefaultsButton({testId: 'xh-options-restore-defaults-btn'}),
55
55
  filler(),
56
56
  button({
57
57
  text: 'Cancel',
58
+ testId: 'xh-options-cancel-btn',
58
59
  onClick: () => model.hide()
59
60
  }),
60
61
  button({
@@ -62,6 +63,7 @@ export const optionsDialog = hoistCmp.factory({
62
63
  text: reloadRequired ? 'Save & Reload' : 'Save',
63
64
  icon: reloadRequired ? Icon.refresh() : Icon.check(),
64
65
  intent: 'success',
66
+ testId: 'xh-options-save-btn',
65
67
  onClick: () => model.saveAsync()
66
68
  })
67
69
  ]
@@ -26,6 +26,7 @@ export const versionBar = hoistCmp.factory({
26
26
 
27
27
  return box({
28
28
  className: `xh-version-bar xh-version-bar--${env.toLowerCase()}`,
29
+ testId: 'xh-version-bar',
29
30
  items: [
30
31
  [XH.appName, env, versionAndBuild].join(' • '),
31
32
  span({
@@ -4,14 +4,12 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {div, filler, img, p, viewport} from '@xh/hoist/cmp/layout';
7
8
  import {hoistCmp, XH} from '@xh/hoist/core';
8
- import {viewport, div, img, p, filler} from '@xh/hoist/cmp/layout';
9
- import {panel} from '@xh/hoist/desktop/cmp/panel';
10
9
  import {button} from '@xh/hoist/desktop/cmp/button';
10
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
11
11
  import {Icon} from '@xh/hoist/icon';
12
-
13
12
  import './IdlePanel.scss';
14
- // @ts-ignore
15
13
  import idleImage from './IdlePanelImage.png';
16
14
 
17
15
  /**
@@ -33,6 +31,7 @@ export const idlePanel = hoistCmp.factory({
33
31
  title: `${XH.clientAppName} is sleeping`,
34
32
  icon: Icon.moon(),
35
33
  className: 'xh-idle-panel',
34
+ testId: 'xh-idle-panel',
36
35
  item: div(
37
36
  img({
38
37
  src: idleImage,
@@ -49,6 +48,7 @@ export const idlePanel = hoistCmp.factory({
49
48
  intent: 'primary',
50
49
  minimal: false,
51
50
  autoFocus: true,
51
+ testId: 'xh-idle-reactivate-btn',
52
52
  onClick: onReactivate
53
53
  })
54
54
  ]
@@ -59,6 +59,7 @@ export const suspendPanel = hoistCmp.factory<AppContainerModel>({
59
59
  title,
60
60
  icon,
61
61
  className: 'xh-suspend-panel',
62
+ testId: 'xh-suspend-panel',
62
63
  item: div({
63
64
  className: 'xh-suspend-panel__inner',
64
65
  items: [
@@ -71,6 +72,7 @@ export const suspendPanel = hoistCmp.factory<AppContainerModel>({
71
72
  text: 'More Details',
72
73
  icon: Icon.detail(),
73
74
  minimal: true,
75
+ testId: 'xh-suspend-details-btn',
74
76
  omit: !exception,
75
77
  onClick: () => XH.exceptionHandler.showExceptionDetails(exception)
76
78
  }),
@@ -81,6 +83,7 @@ export const suspendPanel = hoistCmp.factory<AppContainerModel>({
81
83
  intent: 'primary',
82
84
  minimal: false,
83
85
  autoFocus: true,
86
+ testId: 'xh-suspend-reload-btn',
84
87
  onClick: () => XH.reloadApp()
85
88
  })
86
89
  ]
@@ -64,6 +64,11 @@ export interface AppMenuButtonProps extends ButtonProps {
64
64
 
65
65
  type RenderWithUserProfileCustomFn = (user: HoistUser) => ReactNode;
66
66
 
67
+ /**
68
+ * Application-level menu button rendered in the AppBar. Provides a dropdown menu with standard
69
+ * items for About, Admin, Feedback, Options, Theme, Impersonation, Changelog, and Logout.
70
+ * Individual items can be hidden via props, and custom items added via `extraItems`.
71
+ */
67
72
  export const [AppMenuButton, appMenuButton] = hoistCmp.withFactory<AppMenuButtonProps>({
68
73
  displayName: 'AppMenuButton',
69
74
  model: false,