@xh/hoist 74.0.0 → 74.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,18 +5,17 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
8
- import {box, div, filler, fragment, hbox, vbox} from '@xh/hoist/cmp/layout';
8
+ import {box, div, filler, fragment, hbox, hframe, vbox} from '@xh/hoist/cmp/layout';
9
9
  import {hoistCmp, uses} from '@xh/hoist/core';
10
10
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
11
11
  import {select} from '@xh/hoist/desktop/cmp/input';
12
12
  import {panel} from '@xh/hoist/desktop/cmp/panel';
13
13
  import '@xh/hoist/desktop/register';
14
14
  import {Icon} from '@xh/hoist/icon';
15
- import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
15
+ import {menu, menuItem, popover} from '@xh/hoist/kit/blueprint';
16
16
  import {dragDropContext, draggable, droppable} from '@xh/hoist/kit/react-beautiful-dnd';
17
- import {elemWithin, getTestId, TEST_ID} from '@xh/hoist/utils/js';
17
+ import {elemWithin, getTestId} from '@xh/hoist/utils/js';
18
18
  import {splitLayoutProps} from '@xh/hoist/utils/react';
19
- import {ReactElement} from 'react';
20
19
  import classNames from 'classnames';
21
20
  import {compact, isEmpty, sortBy} from 'lodash';
22
21
  import './GroupingChooser.scss';
@@ -34,14 +33,14 @@ export interface GroupingChooserProps extends ButtonProps<GroupingChooserModel>
34
33
  /** Title for popover (default "GROUP BY") or null to suppress. */
35
34
  popoverTitle?: string;
36
35
 
37
- /** Width in pixels of the popover menu itself. */
36
+ /**
37
+ * Width in pixels of the popover menu itself.
38
+ * If unspecified, will default based on whether favorites are enabled.
39
+ */
38
40
  popoverWidth?: number;
39
41
 
40
42
  /** True (default) to style target button as an input field - blends better in toolbars. */
41
43
  styleButtonAsInput?: boolean;
42
-
43
- /** Icon clicked to launch favorites menu. Defaults to Icon.favorite() */
44
- favoritesIcon?: ReactElement;
45
44
  }
46
45
 
