@xh/hoist 87.1.1 → 87.3.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 (270) hide show
  1. package/.npmignore +39 -0
  2. package/CHANGELOG.md +108 -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/grid/impl/ColumnWidthCalculator.d.ts +1 -1
  86. package/build/types/cmp/grouping/GroupingChooserModel.d.ts +3 -1
  87. package/build/types/cmp/input/HoistInputProps.d.ts +49 -2
  88. package/build/types/cmp/tab/TabContainerModel.d.ts +3 -1
  89. package/build/types/cmp/tab/TabModel.d.ts +3 -1
  90. package/build/types/cmp/treemap/TreeMapModel.d.ts +2 -0
  91. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +2 -0
  92. package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +2 -0
  93. package/build/types/core/HoistBase.d.ts +7 -0
  94. package/build/types/core/HoistProps.d.ts +26 -1
  95. package/build/types/core/model/RootRefreshContextModel.d.ts +1 -0
  96. package/build/types/core/types/Telemetry.d.ts +1 -1
  97. package/build/types/data/Store.d.ts +3 -1
  98. package/build/types/data/StoreSelectionModel.d.ts +3 -1
  99. package/build/types/data/cube/Cube.d.ts +5 -2
  100. package/build/types/data/cube/View.d.ts +2 -0
  101. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -0
  102. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -0
  103. package/build/types/desktop/cmp/button/Button.d.ts +2 -2
  104. package/build/types/desktop/cmp/button/ButtonGroup.d.ts +2 -2
  105. package/build/types/desktop/cmp/dash/DashConfig.d.ts +2 -0
  106. package/build/types/desktop/cmp/dash/DashViewModel.d.ts +3 -1
  107. package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +1 -1
  108. package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -1
  109. package/build/types/desktop/cmp/daterange/DateRangePicker.d.ts +66 -0
  110. package/build/types/desktop/cmp/daterange/impl/CustomTab.d.ts +3 -0
  111. package/build/types/desktop/cmp/daterange/impl/DateRangePickerLocalModel.d.ts +121 -0
  112. package/build/types/desktop/cmp/daterange/impl/PeriodTab.d.ts +2 -0
  113. package/build/types/desktop/cmp/daterange/impl/PresetsTab.d.ts +3 -0
  114. package/build/types/desktop/cmp/daterange/impl/RelativeTab.d.ts +3 -0
  115. package/build/types/desktop/cmp/daterange/impl/TabUtils.d.ts +21 -0
  116. package/build/types/desktop/cmp/daterange/index.d.ts +2 -0
  117. package/build/types/desktop/cmp/dock/DockContainerModel.d.ts +3 -1
  118. package/build/types/desktop/cmp/dock/DockViewModel.d.ts +3 -1
  119. package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +4 -1
  120. package/build/types/desktop/cmp/input/IntentInput.d.ts +38 -0
  121. package/build/types/desktop/cmp/input/NumberInput.d.ts +2 -2
  122. package/build/types/desktop/cmp/input/SegmentedControl.d.ts +25 -4
  123. package/build/types/desktop/cmp/input/TextArea.d.ts +2 -2
  124. package/build/types/desktop/cmp/input/TextInput.d.ts +2 -2
  125. package/build/types/desktop/cmp/input/index.d.ts +1 -0
  126. package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +3 -1
  127. package/build/types/desktop/cmp/panel/PanelModel.d.ts +3 -1
  128. package/build/types/desktop/cmp/rest/RestGridModel.d.ts +1 -1
  129. package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
  130. package/build/types/desktop/hooks/UseHotkeys.d.ts +4 -3
  131. package/build/types/icon/Icon.d.ts +3 -0
  132. package/build/types/inspector/impl/InspectorUtils.d.ts +3 -0
  133. package/build/types/inspector/instances/InstancesModel.d.ts +4 -0
  134. package/build/types/kit/react-day-picker/index.d.ts +3 -3
  135. package/build/types/mobile/cmp/button/Button.d.ts +2 -2
  136. package/build/types/mobile/cmp/input/NumberInput.d.ts +2 -2
  137. package/build/types/mobile/cmp/input/SegmentedControl.d.ts +18 -4
  138. package/build/types/mobile/cmp/input/TextArea.d.ts +2 -2
  139. package/build/types/mobile/cmp/input/TextInput.d.ts +2 -2
  140. package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +8 -2
  141. package/build/types/mobile/cmp/navigator/PageModel.d.ts +3 -1
  142. package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
  143. package/build/types/svc/InspectorService.d.ts +1 -0
  144. package/build/types/utils/js/LangUtils.d.ts +24 -5
  145. package/cmp/README.md +1 -0
  146. package/cmp/ag-grid/AgGridModel.ts +5 -0
  147. package/cmp/badge/Badge.ts +5 -3
  148. package/cmp/card/Card.ts +4 -3
  149. package/cmp/chart/ChartModel.ts +11 -1
  150. package/cmp/clock/Clock.ts +1 -1
  151. package/cmp/dataview/DataViewModel.ts +7 -1
  152. package/cmp/daterange/DateRangePickerModel.ts +628 -0
  153. package/cmp/daterange/DateRangePresets.ts +274 -0
  154. package/cmp/daterange/DateRangeUtils.ts +557 -0
  155. package/cmp/daterange/README.md +434 -0
  156. package/cmp/daterange/Types.ts +203 -0
  157. package/cmp/daterange/index.ts +10 -0
  158. package/cmp/filter/FilterChooserModel.ts +7 -4
  159. package/cmp/form/FormModel.ts +6 -0
  160. package/cmp/form/README.md +1 -0
  161. package/cmp/grid/GridModel.ts +24 -4
  162. package/cmp/grid/Types.ts +3 -0
  163. package/cmp/grid/filter/GridFilterModel.ts +3 -1
  164. package/cmp/grid/impl/ColumnWidthCalculator.ts +5 -10
  165. package/cmp/grouping/GroupingChooserModel.ts +6 -1
  166. package/cmp/input/HoistInputProps.ts +50 -2
  167. package/cmp/input/README.md +42 -0
  168. package/cmp/layout/Box.ts +3 -2
  169. package/cmp/tab/TabContainerModel.ts +9 -1
  170. package/cmp/tab/TabModel.ts +5 -0
  171. package/cmp/treemap/SplitTreeMapModel.ts +12 -2
  172. package/cmp/treemap/TreeMapModel.ts +6 -1
  173. package/cmp/viewmanager/ViewManagerModel.ts +6 -1
  174. package/cmp/zoneGrid/ZoneGridModel.ts +7 -0
  175. package/core/HoistBase.ts +11 -0
  176. package/core/HoistProps.ts +27 -0
  177. package/core/README.md +46 -0
  178. package/core/impl/InstallServices.ts +5 -1
  179. package/core/model/RootRefreshContextModel.ts +2 -0
  180. package/core/types/Telemetry.ts +1 -1
  181. package/data/README.md +3 -1
  182. package/data/Store.ts +7 -0
  183. package/data/StoreSelectionModel.ts +5 -1
  184. package/data/cube/Cube.ts +12 -3
  185. package/data/cube/View.ts +5 -1
  186. package/desktop/README.md +2 -0
  187. package/desktop/appcontainer/LoginPanel.ts +2 -0
  188. package/desktop/cmp/button/Button.scss +71 -42
  189. package/desktop/cmp/button/Button.ts +10 -1
  190. package/desktop/cmp/button/ButtonGroup.ts +4 -1
  191. package/desktop/cmp/button/grid/ColChooserButton.ts +9 -1
  192. package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -0
  193. package/desktop/cmp/dash/DashConfig.ts +3 -0
  194. package/desktop/cmp/dash/DashViewModel.ts +14 -1
  195. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +3 -1
  196. package/desktop/cmp/dash/container/DashContainerModel.ts +3 -1
  197. package/desktop/cmp/daterange/DateRangePicker.scss +577 -0
  198. package/desktop/cmp/daterange/DateRangePicker.ts +357 -0
  199. package/desktop/cmp/daterange/impl/CustomTab.ts +165 -0
  200. package/desktop/cmp/daterange/impl/DateRangePickerLocalModel.ts +482 -0
  201. package/desktop/cmp/daterange/impl/PeriodTab.ts +138 -0
  202. package/desktop/cmp/daterange/impl/PresetsTab.ts +58 -0
  203. package/desktop/cmp/daterange/impl/RelativeTab.ts +108 -0
  204. package/desktop/cmp/daterange/impl/TabUtils.ts +34 -0
  205. package/desktop/cmp/daterange/index.ts +8 -0
  206. package/desktop/cmp/dock/DockContainerModel.ts +6 -1
  207. package/desktop/cmp/dock/DockViewModel.ts +6 -1
  208. package/desktop/cmp/filechooser/FileChooser.ts +4 -0
  209. package/desktop/cmp/form/FormField.ts +1 -0
  210. package/desktop/cmp/grid/editors/DateEditor.ts +11 -2
  211. package/desktop/cmp/grouping/GroupingChooser.ts +8 -2
  212. package/desktop/cmp/input/Checkbox.ts +1 -0
  213. package/desktop/cmp/input/CodeInput.ts +1 -0
  214. package/desktop/cmp/input/DateInput.scss +1 -1
  215. package/desktop/cmp/input/DateInput.ts +1 -0
  216. package/desktop/cmp/input/IntentInput.scss +202 -0
  217. package/desktop/cmp/input/IntentInput.ts +213 -0
  218. package/desktop/cmp/input/NumberInput.ts +11 -2
  219. package/desktop/cmp/input/Picker.ts +1 -0
  220. package/desktop/cmp/input/RadioInput.ts +2 -1
  221. package/desktop/cmp/input/SegmentedControl.scss +57 -5
  222. package/desktop/cmp/input/SegmentedControl.ts +66 -11
  223. package/desktop/cmp/input/Select.ts +9 -7
  224. package/desktop/cmp/input/Slider.ts +2 -0
  225. package/desktop/cmp/input/SwitchInput.ts +1 -0
  226. package/desktop/cmp/input/TextArea.ts +11 -2
  227. package/desktop/cmp/input/TextInput.ts +11 -2
  228. package/desktop/cmp/input/index.ts +1 -0
  229. package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +5 -0
  230. package/desktop/cmp/panel/Panel.ts +8 -5
  231. package/desktop/cmp/panel/PanelModel.ts +8 -0
  232. package/desktop/cmp/panel/impl/ResizeContainer.ts +2 -1
  233. package/desktop/cmp/rest/RestGridModel.ts +4 -0
  234. package/desktop/cmp/tab/impl/TabContainer.ts +2 -0
  235. package/desktop/cmp/toolbar/Toolbar.scss +20 -6
  236. package/desktop/hooks/UseHotkeys.ts +41 -12
  237. package/docs/README.md +2 -0
  238. package/docs/build-and-publish.md +23 -7
  239. package/docs/doc-registry.json +8 -0
  240. package/docs/version-compatibility.md +2 -0
  241. package/icon/Icon.ts +9 -0
  242. package/icon/index.ts +24 -0
  243. package/inspector/README.md +8 -2
  244. package/inspector/impl/InspectorUtils.ts +12 -0
  245. package/inspector/instances/DiagnosticsModel.ts +3 -2
  246. package/inspector/instances/InstancesModel.ts +81 -14
  247. package/inspector/instances/InstancesPanel.ts +6 -0
  248. package/kit/react-day-picker/index.ts +3 -3
  249. package/mobile/appcontainer/LoginPanel.ts +2 -0
  250. package/mobile/cmp/button/Button.ts +16 -3
  251. package/mobile/cmp/form/FormField.ts +1 -0
  252. package/mobile/cmp/input/Checkbox.ts +1 -0
  253. package/mobile/cmp/input/DateInput.ts +1 -0
  254. package/mobile/cmp/input/Label.ts +13 -8
  255. package/mobile/cmp/input/NumberInput.ts +11 -2
  256. package/mobile/cmp/input/SearchInput.ts +1 -0
  257. package/mobile/cmp/input/SegmentedControl.scss +36 -6
  258. package/mobile/cmp/input/SegmentedControl.ts +37 -11
  259. package/mobile/cmp/input/Select.ts +2 -0
  260. package/mobile/cmp/input/SwitchInput.ts +1 -0
  261. package/mobile/cmp/input/TextArea.ts +11 -2
  262. package/mobile/cmp/input/TextInput.ts +11 -2
  263. package/mobile/cmp/navigator/NavigatorModel.ts +20 -8
  264. package/mobile/cmp/navigator/PageModel.ts +6 -1
  265. package/mobile/cmp/tab/impl/TabContainer.ts +9 -1
  266. package/package.json +19 -19
  267. package/styles/vars.scss +32 -2
  268. package/svc/InspectorService.ts +2 -0
  269. package/svc/TraceService.ts +5 -2
  270. package/utils/js/LangUtils.ts +37 -5
