@xh/hoist 80.0.0-SNAPSHOT.1767982629403 → 80.0.0-SNAPSHOT.1768251023007

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.
package/CHANGELOG.md CHANGED
@@ -11,9 +11,15 @@
11
11
  so there is no deprecated alias. Any app usages should swap to `XH.appLoadObserver`.
12
12
  * Removed additional references to deprecated `loadModel` within Hoist itself.
13
13
  * Removed the following instance getters - use new static typeguards instead:
14
- * `Store.isStore`
15
- * `View.isView`
16
- * `Filter.isFilter`
14
+ * `Store.isStore`
15
+ * `View.isView`
16
+ * `Filter.isFilter`
17
+
18
+ ### 🎁 New Features
19
+
20
+ * Added new `AppMenuButton.renderWithUserProfile` prop as a built-in alternative to the default
21
+ hamburger menu. Set to `true` to render the current user's initials instead or provide a function
22
+ to render a custom element for the user.
17
23
 
18
24
  ### ⚙️ Typescript API Adjustments
19
25
 
@@ -1,5 +1,5 @@
1
- import { TabSwitcherConfig, IDynamicTabSwitcherModel } from '@xh/hoist/cmp/tab/Types';
2
- import { HoistModel, PersistOptions, RefreshContextModel, RefreshMode, RenderMode } from '@xh/hoist/core';
1
+ import type { TabSwitcherConfig, IDynamicTabSwitcherModel, TabContainerModelPersistOptions } from '@xh/hoist/cmp/tab/Types';
2
+ import { HoistModel, RefreshContextModel, RefreshMode, RenderMode } from '@xh/hoist/core';
3
3
  import { ReactNode } from 'react';
4
4
  import { TabConfig, TabModel } from './TabModel';