47
46
  /**
@@ -60,24 +59,22 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
60
59
  model,
61
60
  className,
62
61
  emptyText = 'Ungrouped',
63
- popoverWidth = 250,
62
+ popoverWidth,
64
63
  popoverMinHeight,
65
64
  popoverTitle = 'Group By',
66
65
  popoverPosition = 'bottom',
67
66
  styleButtonAsInput = true,
68
67
  testId,
69
- favoritesIcon,
70
68
  ...rest
71
69
  },
72
70
  ref
73
71
  ) {
74
- const {editorIsOpen, favoritesIsOpen, persistFavorites, value, allowEmpty} = model,
75
- isOpen = editorIsOpen || favoritesIsOpen,
72
+ const {editorIsOpen, value, allowEmpty, persistFavorites} = model,
73
+ isOpen = editorIsOpen,
76
74
  label = isEmpty(value) && allowEmpty ? emptyText : model.getValueLabel(value),
77
- [layoutProps, buttonProps] = splitLayoutProps(rest),
78
- favoritesMenuTestId = getTestId(testId, 'favorites-menu'),
79
- favoritesIconTestId = getTestId(testId, 'favorites-icon'),
80
- editorTestId = getTestId(testId, 'editor');
75
+ [layoutProps, buttonProps] = splitLayoutProps(rest);
76
+
77
+ popoverWidth = popoverWidth || (persistFavorites ? 500 : 250);
81
78
 
82
79
  return box({
83
80
  ref,
@@ -87,10 +84,9 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
87
84
  isOpen,
88
85
  popoverRef: model.popoverRef,
89
86
  popoverClassName: 'xh-grouping-chooser-popover xh-popup--framed',
90
- // right align favorites popover to match star icon
91
- // left align editor to keep in place when button changing size when commitOnChange: true
92
- position: favoritesIsOpen ? `${popoverPosition}-right` : `${popoverPosition}-left`,
93
- minimal: styleButtonAsInput,
87
+ // Left align editor to keep in place when button changing size when commitOnChange: true
88
+ position: `${popoverPosition}-left`,
89
+ minimal: false,
94
90
  item: fragment(
95
91
  button({
96
92
  text: label,
@@ -98,34 +94,27 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
98
94
  tabIndex: -1,
99
95
  className: classNames(
100
96
  'xh-grouping-chooser-button',
101
- styleButtonAsInput ? 'xh-grouping-chooser-button--as-input' : null,
102
- persistFavorites ? 'xh-grouping-chooser-button--with-favorites' : null
97
+ styleButtonAsInput ? 'xh-grouping-chooser-button--as-input' : null
103
98
  ),
104
99
  minimal: styleButtonAsInput,
105
100
  ...buttonProps,
106
101
  onClick: () => model.toggleEditor(),
107
102
  testId
108
- }),
109
- favoritesIconCmp({testId: favoritesIconTestId, favoritesIcon})
103
+ })
110
104
  ),
111
- content: favoritesIsOpen
112
- ? favoritesMenu({testId: favoritesMenuTestId})
113
- : editor({
114
- popoverWidth,
115
- popoverMinHeight,
116
- popoverTitle,
117
- emptyText,
118
- testId: editorTestId
119
- }),
105
+ content: popoverCmp({
106
+ popoverWidth,
107
+ popoverMinHeight,
108
+ popoverTitle,
109
+ emptyText,
110
+ testId
111
+ }),
120
112
  onInteraction: (nextOpenState, e) => {
121
113
  if (
122
114
  isOpen &&
123
115
  nextOpenState === false &&
124
116
  e?.target &&
125
- !elemWithin(
126
- e.target as HTMLElement,
127
- 'xh-grouping-chooser-button--with-favorites'
128
- )
117
+ !elemWithin(e.target as HTMLElement, 'xh-grouping-chooser-button')
129
118
  ) {
130
119
  model.commitPendingValueAndClose();
131
120
  }
@@ -138,18 +127,38 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
138
127
  //------------------
139
128
  // Editor
140
129
  //------------------
141
- const editor = hoistCmp.factory<GroupingChooserModel>({
142
- render({popoverWidth, popoverMinHeight, popoverTitle, emptyText, testId}) {
130
+ const popoverCmp = hoistCmp.factory<GroupingChooserModel>({
131
+ render({model, popoverWidth, popoverMinHeight, popoverTitle, emptyText, testId}) {
143
132
  return panel({
144
133
  width: popoverWidth,
145
134
  minHeight: popoverMinHeight,
135
+ items: hframe({
136
+ items: [
137
+ editor({
138
+ popoverTitle,
139
+ emptyText,
140
+ testId: getTestId(testId, 'editor')
141
+ }),
142
+ favoritesChooser({
143
+ omit: !model.persistFavorites,
144
+ testId: getTestId(testId, 'favorites')
145
+ })
146
+ ]
147
+ })
148
+ });
149
+ }
150
+ });
151
+
152
+ const editor = hoistCmp.factory<GroupingChooserModel>({
153
+ render({popoverTitle, emptyText, testId}) {
154
+ return vbox({
155
+ className: 'xh-grouping-chooser__editor',
156
+ testId,
146
157
  items: [
147
158
  div({className: 'xh-popup__title', item: popoverTitle, omit: !popoverTitle}),
148
159
  dimensionList({emptyText}),
149
- addDimensionControl(),
150
- filler()
151
- ],
152
- testId
160
+ addDimensionControl()
161
+ ]
153
162
  });
154
163
  }
155
164
  });
@@ -283,7 +292,7 @@ const addDimensionControl = hoistCmp.factory<GroupingChooserModel>({
283
292
  // ensure the Select loses its internal input state.
284
293
  key: JSON.stringify(options),
285
294
  options,
286
- placeholder: 'Add...',
295
+ placeholder: 'Add level...',
287
296
  flex: 1,
288
297
  width: null,
289
298
  hideDropdownIndicator: true,
@@ -320,47 +329,28 @@ function getDimOptions(dims, model) {
320
329
  //------------------
321
330
  // Favorites
322
331
  //------------------
323
- const favoritesIconCmp = hoistCmp.factory<GroupingChooserModel>({
324
- render({model, testId, favoritesIcon}) {
325
- if (!model.persistFavorites) return null;
326
- return div({
327
- item: favoritesIcon ?? Icon.favorite(),
328
- className: 'xh-grouping-chooser__favorite-icon',
329
- [TEST_ID]: testId,
330
- onClick: e => {
331
- model.toggleFavoritesMenu();
332
- e.stopPropagation();
333
- }
334
- });
335
- }
336
- });
337
-
338
- const favoritesMenu = hoistCmp.factory<GroupingChooserModel>({
332
+ const favoritesChooser = hoistCmp.factory<GroupingChooserModel>({
339
333
  render({model, testId}) {
340
- const options = model.favoritesOptions,
341
- isFavorite = model.isFavorite(model.value),
342
- omitAdd = isEmpty(model.value) || isFavorite,
343
- items = [];
344
-
345
- if (isEmpty(options)) {
346
- items.push(menuItem({text: 'No favorites saved...', disabled: true}));
347
- } else {
348
- items.push(...options.map(it => favoriteMenuItem(it)));
349
- }
350
-
351
- items.push(
352
- menuDivider({omit: omitAdd}),
353
- menuItem({
354
- icon: Icon.add({intent: 'success'}),
355
- text: 'Add current',
356
- omit: omitAdd,
357
- onClick: () => model.addFavorite(model.value)
358
- })
359
- );
334
+ const {favoritesOptions: options, isAddFavoriteEnabled} = model,
335
+ items = isEmpty(options)
336
+ ? [menuItem({text: 'No favorites saved.', disabled: true})]
337
+ : options.map(it => favoriteMenuItem(it));
360
338
 
361
339
  return vbox({
340
+ className: 'xh-grouping-chooser__favorites',
362
341
  testId,
363
- items: [div({className: 'xh-popup__title', item: 'Favorites'}), menu({items})]
342
+ items: [
343
+ div({className: 'xh-popup__title', item: 'Favorites'}),
344
+ menu({items}),
345
+ button({
346
+ text: 'Add current',
347
+ icon: Icon.add({intent: 'success'}),
348
+ className: 'xh-grouping-chooser__favorites__add-btn',
349
+ outlined: true,
350
+ omit: !isAddFavoriteEnabled,
351
+ onClick: () => model.addPendingAsFavorite()
352
+ })
353
+ ]
364
354
  });
365
355
  }
366
356
  });
@@ -369,7 +359,7 @@ const favoriteMenuItem = hoistCmp.factory<GroupingChooserModel>({
369
359
  render({model, value, label}) {
370
360
  return menuItem({
371
361
  text: label,
372
- className: 'xh-grouping-chooser__favorite',
362
+ className: 'xh-grouping-chooser__favorites__favorite',
373
363
  onClick: () => model.setValue(value),
374
364
  labelElement: button({
375
365
  icon: Icon.delete(),
@@ -7,13 +7,14 @@
7
7
 
8
8
  import {box, fragment, hbox} from '@xh/hoist/cmp/layout';
9
9
  import {spinner} from '@xh/hoist/cmp/spinner';
10
- import {hoistCmp, HoistProps, useLocalModel, uses} from '@xh/hoist/core';
10
+ import {hoistCmp, HoistProps, MenuItemLike, useLocalModel, uses} from '@xh/hoist/core';
11
11
  import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
12
12
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {popover} from '@xh/hoist/kit/blueprint';
15
15
  import {useOnVisibleChange} from '@xh/hoist/utils/react';
16
16
  import {startCase} from 'lodash';
17
+ import {ReactElement} from 'react';
17
18
  import {viewMenu} from './ViewMenu';
18
19
  import {ViewManagerLocalModel} from './ViewManagerLocalModel';
19
20
  import {manageDialog} from './dialog/ManageDialog';
@@ -21,28 +22,44 @@ import {saveAsDialog} from './dialog/SaveAsDialog';
21
22
 
22
23
  import './ViewManager.scss';
23
24
 
24
- /**
25
- * Visibility options for save/revert button.
26
- *
27
- * 'never' to hide button.
28
- * 'whenDirty' to only show when persistence state is dirty and button is therefore enabled.
29
- * 'always' will always show button.
30
- */
31
- export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
32
-
33
25
  export interface ViewManagerProps extends HoistProps<ViewManagerModel> {
34
26
  menuButtonProps?: Partial<ButtonProps>;
35
27
  saveButtonProps?: Partial<ButtonProps>;
36
28
  revertButtonProps?: Partial<ButtonProps>;
37
29
 
30
+ /** Button icon when on the default (in-code state) view. Default `Icon.bookmark`. */
31
+ defaultViewIcon?: ReactElement;
32
+ /** Button icon when the selected view is owned by the current user. Default `Icon.bookmark`. */
33
+ ownedViewIcon?: ReactElement;
34
+ /** Button icon when the selected view is shared by another user. Default `Icon.users`. */
35
+ sharedViewIcon?: ReactElement;
36
+ /** Button icon when the selected view is globally shared. Default `Icon.globe`. */
37
+ globalViewIcon?: ReactElement;
38
+
38
39
  /** Default 'whenDirty' */
39
40
  showSaveButton?: ViewManagerStateButtonMode;
40
41
  /** Default 'never' */
41
42
  showRevertButton?: ViewManagerStateButtonMode;
42
- /** Side the save and revert buttons should appear on (default 'right') */
43
+ /** Side relative to the menu on which save/revert buttons should render. Default 'right'. */
43
44
  buttonSide?: 'left' | 'right';
45
+ /**
46
+ * Array of extra menu items. Can contain:
47
+ * + `MenuItems` or configs to create them.
48
+ * + `MenuDividers` or the special string token '-'.
49
+ * + React Elements or strings, which will be interpreted as the `text` property for a MenuItem.
50
+ */
51
+ extraMenuItems?: MenuItemLike[];
44
52
  }