package/.npmignore ADDED
@@ -0,0 +1,39 @@
1
+ # Development tooling directories
2
+ .claude
3
+ .github
4
+ .husky
5
+ .idea
6
+ .planning
7
+ node_modules
8
+ scripts
9
+
10
+ # Development configuration
11
+ *.log
12
+ *.spec.ts
13
+ *.spec.d.ts
14
+ *.tgz
15
+ .DS_Store
16
+ .editorconfig
17
+ .gitignore
18
+ .mcp.json
19
+ .npmrc
20
+ .nvmrc
21
+ .prettierignore
22
+ .prettierrc.json
23
+ .stylelintrc
24
+ context7.json
25
+ eslint.config.js
26
+ pnpm-lock.yaml
27
+ pnpm-workspace.yaml
28
+ tsconfig.tsbuildinfo
29
+ webpack.config.intellij.js
30
+
31
+ # Development documentation
32
+ AGENTS.md
33
+ CLAUDE.md
34
+ CLAUDE.local.md
35
+ SECURITY.md
36
+
37
+ # Internal docs - not served by the MCP doc registry, not useful to consumers
38
+ docs/planning
39
+ docs/archive
package/CHANGELOG.md CHANGED
@@ -12,6 +12,95 @@
12
12
  3. Plain ASCII punctuation only. Use " - " for in-sentence breaks, never an em dash.