5
5
  export interface TabContainerConfig {
@@ -12,7 +12,7 @@ export interface TabContainerConfig {
12
12
  defaultTabId?: string;
13
13
  /**
14
14
  * Base route name for this container. If set, this container will be route-enabled, with the
15
- * route for each tab being "[route]/[tab.id]". Cannot be used with `persistWith`.
15
+ * route for each tab being "[route]/[tab.id]".
16
16
  */
17
17
  route?: string;
18
18
  /**
@@ -35,8 +35,13 @@ export interface TabContainerConfig {
35
35
  * See enum for description of supported modes.
36
36
  */
37
37
  refreshMode?: RefreshMode;
38
- /** Options governing persistence. Cannot be used with `route`. */
39
- persistWith?: PersistOptions;
38
+ /**
39
+ * Options governing persistence. Tab containers can persist their last-active tab as well
40
+ * as favorite tabs for the dynamic `switcher` option. Note that this must be left unset or
41
+ * its nested `persistActiveTabId` option must be set to false if also using `route`, to avoid
42
+ * a possible conflict between an initial route and persisted last active tab.
43
+ */
44
+ persistWith?: TabContainerModelPersistOptions;
40
45
  /**
41
46
  * Placeholder to display if no tabs are provided or all tabs have been removed via
42
47
  * their `omit` config.
@@ -1,7 +1,7 @@
1
1
  import { RouterModel } from '@xh/hoist/appcontainer/RouterModel';
2
2
  import { HoistAuthModel } from '@xh/hoist/core/HoistAuthModel';
3
3
  import { Store } from '@xh/hoist/data';
4
- import { AlertBannerService, AutoRefreshService, ChangelogService, ConfigService, EnvironmentService, FetchOptions, FetchService, GridAutosizeService, GridExportService, IdentityService, IdleService, InspectorService, JsonBlobService, LocalStorageService, PrefService, SessionStorageService, TrackService, WebSocketService, ClientHealthService } from '@xh/hoist/svc';
4
+ import { AlertBannerService, AutoRefreshService, ChangelogService, ClientHealthService, ConfigService, EnvironmentService, FetchOptions, FetchService, GridAutosizeService, GridExportService, IdentityService, IdleService, InspectorService, JsonBlobService, LocalStorageService, PrefService, SessionStorageService, TrackService, WebSocketService } from '@xh/hoist/svc';
5
5
  import { LogLevel } from '@xh/hoist/utils/js';
6
6
  import { Router, State } from 'router5';
7
7
  import { CancelFn } from 'router5/types/types/base';
@@ -175,6 +175,8 @@ export declare class XHApi {
175
175
  * @see IdentityService.username
176
176
  */
177
177
  getUsername(): string;
178
+ /** @returns the current acting user's initials. */
179
+ getUserInitials(): string;
178
180
  /**
179
181
  * Logout the current user.
180
182
  * @see HoistAuthModel.logoutAsync
@@ -1,6 +1,7 @@
1
- import { MenuItemLike } from '@xh/hoist/core';
1
+ import { HoistUser, MenuItemLike } from '@xh/hoist/core';
2
2
  import { ButtonProps } from '@xh/hoist/desktop/cmp/button';
3
3
  import '@xh/hoist/desktop/register';
4
+ import { ReactNode } from 'react';
4
5
  export interface AppMenuButtonProps extends ButtonProps {
5
6
  /**
6
7
  * Array of extra menu items. Can contain:
@@ -31,5 +32,13 @@ export interface AppMenuButtonProps extends ButtonProps {
31
32
  hideOptionsItem?: boolean;
32
33
  /** True to hide the Theme Toggle button. */
33
34
  hideThemeItem?: boolean;
35
+ /**
36
+ * Replace the default hamburger icon with a user profile representation. Set to true to render
37
+ * the user's initials from their `HoistUser.displayName`. Alternately, provide a custom
38
+ * function to render an alternate compact string or element for the current user.
39
+ */
40
+ renderWithUserProfile?: boolean | RenderWithUserProfileCustomFn;
34
41
  }
42
+ type RenderWithUserProfileCustomFn = (user: HoistUser) => ReactNode;
35
43
  export declare const AppMenuButton: import("react").FC<AppMenuButtonProps>, appMenuButton: import("@xh/hoist/core").ElementFactory<AppMenuButtonProps>;
44
+ export {};
@@ -1,6 +1,7 @@
1
- import { MenuItemLike } from '@xh/hoist/core';
1
+ import { HoistUser, MenuItemLike } from '@xh/hoist/core';
2
2
  import { MenuButtonProps } from '@xh/hoist/mobile/cmp/menu';
3
3
  import '@xh/hoist/mobile/register';
4
+ import { ReactNode } from 'react';
4
5
  export interface AppMenuButtonProps extends MenuButtonProps {
5
6
  /** Array of app-specific MenuItems or configs to create them. */
6
7
  extraItems?: MenuItemLike[];
@@ -21,7 +22,14 @@ export interface AppMenuButtonProps extends MenuButtonProps {
21
22
  hideThemeItem?: boolean;
22
23
  /** True to hide the About button */
23
24
  hideAboutItem?: boolean;
25
+ /**
26
+ * Replace the default hamburger icon with a user profile representation. Set to true to render
27
+ * the user's initials from their `HoistUser.displayName`. Alternately, provide a custom
28
+ * function to render an alternate compact string or element for the current user.
29
+ */
30
+ renderWithUserProfile?: boolean | RenderWithUserProfileCustomFn;
24
31
  }
32
+ type RenderWithUserProfileCustomFn = (user: HoistUser) => ReactNode;
25
33
  /**
26
34
  * A top-level application drop down menu, which installs a standard set of menu items for common
27
35
  * application actions. Application specific items can be displayed before these standard items.
@@ -30,3 +38,4 @@ export interface AppMenuButtonProps extends MenuButtonProps {
30
38
  * or they can each be explicitly hidden.
31
39
  */
32
40
  export declare const AppMenuButton: import("react").FC<AppMenuButtonProps>, appMenuButton: import("@xh/hoist/core").ElementFactory<AppMenuButtonProps>;
41
+ export {};
@@ -11,10 +11,12 @@ export declare class IdentityService extends HoistService {
11
11
  private _authUser;
12
12
  private _apparentUser;
13
13
  initAsync(): Promise<void>;
14
- /** Current acting user (see authUser for notes on impersonation) */
14
+ /** @returns current acting user (see authUser for notes on impersonation) */
15
15
  get user(): HoistUser;
16
- /** Current acting user's username. */
16
+ /** @returns current acting user's username. */
17
17
  get username(): string;
18
+ /** @returns current acting user's initials, based on displayName. */
19
+ get userInitials(): string;
18
20
  /**
19
21
  * Actual user who authenticated to the web application.
20
22
  * This will be the same as the user except when an administrator is impersonation another
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {
7
+ import type {
8
8
  TabSwitcherConfig,
9
9
  IDynamicTabSwitcherModel,
10
10
  TabContainerModelPersistOptions
@@ -14,7 +14,6 @@ import {
14
14
  managed,
15
15
  PersistableState,
16
16
  PersistenceProvider,
17
- PersistOptions,
18
17
  RefreshContextModel,
19
18
  RefreshMode,
20
19
  RenderMode,
@@ -41,7 +40,7 @@ export interface TabContainerConfig {
41
40
 
42
41
  /**
43
42
  * Base route name for this container. If set, this container will be route-enabled, with the
44
- * route for each tab being "[route]/[tab.id]". Cannot be used with `persistWith`.
43
+ * route for each tab being "[route]/[tab.id]".
45
44
  */
46
45
  route?: string;
47
46
 
@@ -69,8 +68,13 @@ export interface TabContainerConfig {
69
68
  */
70
69
  refreshMode?: RefreshMode;
71
70
 
72
- /** Options governing persistence. Cannot be used with `route`. */
73
- persistWith?: PersistOptions;
71
+ /**
72
+ * Options governing persistence. Tab containers can persist their last-active tab as well
73
+ * as favorite tabs for the dynamic `switcher` option. Note that this must be left unset or
74
+ * its nested `persistActiveTabId` option must be set to false if also using `route`, to avoid
75
+ * a possible conflict between an initial route and persisted last active tab.
76
+ */
77
+ persistWith?: TabContainerModelPersistOptions;
74
78
 
75
79
  /**
76
80
  * Placeholder to display if no tabs are provided or all tabs have been removed via
package/core/XH.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  AlertBannerService,
15
15
  AutoRefreshService,
16
16
  ChangelogService,
17
+ ClientHealthService,
17
18
  ConfigService,
18
19
  EnvironmentService,
19
20
  FetchOptions,
@@ -28,13 +29,13 @@ import {
28
29
  PrefService,
29
30
  SessionStorageService,
30
31
  TrackService,
31
- WebSocketService,
32
- ClientHealthService
32
+ WebSocketService
33
33
  } from '@xh/hoist/svc';
34
- import {getLogLevel, setLogLevel, LogLevel, apiDeprecated} from '@xh/hoist/utils/js';
34
+ import {apiDeprecated, getLogLevel, LogLevel, setLogLevel} from '@xh/hoist/utils/js';
35
35
  import {camelCase, flatten, isString, uniqueId} from 'lodash';
36
36
  import {Router, State} from 'router5';
37
37
  import {CancelFn} from 'router5/types/types/base';
38
+ import ShortUniqueId from 'short-unique-id';
38
39
  import {SetOptional} from 'type-fest';
39
40
  import {AppContainerModel} from '../appcontainer/AppContainerModel';
40
41
  import {BannerModel} from '../appcontainer/BannerModel';
@@ -66,7 +67,6 @@ import {
66
67
  import {installServicesAsync} from './impl/InstallServices';
67
68
  import {instanceManager} from './impl/InstanceManager';
68
69
  import {HoistModel, ModelSelector, RefreshContextModel} from './model';
69
- import ShortUniqueId from 'short-unique-id';
70
70
 
71
71
  export const MIN_HOIST_CORE_VERSION = '31.2';
72
72
 
@@ -360,6 +360,11 @@ export class XHApi {
360
360
  return this.identityService?.username ?? null;
361
361
  }
362
362
 
363
+ /** @returns the current acting user's initials. */
364
+ getUserInitials(): string {
365
+ return this.identityService?.userInitials ?? null;
366
+ }
367
+
363
368
  /**
364
369
  * Logout the current user.
365
370
  * @see HoistAuthModel.logoutAsync
@@ -6,11 +6,6 @@
6
6
  */
7
7
 
8
8
  .xh-appbar {
9
- // Menu button might be on left or right of appBar - add right margin when on left only.
10
- .xh-app-menu-button:first-child {
11
- margin-right: var(--xh-pad-px);
12
- }
13
-
14
9
  .xh-appbar-icon {
15
10
  margin-right: var(--xh-pad-px);
16
11
  }
@@ -20,6 +15,30 @@
20
15
  font-size: var(--xh-appbar-title-font-size-px);
21
16
  margin-right: var(--xh-pad-px);
22
17
  }
18
+
19
+ .xh-app-menu-button {
20
+ // Menu button might be on left or right of appBar - add right margin when on left only.
21
+ &:first-child {
22
+ margin-right: var(--xh-pad-px);
23
+ }
24
+
25
+ &__user-profile {
26
+ border-radius: 50%;
27
+ border: var(--xh-border-solid);
28
+ cursor: pointer;
29
+ height: 32px;
30
+ line-height: 31px;
31
+ text-align: center;
32
+ width: 32px;
33
+ }
34
+
35
+ // Trigger profile-specific hover styles on parent button hover
36
+ &:hover .xh-app-menu-button__user-profile {
37
+ color: var(--xh-appbar-user-profile-hover-color);
38
+ border-color: var(--xh-appbar-user-profile-hover-color);
39
+ background-color: transparent;
40
+ }
41
+ }
23
42
  }
24
43
 
25
44
  //------------------------
@@ -4,13 +4,16 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {hoistCmp, MenuItemLike, XH} from '@xh/hoist/core';
7
+ import {div} from '@xh/hoist/cmp/layout';
8
+ import {hoistCmp, HoistProps, HoistUser, MenuItemLike, XH} from '@xh/hoist/core';
8
9
  import {ButtonProps, button} from '@xh/hoist/desktop/cmp/button';
9
10
  import '@xh/hoist/desktop/register';
10
11
  import {Icon} from '@xh/hoist/icon';
11
12
  import {menu, popover} from '@xh/hoist/kit/blueprint';
12
13
  import {parseMenuItems} from '@xh/hoist/utils/impl';
13
14
  import {withDefault} from '@xh/hoist/utils/js';
15
+ import {isFunction} from 'lodash';
16
+ import {ReactNode} from 'react';
14
17
 
15
18
  export interface AppMenuButtonProps extends ButtonProps {
16
19
  /**
@@ -50,8 +53,17 @@ export interface AppMenuButtonProps extends ButtonProps {
50
53
 
51
54
  /** True to hide the Theme Toggle button. */
52
55
  hideThemeItem?: boolean;
56
+
57
+ /**
58
+ * Replace the default hamburger icon with a user profile representation. Set to true to render
59
+ * the user's initials from their `HoistUser.displayName`. Alternately, provide a custom
60
+ * function to render an alternate compact string or element for the current user.
61
+ */
62
+ renderWithUserProfile?: boolean | RenderWithUserProfileCustomFn;
53
63
  }
54
64
 
65
+ type RenderWithUserProfileCustomFn = (user: HoistUser) => ReactNode;
66
+
55
67
  export const [AppMenuButton, appMenuButton] = hoistCmp.withFactory<AppMenuButtonProps>({
56
68
  displayName: 'AppMenuButton',
57
69
  model: false,
@@ -70,6 +82,7 @@ export const [AppMenuButton, appMenuButton] = hoistCmp.withFactory<AppMenuButton
70
82
  hideOptionsItem,
71
83
  hideThemeItem,
72
84
  disabled,
85
+ renderWithUserProfile,
73
86
  ...rest
74
87
  } = props;
75
88
 
@@ -79,7 +92,8 @@ export const [AppMenuButton, appMenuButton] = hoistCmp.withFactory<AppMenuButton
79
92
  position: 'bottom-right',
80
93
  minimal: true,
81
94
  item: button({
82
- icon: Icon.menu(),
95
+ icon: renderWithUserProfile ? null : Icon.menu(),
96
+ text: renderWithUserProfile ? userProfile({renderWithUserProfile}) : null,
83
97
  disabled,
84
98
  ...rest
85
99
  }),
@@ -95,6 +109,20 @@ export const [AppMenuButton, appMenuButton] = hoistCmp.withFactory<AppMenuButton
95
109
  //---------------------------
96
110
  // Implementation
97
111
  //---------------------------
112
+ const userProfile = hoistCmp.factory<
113
+ HoistProps & {renderWithUserProfile: true | RenderWithUserProfileCustomFn}
114
+ >({
115
+ model: false,
116
+ render({renderWithUserProfile}) {
117
+ return div({
118
+ className: 'xh-app-menu-button__user-profile',
119
+ item: isFunction(renderWithUserProfile)
120
+ ? renderWithUserProfile(XH.getUser())
121
+ : XH.getUserInitials()
122
+ });
123
+ }
124
+ });
125
+
98
126
  function buildMenuItems(props: AppMenuButtonProps) {
99
127
  let {
100
128
  hideAboutItem,
@@ -18,6 +18,17 @@
18
18
  display: flex;
19
19
  }
20
20
 
21
+ .xh-app-menu-button__user-profile {
22
+ border-radius: 50%;
23
+ border: 1px solid var(--xh-appbar-title-color);
24
+ cursor: pointer;
25
+ height: 28px;
26
+ font-size: var(--xh-font-size-small-em);
27
+ line-height: 27px;
28
+ text-align: center;
29
+ width: 28px;
30
+ }
31
+
21
32
  .xh-button {
22
33
  background: transparent !important;
23
34
  color: var(--xh-appbar-title-color) !important;
@@ -4,11 +4,14 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {hoistCmp, MenuItemLike, XH} from '@xh/hoist/core';
7
+ import {div} from '@xh/hoist/cmp/layout';
8
+ import {hoistCmp, HoistProps, HoistUser, MenuItemLike, XH} from '@xh/hoist/core';
8
9
  import {Icon} from '@xh/hoist/icon';
9
10
  import {menuButton, MenuButtonProps} from '@xh/hoist/mobile/cmp/menu';
10
11
  import '@xh/hoist/mobile/register';
11
12
  import {withDefault} from '@xh/hoist/utils/js';
13
+ import {isFunction} from 'lodash';
14
+ import {ReactNode} from 'react';
12
15
 
13
16
  export interface AppMenuButtonProps extends MenuButtonProps {
14
17
  /** Array of app-specific MenuItems or configs to create them. */
@@ -37,8 +40,17 @@ export interface AppMenuButtonProps extends MenuButtonProps {
37
40
 
38
41
  /** True to hide the About button */
39
42
  hideAboutItem?: boolean;
43
+
44
+ /**
45
+ * Replace the default hamburger icon with a user profile representation. Set to true to render
46
+ * the user's initials from their `HoistUser.displayName`. Alternately, provide a custom
47
+ * function to render an alternate compact string or element for the current user.
48
+ */
49
+ renderWithUserProfile?: boolean | RenderWithUserProfileCustomFn;
40
50
  }
41
51
 
52
+ type RenderWithUserProfileCustomFn = (user: HoistUser) => ReactNode;
53
+
42
54
  /**
43
55
  * A top-level application drop down menu, which installs a standard set of menu items for common
44
56
  * application actions. Application specific items can be displayed before these standard items.
@@ -62,11 +74,13 @@ export const [AppMenuButton, appMenuButton] = hoistCmp.withFactory<AppMenuButton
62
74
  hideOptionsItem,
63
75
  hideThemeItem,
64
76
  hideAboutItem,
77
+ renderWithUserProfile,
65
78
  ...rest
66
79
  } = props;
67
80
 
68
81
  return menuButton({
69
82
  className,
83
+ icon: renderWithUserProfile ? userProfile({renderWithUserProfile}) : Icon.menu(),
70
84
  menuItems: buildMenuItems(props),
71
85
  menuClassName: 'xh-app-menu',
72
86
  popoverProps: {popoverClassName: 'xh-app-menu-popover'},
@@ -78,6 +92,20 @@ export const [AppMenuButton, appMenuButton] = hoistCmp.withFactory<AppMenuButton
78
92
  //---------------------------
79
93
  // Implementation
80
94
  //---------------------------
95
+ const userProfile = hoistCmp.factory<
96
+ HoistProps & {renderWithUserProfile: true | RenderWithUserProfileCustomFn}
97
+ >({
98
+ model: false,
99
+ render({renderWithUserProfile}) {
100
+ return div({
101
+ className: 'xh-app-menu-button__user-profile',
102
+ item: isFunction(renderWithUserProfile)
103
+ ? renderWithUserProfile(XH.getUser())
104
+ : XH.getUserInitials()
105
+ });
106
+ }
107
+ });
108
+
81
109
  function buildMenuItems({
82
110
  hideOptionsItem,
83
111
  hideFeedbackItem,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "80.0.0-SNAPSHOT.1767982629403",
3
+ "version": "80.0.0-SNAPSHOT.1768251023007",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
package/styles/vars.scss CHANGED
@@ -205,13 +205,14 @@ body {
205
205
  //---------
206
206
  --xh-appbar-bg: var(--appbar-bg, var(--xh-bg-alt));
207
207
  --xh-appbar-border-color: var(--appbar-border-color, transparent);
208
+ --xh-appbar-box-shadow: var(--appbar-box-shadow, #{0 0 0 1px rgb(17 20 24 / 10%), 0 1px 1px rgb(17 20 24 / 20%)});
208
209
  --xh-appbar-color: var(--appbar-color, var(--xh-text-color));
209
210
  --xh-appbar-height: var(--appbar-height, 42);
210
211
  --xh-appbar-height-px: calc(var(--xh-appbar-height) * 1px);
211
212
  --xh-appbar-title-color: var(--appbar-title-color, #{mc('blue-grey', '700')});
212
213
  --xh-appbar-title-font-size: var(--appbar-title-font-size, calc(var(--xh-font-size) * 1.9));
213
214
  --xh-appbar-title-font-size-px: calc(var(--xh-appbar-title-font-size) * 1px);
214
- --xh-appbar-box-shadow: var(--appbar-box-shadow, #{0 0 0 1px rgb(17 20 24 / 10%), 0 1px 1px rgb(17 20 24 / 20%)});
215
+ --xh-appbar-user-profile-hover-color: var(--appbar-user-profile-hover-color, var(--xh-orange));
215
216
 
216
217
  &.xh-dark {
217
218
  --xh-appbar-border-color: var(--appbar-border-color, #{mc('blue-grey', '700')});
@@ -30,16 +30,28 @@ export class IdentityService extends HoistService {
30
30
  }
31
31
  }
32
32
 
33
- /** Current acting user (see authUser for notes on impersonation) */
33
+ /** @returns current acting user (see authUser for notes on impersonation) */
34
34
  get user(): HoistUser {
35
35
  return this._apparentUser;
36
36
  }
37
37
 
38
- /** Current acting user's username. */
38
+ /** @returns current acting user's username. */
39
39
  get username(): string {
40
40
  return this.user?.username ?? null;
41
41
  }
42
42
 
43
+ /** @returns current acting user's initials, based on displayName. */
44
+ get userInitials(): string {
45
+ // Handle common case of displayName being left as an email address.
46
+ const [displayName] = this.user.displayName.split('@'),
47
+ nameParts = displayName.split(/[\s.]+/);
48
+
49
+ return nameParts
50
+ .map(part => part.charAt(0).toUpperCase())
51
+ .join('')
52
+ .substring(0, XH.isMobileApp ? 2 : 3);
53
+ }
54
+
43
55
  /**
44
56
  * Actual user who authenticated to the web application.
45
57
  * This will be the same as the user except when an administrator is impersonation another