45
53
 
54
+ /**
55
+ * Visibility options for save/revert buttons inlined next to the ViewManager menu:
56
+ * 'never' to always hide - user must save/revert via menu.
57
+ * 'whenDirty' (default) to show only when view state is dirty and the button is enabled.
58
+ * 'always' to always show, including when view not dirty and the button is disabled.
59
+ * Useful to avoid jumpiness in toolbar layouts.
60
+ */
61
+ export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
62
+
46
63
  /**
47
64
  * Desktop ViewManager component - a button-based menu for saving and swapping between named
48
65
  * bundles of persisted component state (e.g. grid views, dashboards, and similar).
@@ -60,9 +77,14 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
60
77
  menuButtonProps,
61
78
  saveButtonProps,
62
79
  revertButtonProps,
80
+ defaultViewIcon = Icon.bookmark(),
81
+ ownedViewIcon = Icon.bookmark(),
82
+ sharedViewIcon = Icon.users(),
83
+ globalViewIcon = Icon.globe(),
63
84
  showSaveButton = 'whenDirty',
64
85
  showRevertButton = 'never',
65
- buttonSide = 'right'
86
+ buttonSide = 'right',
87
+ extraMenuItems = []
66
88
  }: ViewManagerProps) {
67
89
  const {loadModel} = model,
68
90
  locModel = useLocalModel(() => new ViewManagerLocalModel(model)),
@@ -70,7 +92,17 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
70
92
  revert = revertButton({model: locModel, mode: showRevertButton, ...revertButtonProps}),
71
93
  menu = popover({
72
94
  disabled: !locModel.isVisible, // Prevent orphaned popover menu
73
- item: menuButton({model: locModel, ...menuButtonProps}),
95
+ item: menuButton({
96
+ model: locModel,
97
+ icon: buttonIcon({
98
+ model: locModel,
99
+ defaultViewIcon,
100
+ ownedViewIcon,
101
+ sharedViewIcon,
102
+ globalViewIcon
103
+ }),
104
+ ...menuButtonProps
105
+ }),
74
106
  content: loadModel.isPending
75
107
  ? box({
76
108
  item: spinner({compact: true}),
@@ -79,7 +111,7 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
79
111
  height: 30,
80
112
  width: 30
81
113
  })
82
- : viewMenu({model: locModel}),
114
+ : viewMenu({model: locModel, extraMenuItems}),
83
115
  onOpening: () => model.refreshAsync(),
84
116
  placement: 'bottom',
85
117
  popoverClassName: 'xh-view-manager__popover'
@@ -97,13 +129,13 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
97
129
  });
98
130
 
99
131
  const menuButton = hoistCmp.factory<ViewManagerLocalModel>({
100
- render({model, ...rest}) {
132
+ render({model, icon, ...rest}) {
101
133
  const {view, typeDisplayName, isLoading} = model.parent;
102
134
  return button({
103
135
  className: 'xh-view-manager__menu-button',
104
136
  text: view.isDefault ? `Default ${startCase(typeDisplayName)}` : view.name,
105
137
  icon: !isLoading
106
- ? Icon.bookmark()
138
+ ? icon
107
139
  : box({
108
140
  item: spinner({width: 13, height: 13, style: {margin: 'auto'}}),
109
141
  width: 16.25
@@ -115,6 +147,16 @@ const menuButton = hoistCmp.factory<ViewManagerLocalModel>({
115
147
  }
116
148
  });
117
149
 
150
+ const buttonIcon = hoistCmp.factory<ViewManagerLocalModel>({
151
+ render({model, ownedViewIcon, sharedViewIcon, globalViewIcon, defaultViewIcon}) {
152
+ const {view} = model.parent;
153
+ if (view.isOwned) return ownedViewIcon;
154
+ if (view.isShared) return sharedViewIcon;
155
+ if (view.isGlobal) return globalViewIcon;
156
+ return defaultViewIcon;
157
+ }
158
+ });
159
+
118
160
  const saveButton = hoistCmp.factory<ViewManagerLocalModel>({
119
161
  render({model, mode, ...rest}) {
120
162
  if (hideStateButton(model, mode)) return null;
@@ -11,6 +11,7 @@ import {switchInput} from '@xh/hoist/desktop/cmp/input';
11
11
  import {Icon} from '@xh/hoist/icon';
12
12
  import {menu, menuDivider, menuItem} from '@xh/hoist/kit/blueprint';
13
13
  import {pluralize} from '@xh/hoist/utils/js';
14
+ import {filterConsecutiveMenuSeparators, parseMenuItems} from '@xh/hoist/utils/impl';
14
15
  import {Dictionary} from 'express-serve-static-core';
15
16
  import {each, filter, groupBy, isEmpty, isFunction, orderBy, some, startCase} from 'lodash';
16
17
  import {ReactNode} from 'react';
@@ -20,10 +21,16 @@ import {ViewManagerLocalModel} from './ViewManagerLocalModel';
20
21
  * Default Menu used by ViewManager.
21
22
  */