13
13
  -->
14
14
 
15
+ ## 87.3.0 - 2026-09-10
16
+
17
+ ### 🐞 Bug Fixes
18
+
19
+ * Fixed `SegmentedControl` clipping an option's label when `equalSegmentWidths` divided the tray
20
+ too narrowly - options now hold their own content width, sharing equally only where there is room.
21
+ * Fixed `useHotkeys()` throwing a React hook-count error when its arguments changed across
22
+ renders - a collapsible `Panel` given `hotkeys` no longer crashes when first expanded. Hotkeys
23
+ may now also be changed after the first render.
24
+ * Fixed mobile `Navigator` back-navigation leaving the page stack and the route permanently out of
25
+ sync when a stale `allowSlidePrev` caused Swiper to silently skip the transition.
26
+ * Fixed `RadioInput` and `Checkbox` options overhanging the top of the `Toolbar` - they
27
+ are now centered within the compact item height, sharing one rule with `SwitchInput`.
28
+ * Fixed an unlabeled `Slider` (`labelRenderer: false`) sitting high in a compact `Toolbar`.
29
+ * Fixed `SegmentedControl` overflowing a compact `Toolbar` - it now takes its compact sizing from
30
+ the toolbar, without needing its own `compact: true`.
31
+ * Fixed `dateEditor` crashing when opening its picker on a column backed by a `localDate` field -
32
+ the editor now defaults its `valueType` from the Store field type.
33
+
34
+ ## 87.2.0 - 2026-09-08
35
+
36
+ ### 🎁 New Features
37
+
38
+ * Added `DateRangePicker` (`desktop/cmp/daterange`), a compact control for selecting a period as a
39
+ preset (Today, MTD, Prev 30 Days, ...), a relative lookback, a calendar month or year, or a custom
40
+ date range. Its `DateRangePickerModel` persists the selection as JSON and resolves it to date
41
+ ranges and `FieldFilterSpec`s.
42
+ * Added `IntentInput` (desktop), a compact input for selecting a Hoist `Intent`.
43
+ * Added `SegmentedControl.showOptionDividers` and `SegmentedControl.showTrayBackground` (desktop and
44
+ mobile) to provide more structure by default to the input control with easy options to customize.
45
+ * Added `SegmentedControl.equalSegmentWidths` to the desktop control, matching the existing mobile
46
+ prop. Defaults to `true`, so a filled control now divides its width into equal segments rather
47
+ than sizing each option to its own label.
48
+ * Added `HoistBase.xhName`, an optional developer-facing name for an instance, shown in log output,
49
+ trace spans (new `xh.name` tag), and the Inspector. Set it on any Hoist model config to tell peers
50
+ of the same class apart - instances log as `ClassName [xhName]`, or `ClassName [id]` when unnamed.
51
+ Hoist names services, `XH.appModel`, and models created by a named parent automatically.
52
+ * Added read-only detail panels to the Admin Console's Config, User Preferences, and JSON Blobs
53
+ tabs. The Config panel shows every view of a config's value - resolved, instance override,
54
+ database, and typedClass defaults - and renders notes as Markdown. Editing is now confined to the
55
+ grid's editor: double-click no longer opens a view-only dialog for read-only admins.
56
+ * Added `domAttrs`, a prop for applying arbitrary `data-*` and `aria-*` attributes to the primary
57
+ DOM element a component renders - the same element that receives `data-testid` from `testId`.
58
+ Supported by `Box` and the layout components built on it (and therefore `Panel`, `Toolbar`, and
59
+ similar containers), `Button`, `ButtonGroup`, `Card`, `Badge`, `FormField`, and the desktop and
60
+ mobile inputs.
61
+ * Added `enablePasswordManagers` to `TextInput`, `TextArea`, and `NumberInput` (desktop and mobile).
62
+ Defaults to `false`, applying `data-1p-ignore`, `data-lpignore`, and `data-bwignore` so password
63
+ managers stop offering saved logins on ordinary data-entry fields. Apps with hand-rolled login
64
+ forms should set it `true` on their credential inputs - Hoist's own `LoginPanel` already does.
65
+
66
+ ### 🐞 Bug Fixes
67
+
68
+ * Fixed `testId` being silently dropped by desktop `Slider`, desktop `FileChooser`, and mobile
69
+ `Label` - all accepted the prop but never emitted a `data-testid` attribute.
70
+ * Fixed icon misalignment in desktop `DateInput` when a `leftIcon` is specified.
71
+ * Fixed `SegmentedControl.fill: false` leaving an empty run of tray to the right of its options -
72
+ the control now sizes to its options.
73
+ * Fixed desktop `SegmentedControl` keeping the last clicked segment highlighted after its bound
74
+ value was cleared programmatically - the control now renders no selection, with `aria-checked`
75
+ cleared, whenever its value matches no option.
76
+ * Fixed desktop `Select` not reliably scrolling the selected option into view when opening its
77
+ menu - a regression from the v86 react-select upgrade. Selects with `enableFilter: false` never
78
+ scrolled; others did so intermittently.
79
+ * `Store` and `Cube` now throw a clear error at construction when given fields with duplicate
80
+ names. Previously such a `Cube` failed later with a cryptic `Cannot redefine property` error when
81
+ creating a `View` that exposes leaves.
82
+
83
+ ### ✨ Styles
84
+
85
+ * Desktop `Button` active styling now also keys on Blueprint's `bp6-active` class, so any button
86
+ that opens an *uncontrolled* popover renders in its `active` state while that popover is showing,
87
+ with no per-component code. This brings Hoist's own menu triggers into line (`AppMenuButton`,
88
+ `ExpandToLevelButton`, `DashCanvasAddViewButton`, `DashContainerMenuButton`, and the `Toolbar`,
89
+ `TabSwitcher`, and `DashCanvas` overflow menus) and applies to app buttons as well. A button that
90
+ needs full control of its appearance can opt out by taking ownership of its popover's `isOpen`.
91
+ * `SegmentedControl.outlined` now adds a border to the tray without also removing its background,
92
+ and defaults to `true`. Pair with `showTrayBackground: false` for the previous appearance.
93
+ * Restyled the `SegmentedControl` tray to draw its background from `--xh-bg-alt` in both themes,
94
+ replacing a bespoke blue-grey mix that read heavier than the surrounding theme.
95
+ * Added `--xh-date-range-picker-*` CSS variables for the new `DateRangePicker`.
96
+ * The desktop `GroupingChooser` trigger button now renders `outlined` when in button mode
97
+ (`styleButtonAsInput: false`), matching the `ViewManager` trigger.
98
+
99
+ ### 📚 Libraries
100
+
101
+ * @daypicker/react `added @ 10.0` (replaces react-day-picker under its new package name)
102
+ * react-day-picker `removed`
103
+
15
104
  ## 87.1.1 - 2026-09-02
16
105
 
17
106
  ### 🐞 Bug Fixes
@@ -37,12 +126,11 @@
37
126
 
38
127
  * Upgraded `react-dropzone` to v20, which drops its UMD build and ships as an ESM + CJS package with
39
128
  an `exports` map. Requires Node >= 22 to install.
40
- * Added an opt-in `enforceValueInOptions` prop to the desktop and mobile `Select`, constraining the
41
- value to the current `options` and dropping any selection no longer found there. Enforced once
42
- `options` is non-null, so pass null while options load.
43
129
  * Extended the package `sideEffects` declaration to cover the vendored golden-layout implementation
44
130
  and the barrels with registration or configuration side effects on import - icon, mobx, blueprint
45
- kit, golden-layout kit, and persist. Required by the tree-shaking in hoist-dev-utils v15.
131
+ kit, golden-layout kit, and persist. ~~Required by the tree-shaking in hoist-dev-utils v15.~~
132
+ (Tree-shaking reverted in dev-utils 15.0.1 - take 15.0.1+, see
133
+ [#4640](https://github.com/xh/hoist-react/issues/4640).)
46
134
  * Restructured `PersistenceProvider` provider registration to remove a base/subclass import cycle
47
135
  that breaks under tree-shaking bundlers' re-export optimization. No API change.
48
136
 
@@ -133,11 +221,11 @@ and performance optimizations, grouped by topic below.
133
221
 
134
222
  #### Grid - Data Update Timing
135
223
 
136
- * Grids now always apply `Store` data changes to ag-Grid in a fresh macrotask - pending UI
137
- updates (e.g. load masks) paint first, and rapid changes coalesce. This strengthens an existing
138
- requirement: grid reads after a data change were already subject to a minimal async debounce
139
- and should already route through `GridModel.whenReadyAsync()`, which now provides a hard
140
- guarantee that all store data has been applied to ag-Grid.
224
+ * Grids now always apply `Store` data changes to ag-Grid in a fresh macrotask - pending UI updates
225
+ (e.g. load masks) paint first, and rapid changes coalesce. This strengthens an existing
226
+ requirement: grid reads after a data change were already subject to a minimal async debounce and
227
+ should already route through `GridModel.whenReadyAsync()`, which now provides a hard guarantee
228
+ that all store data has been applied to ag-Grid.
141
229
  * Grids now pace update-driven re-sorts and managed autosizes off their own measured cost, instead
142
230
  of re-running them every tick. Managed autosize still runs immediately on a `Store` load or filter
143
231
  change. Tune via the `deferredSortFactor` and `deferredAutosizeFactor` experimental flags.
@@ -242,13 +330,13 @@ columns.
242
330
  scrolled to the `top`, `middle`, or `bottom` of the viewport, instead of scrolling only the
243
331
  minimum amount required.
244
332
  * Improved `Grid` data update performance with tiered ag-Grid transaction handling. Update
245
- transactions that provably cannot affect row order, grouping, or tree structure now skip
246
- ag-Grid's model refresh entirely, and transactions that would re-order rows apply their cell
247
- values immediately, with row order restored by a managed, idle-scheduled re-sort. New records
248
- still sort into place on arrival.
333
+ transactions that provably cannot affect row order, grouping, or tree structure now skip ag-Grid's
334
+ model refresh entirely, and transactions that would re-order rows apply their cell values
335
+ immediately, with row order restored by a managed, idle-scheduled re-sort. New records still sort
336
+ into place on arrival.
249
337
  * Added `StoreTransaction.changedFields`, letting data producers assert exactly which fields a
250
- value-only update touched. Cube `View`s supply this automatically, extending the no-re-sort
251
- Grid fast path to view-connected stores. See the data package README for details.
338
+ value-only update touched. Cube `View`s supply this automatically, extending the no-re-sort Grid
339
+ fast path to view-connected stores. See the data package README for details.
252
340
 
253
341
  #### Admin Console
254
342
 
@@ -293,8 +381,8 @@ columns.
293
381
  stores, and adds a Diagnostics panel - a live readout of the data-pipeline `diagnostics`
294
382
  published by selected Stores, Cubes, Cube Views, and GridModels, with controls to reset counters
295
383
  and stream ops to the devtools console.
296
- * Added `XH.getCubes()` and `XH.getViews()` to enumerate all active `Cube` and `View` instances,
297
- now registered with Hoist's instance registry.
384
+ * Added `XH.getCubes()` and `XH.getViews()` to enumerate all active `Cube` and `View` instances, now
385
+ registered with Hoist's instance registry.
298
386
  * Added `useComposedRefs` - a hook variant of `composeRefs` that manages identity via `useCallback`
299
387
  and forwards React 19 ref-callback cleanups. Prefer it when composing refs within a component
300
388
  render function.
@@ -358,9 +446,9 @@ columns.
358
446
  * Removed the deprecated webpack-only `~` prefix from bare-module SCSS imports and the `inter-ui`
359
447
  font-path URL in framework styles. Modern sass-loader and css-loader resolve the same package
360
448
  paths without it, and the prefix breaks under other bundlers.
361
- * Replaced `GridExperimentalFlags.deltaSort` with `deltaSortRatio` - Hoist now manages ag-Grid
362
- delta sorting automatically, using it for re-sorts touching fewer than this percentage of rows
363
- (default 50). See the Grid transaction handling entry under New Features.
449
+ * Replaced `GridExperimentalFlags.deltaSort` with `deltaSortRatio` - Hoist now manages ag-Grid delta
450
+ sorting automatically, using it for re-sorts touching fewer than this percentage of rows (default
451
+ 50). See the Grid transaction handling entry under New Features.
364
452
  * Added `GridExperimentalFlags.deferredSortFactor` to tune the pacing of the managed re-sort on
365
453
  updating grids - a re-sort costing E ms defers the next for `E * factor` (default 4). Set 0 to
366
454
  disable deferral and re-sort synchronously on every change.
@@ -5,8 +5,14 @@
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {AppModel} from '@xh/hoist/admin/AppModel';
8
+ import {div, span} from '@xh/hoist/cmp/layout';
9
+ import {markdown} from '@xh/hoist/cmp/markdown';
8
10
  import {XH} from '@xh/hoist/core';
11
+ import {defaultReadonlyRenderer} from '@xh/hoist/desktop/cmp/form';
12
+ import {jsonInput} from '@xh/hoist/desktop/cmp/input';
9
13
  import {LocalDate} from '@xh/hoist/utils/datetime';
14
+ import {isString} from 'lodash';
15
+ import {ReactElement, ReactNode} from 'react';
10
16
 
11
17
  /**
12
18
  * Generate a standardized filename for an Admin module grid export, without datestamp.
@@ -26,3 +32,61 @@ export function exportFilenameWithDate(moduleName: string): () => string {
26
32
  export function getAppModel<T extends AppModel>() {
27
33
  return XH.appModel as T;
28
34
  }
35
+
36
+ /** Muted "N/A" placeholder for empty values in read-only detail forms. */
37
+ export function naSpan(): ReactNode {
38
+ return span({item: 'N/A', className: 'xh-text-color-muted'});
39
+ }
40
+
41
+ /**
42
+ * Default readonly renderer for detail forms - FormField's standard formatting (dates, numbers,
43
+ * booleans, JSON, text), or {@link naSpan} when null.
44
+ */
45
+ export function valOrNa(v: any): ReactNode {
46
+ return v != null ? defaultReadonlyRenderer(v) : naSpan();
47
+ }
48
+
49
+ /**
50
+ * Read-only JsonInput for detail displays - fixed to `height` if given, otherwise filling its
51
+ * container. Pair fill mode with the `xh-admin-readonly-form__fill` class on the enclosing
52
+ * `formFieldSet` to have it take the remaining panel height.
53
+ */
54
+ export function readonlyJsonInput(value: any, height: number = null): ReactElement {
55
+ return jsonInput({
56
+ value: isString(value) ? value : JSON.stringify(value),
57
+ readonly: true,
58
+ autoFormat: true,
59
+ enableSearch: true,
60
+ width: '100%',
61
+ ...(height != null ? {height} : {flex: 1, height: '100%'})
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Readonly renderer for stored scalar values (config / preference values) - the value as literal
67
+ * text, with no number or date formatting, or {@link naSpan} when null.
68
+ */
69
+ export function rawValueRenderer(v: any): ReactNode {
70
+ return v == null ? naSpan() : v.toString();
71
+ }
72
+
73
+ /** Readonly renderer for JSON fields in detail forms - {@link readonlyJsonInput} in fill mode. */
74
+ export function jsonRenderer(v: any): ReactNode {
75
+ return v == null ? naSpan() : readonlyJsonInput(v);
76
+ }
77
+
78
+ /** Readonly renderer for boolean fields in detail forms - "Yes" / "No", or {@link naSpan} when null. */
79
+ export function yesNoRenderer(v: boolean): ReactNode {
80
+ return v == null ? naSpan() : v ? 'Yes' : 'No';
81
+ }
82
+
83
+ /**
84
+ * Readonly renderer for free-text fields in detail forms that may contain Markdown (notes,
85
+ * descriptions). Renders via {@link markdown} within a wrapper styled for compact display, or
86
+ * {@link naSpan} when null.
87
+ */
88
+ export function markdownRenderer(v: string): ReactNode {
89
+ return v == null
90
+ ? naSpan()
91
+ : div({className: 'xh-admin-readonly-form__markdown', item: markdown({content: v})});
92
+ }
package/admin/App.scss CHANGED
@@ -23,6 +23,94 @@
23
23
  background-color: var(--xh-intent-primary-darker);
24
24
  font-family: var(--xh-font-family-mono);
25
25
  }
26
+
27
+ // Value type badges - shown in grid cells and detail forms, so drop Badge's inline-text offset.
28
+ // Fixed width keeps a column of mixed types from looking ragged.
29
+ .xh-value-type-badge {
30
+ --xh-badge-font-size: 0.85em;
31
+ --xh-badge-font-weight: normal;
32
+ margin-left: 0;
33
+ width: 64px;
34
+ }
35
+ }
36
+
37
+ // Read-only detail forms within the Admin Console - stacked fields with small, muted, uppercase
38
+ // labels sitting directly above their values. Apply to the container of a read-only `form`.
39
+ // Styled entirely via the framework's form-field CSS vars - see `styles/vars.scss`.
40
+ .xh-admin-readonly-form {
41
+ --xh-form-field-margin: 0 0 var(--xh-pad-half-px) 0;
42
+ --xh-form-field-label-color: var(--xh-text-color-muted);
43
+ --xh-form-field-label-font-size: var(--xh-font-size-small-px);
44
+ --xh-form-field-label-font-weight: 600;
45
+ --xh-form-field-label-text-transform: uppercase;
46
+ --xh-form-field-readonly-label-margin: 0;
47
+
48
+ // A field set (or other child) that stretches to fill the remaining height, passing the flex
49
+ // down to a read-only code editor within. `.xh-card` renders as a <fieldset>, so make it an
50
+ // explicit flex column.
51
+ &__fill {
52
+ display: flex;
53
+ // Floor via flex-basis, not min-height - a min-height-clamped <fieldset> collapses its content.
54
+ flex: 1 0 250px;
55
+ flex-direction: column;
56
+
57
+ > .xh-card__content,
58
+ .xh-form-field,
59
+ .xh-form-field__inner,
60
+ .xh-form-field__readonly-display {
61
+ flex: 1;
62
+ min-height: 0;
63
+ }
64
+
65
+ // FormField's read-only wrapper is a block - make it (and the display within) flex columns
66
+ // so a percentage-height editor has a definite height to fill.
67
+ .xh-form-field__inner,
68
+ .xh-form-field__readonly-display {
69
+ display: flex;
70
+ flex-direction: column;
71
+ }
72
+
73
+ .xh-form-field__readonly-display {
74
+ padding: 0;
75
+ }
76
+ }
77
+
78
+ // Markdown-rendered text fields - undo the readonly display's pre-wrap (which turns
79
+ // react-markdown's inter-element newlines into blank lines) and tighten block spacing.
80
+ &__markdown {
81
+ white-space: normal;
82
+
83
+ p,
84
+ ul,
85
+ ol {
86
+ margin: 0 0 var(--xh-pad-half-px) 0;
87
+ }
88
+
89
+ ul,
90
+ ol {
91
+ padding-left: 1.5em;
92
+ }
93
+
94
+ li > p {
95
+ margin: 0;
96
+ }
97
+
98
+ > :last-child {
99
+ margin-bottom: 0;
100
+ }
101
+ }
102
+
103
+ // Frame read-only editors - they otherwise float borderless within a field set.
104
+ .xh-code-input {
105
+ border: var(--xh-border-solid);
106
+ border-radius: var(--xh-border-radius-px);
107
+ }
108
+ }
109
+
110
+ // RestDetailPanel form body.
111
+ .xh-admin-rest-detail__form {
112
+ padding: var(--xh-pad-px);
113
+ gap: var(--xh-pad-px);
26
114
  }
27
115
 
28
116
  .xh-units-label {
@@ -4,9 +4,13 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {badge} from '@xh/hoist/cmp/badge';
7
8
  import * as Col from '@xh/hoist/cmp/grid/columns';
8
- import {truncate} from 'lodash';
9
9
  import {ColumnSpec} from '@xh/hoist/cmp/grid/columns';
10
+ import {Intent} from '@xh/hoist/core';
11
+ import {Icon} from '@xh/hoist/icon';
12
+ import {truncate} from 'lodash';
13
+ import {ReactElement} from 'react';
10
14
 
11
15
  export const value: ColumnSpec = {
12
16
  field: {name: 'value', type: 'auto'},
@@ -35,8 +39,31 @@ export const valueType: ColumnSpec = {
35
39
  type: 'string',
36
40
  displayName: 'Type'
37
41
  },
38
- width: 80,
39
- align: 'center'
42
+ width: 90,
43
+ align: 'center',
44
+ renderer: v => valueTypeRenderer(v)
45
+ };
46
+
47
+ /** Renders a config value type (json, string, int, bool, pwd...) as an icon + colored badge. */
48
+ export function valueTypeRenderer(valueType: string): ReactElement {
49
+ if (!valueType) return null;
50
+ const {icon, intent} = VALUE_TYPE_BADGES[valueType] ?? {};
51
+ return badge({
52
+ item: valueType,
53
+ icon: icon?.(),
54
+ intent,
55
+ className: 'xh-value-type-badge'
56
+ });
57
+ }
58
+
59
+ const VALUE_TYPE_BADGES: Record<string, {icon: () => ReactElement; intent?: Intent}> = {
60
+ json: {icon: Icon.json, intent: 'primary'},
61
+ bool: {icon: Icon.checkSquare, intent: 'success'},
62
+ int: {icon: Icon.calculator, intent: 'warning'},
63
+ long: {icon: Icon.calculator, intent: 'warning'},
64
+ double: {icon: Icon.calculator, intent: 'warning'},
65
+ pwd: {icon: Icon.lock, intent: 'danger'},
66
+ string: {icon: Icon.list}
40
67
  };
41
68
 
42
69
  export const groupName: ColumnSpec = {
@@ -0,0 +1,64 @@
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 {FormModel} from '@xh/hoist/cmp/form';
8
+ import {HoistModel, lookup, managed} from '@xh/hoist/core';
9
+ import {StoreRecord} from '@xh/hoist/data';
10
+ import {RestGridModel} from '@xh/hoist/desktop/cmp/rest';
11
+ import {computed, makeObservable} from '@xh/hoist/mobx';
12
+
13
+ /**
14
+ * Backs a {@link restDetailPanel}. Resolves the host RestGridModel via `@lookup` - the model of an
15
+ * enclosing component must expose it as a public property - then tracks its single selection and
16
+ * mirrors the selected record into a read-only FormModel with a field for every store field.
17
+ */
18
+ export class RestDetailModel extends HoistModel {
19
+ override xhImpl = true;
20
+
21
+ @lookup(RestGridModel) gridModel: RestGridModel;
22
+
23
+ @managed formModel: FormModel;
24
+
25
+ get record(): StoreRecord {
26
+ return this.gridModel.selectedRecord;
27
+ }
28
+
29
+ @computed
30
+ get hasSelection(): boolean {
31
+ return !!this.record;
32
+ }
33
+
34
+ get readonly(): boolean {
35
+ return this.gridModel.readonly;
36
+ }
37
+
38
+ constructor() {
39
+ super();
40
+ makeObservable(this);
41
+ }
42
+
43
+ override onLinked() {
44
+ this.formModel = new FormModel({
45
+ readonly: true,
46
+ fields: this.gridModel.store.fields.map(f => ({
47
+ name: f.name,
48
+ displayName: f.displayName
49
+ }))
50
+ });
51
+
52
+ // Record identity changes on selection and on store reload (e.g. after a save), so the
53
+ // form tracks both a new selection and fresh data for the current one.
54
+ this.addReaction({
55
+ track: () => this.record,
56
+ run: rec => this.formModel.init(rec?.data ?? {}),
57
+ fireImmediately: true
58
+ });
59
+ }
60
+
61
+ edit() {
62
+ this.gridModel.editRecord(this.record);
63
+ }
64
+ }
@@ -0,0 +1,106 @@
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 {valOrNa} from '@xh/hoist/admin/AdminUtils';
8
+ import {form, FormModel} from '@xh/hoist/cmp/form';
9
+ import {filler, placeholder, vframe} from '@xh/hoist/cmp/layout';
10
+ import {creates, hoistCmp, HoistProps, PersistOptions} from '@xh/hoist/core';
11
+ import {StoreRecord} from '@xh/hoist/data';
12
+ import {button} from '@xh/hoist/desktop/cmp/button';
13
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
14
+ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
15
+ import {Icon} from '@xh/hoist/icon';
16
+ import {startCase} from 'lodash';
17
+ import {ReactElement, ReactNode} from 'react';
18
+ import {RestDetailModel} from './RestDetailModel';
19
+
20
+ export interface RestDetailPanelProps extends HoistProps<RestDetailModel> {
21
+ /** Icon for the panel header and empty-state placeholder. */
22
+ icon: ReactElement;
23
+ /** Placeholder text when no single record is selected. */
24
+ emptyText?: string;
25
+ /** Default width in pixels. */
26
+ defaultSize?: number;
27
+ /** Minimum width in pixels. */
28
+ minSize?: number;
29
+ /** Options for persisting panel size and collapsed state. */
30
+ persistWith?: PersistOptions;
31
+ /** Panel title for the selected record - defaults to its `name`. */
32
+ titleFn?: (record: StoreRecord) => ReactNode;
33
+ /**
34
+ * Renders the form body for the selected record - typically one or more `formFieldSet`s of
35
+ * `formField`s bound to the store's field names. Rendered within a read-only `form` bound to
36
+ * the model's FormModel, styled with the `xh-admin-readonly-form` class.
37
+ */
38
+ renderForm: (record: StoreRecord, formModel: FormModel) => ReactNode;
39
+ }
40
+
41
+ /**
42
+ * Read-only detail view of the record selected in a RestGrid, docked to its right. Tracks the
43
+ * grid's selection, refreshes when the grid reloads, and offers an Edit button to open the grid's
44
+ * editor dialog for the selected record.
45
+ *
46
+ * Render as a sibling of `restGrid()` within an `hframe`. The host RestGridModel is resolved via
47
+ * `@lookup`, so the model of an enclosing component must expose it as a public property.
48
+ */
49
+ export const restDetailPanel = hoistCmp.factory<RestDetailPanelProps>({
50
+ displayName: 'RestDetailPanel',
51
+ className: 'xh-admin-rest-detail',
52
+ model: creates(RestDetailModel),
53
+
54
+ render({model, className, icon, titleFn, renderForm, ...props}) {
55
+ const {
56
+ emptyText = 'Select a record to view its details.',
57
+ defaultSize = 550,
58
+ minSize = 400,
59
+ persistWith
60
+ } = props,
61
+ {hasSelection, readonly, record, formModel, gridModel} = model;
62
+
63
+ return panel({
64
+ className,
65
+ title: hasSelection
66
+ ? (titleFn?.(record) ?? record.data.name)
67
+ : `${startCase(gridModel.unit)} Detail`,
68
+ icon,
69
+ compactHeader: true,
70
+ modelConfig: {
71
+ side: 'right',
72
+ defaultSize,
73
+ minSize,
74
+ persistWith,
75
+ // Skip per-selection form work while collapsed.
76
+ renderMode: 'unmountOnHide'
77
+ },
78
+ item: hasSelection
79
+ ? form({
80
+ model: formModel,
81
+ fieldDefaults: {readonlyRenderer: valOrNa},
82
+ item: vframe({
83
+ className: 'xh-admin-rest-detail__form xh-admin-readonly-form',
84
+ // Scroll when content outgrows the panel - inline to beat the frame's
85
+ // own overflow: hidden.
86
+ overflowY: 'auto',
87
+ items: renderForm(record, formModel)
88
+ })
89
+ })
90
+ : placeholder(icon, emptyText),
91
+ bbar: toolbar({
92
+ omit: !hasSelection || readonly,
93
+ items: [
94
+ filler(),
95
+ button({
96
+ text: 'Edit',
97
+ icon: Icon.edit(),
98
+ intent: 'primary',
99
+ outlined: true,
100
+ onClick: () => model.edit()
101
+ })
102
+ ]
103
+ })
104
+ });
105
+ }
106
+ });