22
23
  export const viewMenu = hoistCmp.factory<ViewManagerLocalModel>({
23
- render({model}) {
24
+ render({model, extraMenuItems}) {
24
25
  return menu({
25
26
  className: 'xh-view-manager__menu',
26
- items: [...getNavMenuItems(model.parent), menuDivider(), ...getOtherMenuItems(model)]
27
+ items: [
28
+ ...getNavMenuItems(model.parent),
29
+ menuDivider(),
30
+ ...parseMenuItems(extraMenuItems),
31
+ menuDivider(),
32
+ ...getOtherMenuItems(model)
33
+ ].filter(filterConsecutiveMenuSeparators())
27
34
  });
28
35
  }
29
36
  });
@@ -7,6 +7,7 @@
7
7
  import {FeedbackDialogModel} from '@xh/hoist/appcontainer/FeedbackDialogModel';
8
8
  import {filler} from '@xh/hoist/cmp/layout';
9
9
  import {hoistCmp, uses} from '@xh/hoist/core';
10
+ import {Icon} from '@xh/hoist/icon';
10
11
  import {button} from '@xh/hoist/mobile/cmp/button';
11
12
  import {dialog} from '@xh/hoist/mobile/cmp/dialog';
12
13
  import {textArea} from '@xh/hoist/mobile/cmp/input';
@@ -41,6 +42,9 @@ export const feedbackDialog = hoistCmp.factory({
41
42
  }),
42
43
  button({
43
44
  text: 'Send',
45
+ icon: Icon.mail(),
46
+ intent: 'primary',
47
+ outlined: true,
44
48
  onClick: () => model.submitAsync()
45
49
  })
46
50
  ]
@@ -57,8 +57,10 @@ export const optionsDialog = hoistCmp.factory({
57
57
  onClick: () => model.hide()
58
58
  }),
59
59
  button({
60
- text: 'Save',
60
+ text: 'Apply',
61
61
  icon: reloadRequired ? Icon.refresh() : Icon.check(),
62
+ intent: 'primary',
63
+ outlined: true,
62
64
  disabled: !formModel.isDirty,
63
65
  onClick: () => model.saveAsync()
64
66
  })
@@ -60,7 +60,6 @@ export const [ColChooser, colChooser] = hoistCmp.withFactory<ColChooserProps>({
60
60
  onDragEnd: impl.onDragEnd,
61
61
  items: [
62
62
  panel({
63
- title: 'Visible Columns',
64
63
  className: 'xh-col-chooser__section',
65
64
  scrollable: true,
66
65
  items: [
@@ -119,8 +118,10 @@ export const [ColChooser, colChooser] = hoistCmp.withFactory<ColChooserProps>({
119
118
  onClick: () => model.close()
120
119
  }),
121
120
  button({
122
- text: 'Save',
121
+ text: 'Apply',
123
122
  icon: Icon.check(),
123
+ intent: 'primary',
124
+ outlined: true,
124
125
  onClick: () => {
125
126
  model.commit();
126
127
  model.close();
@@ -16,18 +16,23 @@
16
16
  }
17
17
  }
18
18
 
19
+ &__editor {
20
+ .xh-panel__content {
21
+ padding: var(--xh-pad-px);
22
+ }
23
+ }
24
+
19
25
  &__row {
20
26
  display: flex;
21
27
  align-items: center;
22
28
  background-color: var(--xh-bg);
23
- height: 35px;
29
+ height: 45px;
24
30
  flex-shrink: 0;
25
31
 
26
32
  &__grabber {
27
33
  display: flex;
28
34
  align-items: center;
29
35
  justify-content: center;
30
- height: 30px;
31
36
  width: 30px;
32
37
  }
33
38
 
@@ -49,30 +54,46 @@
49
54
  }
50
55
 
51
56
  &__add-control {
52
- padding: var(--xh-pad-half-px);
57
+ // Align with already-added level controls.
58
+ padding: 5px 40px 5px 30px;
53
59
  }
54
60
 
55
- &__favorite {
56
- height: 35px;
57
- align-items: center;
61
+ &__favorites {
62
+ // Fix at 50% of dialog height, preventing panel's default flex:auto
63
+ flex: none !important;
64
+ height: 50%;
58
65
 
59
- .xh-button {
60
- justify-content: left;
66
+ // Section heading style for internal title (like colChooser)
67
+ .xh-panel-header {
68
+ background-color: var(--xh-appbar-bg);
69
+ color: var(--xh-appbar-title-color);
70
+ padding-left: var(--xh-pad-px);
61
71
  }
62
- }
63
- }
64
72
 
65
- .xh-grouping-chooser-popover {
66
- .dialog {
67
- min-width: unset;
68
- }
73
+ .xh-panel__content {
74
+ padding: var(--xh-pad-px);
75
+ }
69
76
 
70
- .xh-dialog__inner {
71
- padding: 0;
72
- }
77
+ // Empty state placeholder - don't flex as it pushes add button down.
78
+ .xh-placeholder {
79
+ flex: none;
80
+ margin: var(--xh-pad-px);
81
+ }
82
+
83
+ // Individual favorite items (hBoxes w/buttons)
84
+ &__favorite {
85
+ flex: none !important;
86
+ height: 35px;
87
+ align-items: center;
73
88
 
74
- &__content {
75
- padding: 3px var(--xh-pad-half-px);
76
- min-width: 240px; // Required to allow for toolbars
89
+ .xh-button {
90
+ justify-content: left;
91
+ }
92
+ }
93
+
94
+ &__add-btn {
95
+ width: 200px;
96
+ margin: var(--xh-pad-px) auto;
97
+ }
77
98
  }
78
99
  }