@memberjunction/ng-explorer-core 6.1.4 → 6.2.0-edge.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.
- package/dist/app-routing.module.d.ts +23 -1
- package/dist/app-routing.module.d.ts.map +1 -1
- package/dist/app-routing.module.js +73 -17
- package/dist/app-routing.module.js.map +1 -1
- package/dist/generated/lazy-feature-config.d.ts +1 -1
- package/dist/generated/lazy-feature-config.d.ts.map +1 -1
- package/dist/generated/lazy-feature-config.js +1 -8
- package/dist/generated/lazy-feature-config.js.map +1 -1
- package/dist/lib/command-palette/command-palette.component.d.ts +7 -1
- package/dist/lib/command-palette/command-palette.component.d.ts.map +1 -1
- package/dist/lib/command-palette/command-palette.component.js +28 -14
- package/dist/lib/command-palette/command-palette.component.js.map +1 -1
- package/dist/lib/conversation-feedback/conversation-feedback.d.ts +103 -13
- package/dist/lib/conversation-feedback/conversation-feedback.d.ts.map +1 -1
- package/dist/lib/conversation-feedback/conversation-feedback.js +219 -67
- package/dist/lib/conversation-feedback/conversation-feedback.js.map +1 -1
- package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.d.ts +64 -10
- package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.d.ts.map +1 -1
- package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.js +172 -66
- package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.js.map +1 -1
- package/dist/lib/generic/resource-container-component.d.ts +19 -5
- package/dist/lib/generic/resource-container-component.d.ts.map +1 -1
- package/dist/lib/generic/resource-container-component.js +43 -13
- package/dist/lib/generic/resource-container-component.js.map +1 -1
- package/dist/lib/guards/entities.guard.d.ts +4 -0
- package/dist/lib/guards/entities.guard.d.ts.map +1 -1
- package/dist/lib/guards/entities.guard.js +10 -2
- package/dist/lib/guards/entities.guard.js.map +1 -1
- package/dist/lib/identity-claims/claim-redeem.component.d.ts +4 -0
- package/dist/lib/identity-claims/claim-redeem.component.d.ts.map +1 -1
- package/dist/lib/identity-claims/claim-redeem.component.js +10 -2
- package/dist/lib/identity-claims/claim-redeem.component.js.map +1 -1
- package/dist/lib/oauth/oauth-callback.component.d.ts +4 -0
- package/dist/lib/oauth/oauth-callback.component.d.ts.map +1 -1
- package/dist/lib/oauth/oauth-callback.component.js +10 -2
- package/dist/lib/oauth/oauth-callback.component.js.map +1 -1
- package/dist/lib/profile/profile-dialog.component.d.ts.map +1 -1
- package/dist/lib/profile/profile-dialog.component.js +194 -191
- package/dist/lib/profile/profile-dialog.component.js.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +20 -2
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.js +33 -9
- package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-collections-resource.component.d.ts +80 -10
- package/dist/lib/resource-wrappers/chat-collections-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-collections-resource.component.js +168 -56
- package/dist/lib/resource-wrappers/chat-collections-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +186 -23
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +500 -177
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-deeplink-params.d.ts +2 -0
- package/dist/lib/resource-wrappers/chat-deeplink-params.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-deeplink-params.js +5 -1
- package/dist/lib/resource-wrappers/chat-deeplink-params.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-search-routing.d.ts +25 -0
- package/dist/lib/resource-wrappers/chat-search-routing.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/chat-search-routing.js +24 -0
- package/dist/lib/resource-wrappers/chat-search-routing.js.map +1 -0
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts +14 -2
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.js +32 -8
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts +54 -8
- package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-resource.component.js +138 -46
- package/dist/lib/resource-wrappers/dashboard-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/livekit-room-resource.component.d.ts +223 -30
- package/dist/lib/resource-wrappers/livekit-room-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/livekit-room-resource.component.js +438 -118
- package/dist/lib/resource-wrappers/livekit-room-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/search-results-resource.component.d.ts +7 -3
- package/dist/lib/resource-wrappers/search-results-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/search-results-resource.component.js +23 -15
- package/dist/lib/resource-wrappers/search-results-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/view-resource.component.d.ts +68 -10
- package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/view-resource.component.js +178 -62
- package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
- package/dist/lib/services/startup-validation.service.d.ts +4 -0
- package/dist/lib/services/startup-validation.service.d.ts.map +1 -1
- package/dist/lib/services/startup-validation.service.js +10 -2
- package/dist/lib/services/startup-validation.service.js.map +1 -1
- package/dist/lib/services/system-validation.service.d.ts +13 -1
- package/dist/lib/services/system-validation.service.d.ts.map +1 -1
- package/dist/lib/services/system-validation.service.js +29 -5
- package/dist/lib/services/system-validation.service.js.map +1 -1
- package/dist/lib/shell/components/dialogs/app-access-dialog.component.d.ts +54 -4
- package/dist/lib/shell/components/dialogs/app-access-dialog.component.d.ts.map +1 -1
- package/dist/lib/shell/components/dialogs/app-access-dialog.component.js +130 -40
- package/dist/lib/shell/components/dialogs/app-access-dialog.component.js.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.d.ts +29 -1
- package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.js +56 -12
- package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
- package/dist/lib/shell/components/header/app-switcher.component.d.ts +33 -5
- package/dist/lib/shell/components/header/app-switcher.component.d.ts.map +1 -1
- package/dist/lib/shell/components/header/app-switcher.component.js +85 -29
- package/dist/lib/shell/components/header/app-switcher.component.js.map +1 -1
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts +7 -1
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts.map +1 -1
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.js +63 -9
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.js.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +30 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.js +63 -15
- package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +99 -9
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.js +310 -110
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/shell/loading-themes.d.ts +16 -12
- package/dist/lib/shell/loading-themes.d.ts.map +1 -1
- package/dist/lib/shell/loading-themes.js +145 -137
- package/dist/lib/shell/loading-themes.js.map +1 -1
- package/dist/lib/shell/readiness-beacon.d.ts +24 -0
- package/dist/lib/shell/readiness-beacon.d.ts.map +1 -0
- package/dist/lib/shell/readiness-beacon.js +40 -0
- package/dist/lib/shell/readiness-beacon.js.map +1 -0
- package/dist/lib/shell/services/about-dialog.service.d.ts +6 -0
- package/dist/lib/shell/services/about-dialog.service.d.ts.map +1 -1
- package/dist/lib/shell/services/about-dialog.service.js +16 -4
- package/dist/lib/shell/services/about-dialog.service.js.map +1 -1
- package/dist/lib/shell/services/profile-dialog.service.d.ts +6 -0
- package/dist/lib/shell/services/profile-dialog.service.d.ts.map +1 -1
- package/dist/lib/shell/services/profile-dialog.service.js +16 -4
- package/dist/lib/shell/services/profile-dialog.service.js.map +1 -1
- package/dist/lib/shell/services/user-sharing-center-dialog.service.d.ts +6 -0
- package/dist/lib/shell/services/user-sharing-center-dialog.service.d.ts.map +1 -1
- package/dist/lib/shell/services/user-sharing-center-dialog.service.js +15 -3
- package/dist/lib/shell/services/user-sharing-center-dialog.service.js.map +1 -1
- package/dist/lib/shell/shell.component.d.ts +275 -40
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +704 -290
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/single-dashboard/Components/add-item/add-item.component.d.ts +64 -8
- package/dist/lib/single-dashboard/Components/add-item/add-item.component.d.ts.map +1 -1
- package/dist/lib/single-dashboard/Components/add-item/add-item.component.js +129 -37
- package/dist/lib/single-dashboard/Components/add-item/add-item.component.js.map +1 -1
- package/dist/lib/single-dashboard/Components/delete-item/delete-item.component.d.ts +26 -2
- package/dist/lib/single-dashboard/Components/delete-item/delete-item.component.d.ts.map +1 -1
- package/dist/lib/single-dashboard/Components/delete-item/delete-item.component.js +48 -10
- package/dist/lib/single-dashboard/Components/delete-item/delete-item.component.js.map +1 -1
- package/dist/lib/single-dashboard/Components/edit-dashboard/edit-dashboard.component.d.ts +84 -6
- package/dist/lib/single-dashboard/Components/edit-dashboard/edit-dashboard.component.d.ts.map +1 -1
- package/dist/lib/single-dashboard/Components/edit-dashboard/edit-dashboard.component.js +133 -29
- package/dist/lib/single-dashboard/Components/edit-dashboard/edit-dashboard.component.js.map +1 -1
- package/dist/lib/single-dashboard/single-dashboard.component.d.ts +173 -23
- package/dist/lib/single-dashboard/single-dashboard.component.d.ts.map +1 -1
- package/dist/lib/single-dashboard/single-dashboard.component.js +383 -109
- package/dist/lib/single-dashboard/single-dashboard.component.js.map +1 -1
- package/dist/lib/single-list-detail/single-list-detail.component.d.ts +383 -55
- package/dist/lib/single-list-detail/single-list-detail.component.d.ts.map +1 -1
- package/dist/lib/single-list-detail/single-list-detail.component.js +962 -326
- package/dist/lib/single-list-detail/single-list-detail.component.js.map +1 -1
- package/dist/lib/single-query/single-query.component.d.ts +22 -2
- package/dist/lib/single-query/single-query.component.d.ts.map +1 -1
- package/dist/lib/single-query/single-query.component.js +40 -10
- package/dist/lib/single-query/single-query.component.js.map +1 -1
- package/dist/lib/single-record/single-record.component.d.ts +41 -3
- package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
- package/dist/lib/single-record/single-record.component.js +71 -11
- package/dist/lib/single-record/single-record.component.js.map +1 -1
- package/dist/lib/single-search-result/single-search-result.component.d.ts +23 -3
- package/dist/lib/single-search-result/single-search-result.component.d.ts.map +1 -1
- package/dist/lib/single-search-result/single-search-result.component.js +42 -12
- package/dist/lib/single-search-result/single-search-result.component.js.map +1 -1
- package/dist/lib/system-validation/system-validation-banner.component.d.ts +12 -2
- package/dist/lib/system-validation/system-validation-banner.component.d.ts.map +1 -1
- package/dist/lib/system-validation/system-validation-banner.component.js +25 -5
- package/dist/lib/system-validation/system-validation-banner.component.js.map +1 -1
- package/dist/lib/user-menu/user-menu.types.d.ts +4 -0
- package/dist/lib/user-menu/user-menu.types.d.ts.map +1 -1
- package/dist/lib/user-menu/user-menu.types.js +10 -2
- package/dist/lib/user-menu/user-menu.types.js.map +1 -1
- package/dist/lib/user-notifications/user-notifications.component.d.ts +59 -7
- package/dist/lib/user-notifications/user-notifications.component.d.ts.map +1 -1
- package/dist/lib/user-notifications/user-notifications.component.js +151 -47
- package/dist/lib/user-notifications/user-notifications.component.js.map +1 -1
- package/dist/lib/user-profile/user-profile.component.d.ts +3 -2
- package/dist/lib/user-profile/user-profile.component.d.ts.map +1 -1
- package/dist/lib/user-profile/user-profile.component.js +5 -4
- package/dist/lib/user-profile/user-profile.component.js.map +1 -1
- package/package.json +52 -52
|
@@ -15,6 +15,7 @@ import { InstanceConfigEngine, UserInfoEngine } from '@memberjunction/core-entit
|
|
|
15
15
|
import { PACKAGE_VERSION } from '@memberjunction/graphql-dataprovider';
|
|
16
16
|
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
|
|
17
17
|
import { ApplyShellChromePolicy, BaseShellChromePolicy } from './shell-chrome-policy';
|
|
18
|
+
import { SetReadinessBeacon } from './readiness-beacon';
|
|
18
19
|
import * as i0 from "@angular/core";
|
|
19
20
|
import * as i1 from "@memberjunction/ng-base-application";
|
|
20
21
|
import * as i2 from "@memberjunction/ng-shared";
|
|
@@ -567,7 +568,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
567
568
|
profileDialogService;
|
|
568
569
|
viewContainerRef;
|
|
569
570
|
titleService;
|
|
570
|
-
|
|
571
|
+
DeveloperModeService;
|
|
571
572
|
startupValidationService;
|
|
572
573
|
commandPaletteService;
|
|
573
574
|
themeService;
|
|
@@ -581,11 +582,60 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
581
582
|
/** Newest workspace configuration emission — see the staleness guard in the Configuration subscription */
|
|
582
583
|
latestSyncedConfig = null;
|
|
583
584
|
initialNavigationComplete = false; // Track if initial navigation has completed
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
585
|
+
ActiveApp = null;
|
|
586
|
+
/** @deprecated Use {@link ActiveApp}. */
|
|
587
|
+
get activeApp() {
|
|
588
|
+
return this.ActiveApp;
|
|
589
|
+
}
|
|
590
|
+
/** @deprecated Use {@link ActiveApp}. */
|
|
591
|
+
set activeApp(value) {
|
|
592
|
+
this.ActiveApp = value;
|
|
593
|
+
}
|
|
594
|
+
_loading = true;
|
|
595
|
+
/**
|
|
596
|
+
* Shell loading-screen flag, and the source of the Computer Use readiness
|
|
597
|
+
* beacon. When it clears, the shell has finished loading the active route's
|
|
598
|
+
* resource, so `data-mj-ready="true"` is published on `<html>`; raising it
|
|
599
|
+
* again on navigation clears the beacon.
|
|
600
|
+
*
|
|
601
|
+
* The beacon lives in this setter rather than at the call sites because ~22
|
|
602
|
+
* places assign `loading` — routing, resource load, app switch, error paths.
|
|
603
|
+
* Publishing here means every one of them stays correct for free.
|
|
604
|
+
*/
|
|
605
|
+
get Loading() {
|
|
606
|
+
return this._loading;
|
|
607
|
+
}
|
|
608
|
+
set Loading(value) {
|
|
609
|
+
this._loading = value;
|
|
610
|
+
SetReadinessBeacon(!value);
|
|
611
|
+
}
|
|
612
|
+
/** @deprecated Use {@link Loading}. */
|
|
613
|
+
get loading() {
|
|
614
|
+
return this.Loading;
|
|
615
|
+
}
|
|
616
|
+
/** @deprecated Use {@link Loading}. */
|
|
617
|
+
set loading(value) {
|
|
618
|
+
this.Loading = value;
|
|
619
|
+
}
|
|
620
|
+
Initialized = false;
|
|
621
|
+
/** @deprecated Use {@link Initialized}. */
|
|
622
|
+
get initialized() {
|
|
623
|
+
return this.Initialized;
|
|
624
|
+
}
|
|
625
|
+
/** @deprecated Use {@link Initialized}. */
|
|
626
|
+
set initialized(value) {
|
|
627
|
+
this.Initialized = value;
|
|
628
|
+
}
|
|
587
629
|
waitingForFirstResource = false;
|
|
588
|
-
|
|
630
|
+
TabBarVisible = true;
|
|
631
|
+
/** @deprecated Use {@link TabBarVisible}. */
|
|
632
|
+
get tabBarVisible() {
|
|
633
|
+
return this.TabBarVisible;
|
|
634
|
+
}
|
|
635
|
+
/** @deprecated Use {@link TabBarVisible}. */
|
|
636
|
+
set tabBarVisible(value) {
|
|
637
|
+
this.TabBarVisible = value;
|
|
638
|
+
} // Controlled by workspace manager
|
|
589
639
|
/**
|
|
590
640
|
* True when the deployment uses the records-as-tabs record-open style
|
|
591
641
|
* (`Shell.RecordOpen.Style` != 'classic'): records open as native Golden
|
|
@@ -596,15 +646,79 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
596
646
|
get RecordTabsStyle() {
|
|
597
647
|
return this.chromeFlags.recordOpenStyle === 'records';
|
|
598
648
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
649
|
+
UserMenuVisible = false;
|
|
650
|
+
/** @deprecated Use {@link UserMenuVisible}. */
|
|
651
|
+
get userMenuVisible() {
|
|
652
|
+
return this.UserMenuVisible;
|
|
653
|
+
}
|
|
654
|
+
/** @deprecated Use {@link UserMenuVisible}. */
|
|
655
|
+
set userMenuVisible(value) {
|
|
656
|
+
this.UserMenuVisible = value;
|
|
657
|
+
} // User avatar context menu
|
|
658
|
+
MobileNavOpen = false;
|
|
659
|
+
/** @deprecated Use {@link MobileNavOpen}. */
|
|
660
|
+
get mobileNavOpen() {
|
|
661
|
+
return this.MobileNavOpen;
|
|
662
|
+
}
|
|
663
|
+
/** @deprecated Use {@link MobileNavOpen}. */
|
|
664
|
+
set mobileNavOpen(value) {
|
|
665
|
+
this.MobileNavOpen = value;
|
|
666
|
+
} // Mobile navigation drawer
|
|
667
|
+
UnreadNotificationCount = 0;
|
|
668
|
+
/** @deprecated Use {@link UnreadNotificationCount}. */
|
|
669
|
+
get unreadNotificationCount() {
|
|
670
|
+
return this.UnreadNotificationCount;
|
|
671
|
+
}
|
|
672
|
+
/** @deprecated Use {@link UnreadNotificationCount}. */
|
|
673
|
+
set unreadNotificationCount(value) {
|
|
674
|
+
this.UnreadNotificationCount = value;
|
|
675
|
+
} // Notification badge count
|
|
602
676
|
// Global Activity indicator (P3)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
677
|
+
ActivityItems = [];
|
|
678
|
+
/** @deprecated Use {@link ActivityItems}. */
|
|
679
|
+
get activityItems() {
|
|
680
|
+
return this.ActivityItems;
|
|
681
|
+
}
|
|
682
|
+
/** @deprecated Use {@link ActivityItems}. */
|
|
683
|
+
set activityItems(value) {
|
|
684
|
+
this.ActivityItems = value;
|
|
685
|
+
}
|
|
686
|
+
ActivityRunningCount = 0;
|
|
687
|
+
/** @deprecated Use {@link ActivityRunningCount}. */
|
|
688
|
+
get activityRunningCount() {
|
|
689
|
+
return this.ActivityRunningCount;
|
|
690
|
+
}
|
|
691
|
+
/** @deprecated Use {@link ActivityRunningCount}. */
|
|
692
|
+
set activityRunningCount(value) {
|
|
693
|
+
this.ActivityRunningCount = value;
|
|
694
|
+
}
|
|
695
|
+
ActivityOpen = false;
|
|
696
|
+
/** @deprecated Use {@link ActivityOpen}. */
|
|
697
|
+
get activityOpen() {
|
|
698
|
+
return this.ActivityOpen;
|
|
699
|
+
}
|
|
700
|
+
/** @deprecated Use {@link ActivityOpen}. */
|
|
701
|
+
set activityOpen(value) {
|
|
702
|
+
this.ActivityOpen = value;
|
|
703
|
+
}
|
|
704
|
+
IsViewingSystemTab = false;
|
|
705
|
+
/** @deprecated Use {@link IsViewingSystemTab}. */
|
|
706
|
+
get isViewingSystemTab() {
|
|
707
|
+
return this.IsViewingSystemTab;
|
|
708
|
+
}
|
|
709
|
+
/** @deprecated Use {@link IsViewingSystemTab}. */
|
|
710
|
+
set isViewingSystemTab(value) {
|
|
711
|
+
this.IsViewingSystemTab = value;
|
|
712
|
+
} // True when viewing a resource tab (not associated with a registered app)
|
|
713
|
+
LoadingAppId = null;
|
|
714
|
+
/** @deprecated Use {@link LoadingAppId}. */
|
|
715
|
+
get loadingAppId() {
|
|
716
|
+
return this.LoadingAppId;
|
|
717
|
+
}
|
|
718
|
+
/** @deprecated Use {@link LoadingAppId}. */
|
|
719
|
+
set loadingAppId(value) {
|
|
720
|
+
this.LoadingAppId = value;
|
|
721
|
+
} // ID of app currently being loaded (for app switcher loading indicator)
|
|
608
722
|
// Loading animation state
|
|
609
723
|
loadingMessageInterval = null;
|
|
610
724
|
loadingMessageIndex = 0;
|
|
@@ -626,32 +740,152 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
626
740
|
MJVersion = PACKAGE_VERSION;
|
|
627
741
|
loadingResetTimeout = null;
|
|
628
742
|
loadingResetDelayMs = 20_000; // 20 seconds before showing reset option
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
743
|
+
CurrentLoadingText;
|
|
744
|
+
/** @deprecated Use {@link CurrentLoadingText}. */
|
|
745
|
+
get currentLoadingText() {
|
|
746
|
+
return this.CurrentLoadingText;
|
|
747
|
+
}
|
|
748
|
+
/** @deprecated Use {@link CurrentLoadingText}. */
|
|
749
|
+
set currentLoadingText(value) {
|
|
750
|
+
this.CurrentLoadingText = value;
|
|
751
|
+
}
|
|
752
|
+
CurrentLoadingColor;
|
|
753
|
+
/** @deprecated Use {@link CurrentLoadingColor}. */
|
|
754
|
+
get currentLoadingColor() {
|
|
755
|
+
return this.CurrentLoadingColor;
|
|
756
|
+
}
|
|
757
|
+
/** @deprecated Use {@link CurrentLoadingColor}. */
|
|
758
|
+
set currentLoadingColor(value) {
|
|
759
|
+
this.CurrentLoadingColor = value;
|
|
760
|
+
}
|
|
761
|
+
CurrentLoadingTextColor;
|
|
762
|
+
/** @deprecated Use {@link CurrentLoadingTextColor}. */
|
|
763
|
+
get currentLoadingTextColor() {
|
|
764
|
+
return this.CurrentLoadingTextColor;
|
|
765
|
+
}
|
|
766
|
+
/** @deprecated Use {@link CurrentLoadingTextColor}. */
|
|
767
|
+
set currentLoadingTextColor(value) {
|
|
768
|
+
this.CurrentLoadingTextColor = value;
|
|
769
|
+
}
|
|
770
|
+
CurrentLoadingGradient;
|
|
771
|
+
/** @deprecated Use {@link CurrentLoadingGradient}. */
|
|
772
|
+
get currentLoadingGradient() {
|
|
773
|
+
return this.CurrentLoadingGradient;
|
|
774
|
+
}
|
|
775
|
+
/** @deprecated Use {@link CurrentLoadingGradient}. */
|
|
776
|
+
set currentLoadingGradient(value) {
|
|
777
|
+
this.CurrentLoadingGradient = value;
|
|
778
|
+
}
|
|
779
|
+
CurrentLoadingAnimation = 'pulse';
|
|
780
|
+
/** @deprecated Use {@link CurrentLoadingAnimation}. */
|
|
781
|
+
get currentLoadingAnimation() {
|
|
782
|
+
return this.CurrentLoadingAnimation;
|
|
783
|
+
}
|
|
784
|
+
/** @deprecated Use {@link CurrentLoadingAnimation}. */
|
|
785
|
+
set currentLoadingAnimation(value) {
|
|
786
|
+
this.CurrentLoadingAnimation = value;
|
|
787
|
+
}
|
|
634
788
|
// User avatar state
|
|
635
|
-
|
|
636
|
-
|
|
789
|
+
UserImageURL = '';
|
|
790
|
+
/** @deprecated Use {@link UserImageURL}. */
|
|
791
|
+
get userImageURL() {
|
|
792
|
+
return this.UserImageURL;
|
|
793
|
+
}
|
|
794
|
+
/** @deprecated Use {@link UserImageURL}. */
|
|
795
|
+
set userImageURL(value) {
|
|
796
|
+
this.UserImageURL = value;
|
|
797
|
+
}
|
|
798
|
+
UserIconClass = null;
|
|
799
|
+
/** @deprecated Use {@link UserIconClass}. */
|
|
800
|
+
get userIconClass() {
|
|
801
|
+
return this.UserIconClass;
|
|
802
|
+
}
|
|
803
|
+
/** @deprecated Use {@link UserIconClass}. */
|
|
804
|
+
set userIconClass(value) {
|
|
805
|
+
this.UserIconClass = value;
|
|
806
|
+
}
|
|
637
807
|
userName = '';
|
|
638
|
-
|
|
808
|
+
UserEmail = '';
|
|
809
|
+
/** @deprecated Use {@link UserEmail}. */
|
|
810
|
+
get userEmail() {
|
|
811
|
+
return this.UserEmail;
|
|
812
|
+
}
|
|
813
|
+
/** @deprecated Use {@link UserEmail}. */
|
|
814
|
+
set userEmail(value) {
|
|
815
|
+
this.UserEmail = value;
|
|
816
|
+
}
|
|
639
817
|
userEntity = null;
|
|
640
818
|
// User menu plugin system
|
|
641
819
|
userMenu = null;
|
|
642
|
-
|
|
820
|
+
UserMenuElements = [];
|
|
821
|
+
/** @deprecated Use {@link UserMenuElements}. */
|
|
822
|
+
get userMenuElements() {
|
|
823
|
+
return this.UserMenuElements;
|
|
824
|
+
}
|
|
825
|
+
/** @deprecated Use {@link UserMenuElements}. */
|
|
826
|
+
set userMenuElements(value) {
|
|
827
|
+
this.UserMenuElements = value;
|
|
828
|
+
}
|
|
643
829
|
destroy$ = new Subject();
|
|
644
830
|
// Pin progress overlay
|
|
645
831
|
PinProgressVisible = false;
|
|
646
832
|
PinProgressText = '';
|
|
647
833
|
// Search state
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
834
|
+
IsSearchOpen = false;
|
|
835
|
+
/** @deprecated Use {@link IsSearchOpen}. */
|
|
836
|
+
get isSearchOpen() {
|
|
837
|
+
return this.IsSearchOpen;
|
|
838
|
+
}
|
|
839
|
+
/** @deprecated Use {@link IsSearchOpen}. */
|
|
840
|
+
set isSearchOpen(value) {
|
|
841
|
+
this.IsSearchOpen = value;
|
|
842
|
+
}
|
|
843
|
+
SearchableEntities = [];
|
|
844
|
+
/** @deprecated Use {@link SearchableEntities}. */
|
|
845
|
+
get searchableEntities() {
|
|
846
|
+
return this.SearchableEntities;
|
|
847
|
+
}
|
|
848
|
+
/** @deprecated Use {@link SearchableEntities}. */
|
|
849
|
+
set searchableEntities(value) {
|
|
850
|
+
this.SearchableEntities = value;
|
|
851
|
+
}
|
|
852
|
+
SelectedEntity = null;
|
|
853
|
+
/** @deprecated Use {@link SelectedEntity}. */
|
|
854
|
+
get selectedEntity() {
|
|
855
|
+
return this.SelectedEntity;
|
|
856
|
+
}
|
|
857
|
+
/** @deprecated Use {@link SelectedEntity}. */
|
|
858
|
+
set selectedEntity(value) {
|
|
859
|
+
this.SelectedEntity = value;
|
|
860
|
+
}
|
|
861
|
+
SearchInput;
|
|
862
|
+
/** @deprecated Use {@link SearchInput}. */
|
|
863
|
+
get searchInput() {
|
|
864
|
+
return this.SearchInput;
|
|
865
|
+
}
|
|
866
|
+
/** @deprecated Use {@link SearchInput}. */
|
|
867
|
+
set searchInput(value) {
|
|
868
|
+
this.SearchInput = value;
|
|
869
|
+
}
|
|
652
870
|
/** Mobile drawer + its toggle — used to return focus to the toggle when the drawer closes. */
|
|
653
|
-
|
|
654
|
-
|
|
871
|
+
MobileNavDrawer;
|
|
872
|
+
/** @deprecated Use {@link MobileNavDrawer}. */
|
|
873
|
+
get mobileNavDrawer() {
|
|
874
|
+
return this.MobileNavDrawer;
|
|
875
|
+
}
|
|
876
|
+
/** @deprecated Use {@link MobileNavDrawer}. */
|
|
877
|
+
set mobileNavDrawer(value) {
|
|
878
|
+
this.MobileNavDrawer = value;
|
|
879
|
+
}
|
|
880
|
+
MobileNavToggle;
|
|
881
|
+
/** @deprecated Use {@link MobileNavToggle}. */
|
|
882
|
+
get mobileNavToggle() {
|
|
883
|
+
return this.MobileNavToggle;
|
|
884
|
+
}
|
|
885
|
+
/** @deprecated Use {@link MobileNavToggle}. */
|
|
886
|
+
set mobileNavToggle(value) {
|
|
887
|
+
this.MobileNavToggle = value;
|
|
888
|
+
}
|
|
655
889
|
// Legacy universal search overlay (omnibar-off MOBILE path) — opened by the
|
|
656
890
|
// mobile search icon or Ctrl/Cmd+K when the inline composite isn't visible.
|
|
657
891
|
// Desktop omnibar-off uses the inline header composite instead.
|
|
@@ -747,7 +981,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
747
981
|
// is the same guarantee read off state we already have, instead of a new
|
|
748
982
|
// dirty-tracking pipeline.)
|
|
749
983
|
this.workspaceManager.RecordsRegionTabFilter = this.resolvedRecordOpenStyle === 'records'
|
|
750
|
-
? (tab) => IsRecordsRegionTab(tab.configuration) && !this.
|
|
984
|
+
? (tab) => IsRecordsRegionTab(tab.configuration) && !this.TabContainerRef?.IsRecordTabEditing(tab.id)
|
|
751
985
|
: null;
|
|
752
986
|
}
|
|
753
987
|
get ShowSearchBar() {
|
|
@@ -833,25 +1067,57 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
833
1067
|
/** Palette footer gear → My Profile (where the Command Palette section lives). */
|
|
834
1068
|
OnPaletteSettingsRequested() {
|
|
835
1069
|
this.profileDialogService.open(this.viewContainerRef, {
|
|
836
|
-
avatarUrl: this.
|
|
837
|
-
avatarIconClass: this.
|
|
1070
|
+
avatarUrl: this.UserImageURL || null,
|
|
1071
|
+
avatarIconClass: this.UserIconClass || null
|
|
838
1072
|
});
|
|
839
1073
|
}
|
|
840
|
-
|
|
1074
|
+
OmnibarPalette;
|
|
1075
|
+
/** @deprecated Use {@link OmnibarPalette}. */
|
|
1076
|
+
get omnibarPalette() {
|
|
1077
|
+
return this.OmnibarPalette;
|
|
1078
|
+
}
|
|
1079
|
+
/** @deprecated Use {@link OmnibarPalette}. */
|
|
1080
|
+
set omnibarPalette(value) {
|
|
1081
|
+
this.OmnibarPalette = value;
|
|
1082
|
+
}
|
|
841
1083
|
/** Legacy inline search composite (omnibar-off desktop). Structural typing keeps
|
|
842
1084
|
the shell decoupled from the ng-search component class. */
|
|
843
|
-
|
|
1085
|
+
ShellSearchComposite;
|
|
1086
|
+
/** @deprecated Use {@link ShellSearchComposite}. */
|
|
1087
|
+
get shellSearchComposite() {
|
|
1088
|
+
return this.ShellSearchComposite;
|
|
1089
|
+
}
|
|
1090
|
+
/** @deprecated Use {@link ShellSearchComposite}. */
|
|
1091
|
+
set shellSearchComposite(value) {
|
|
1092
|
+
this.ShellSearchComposite = value;
|
|
1093
|
+
}
|
|
844
1094
|
/** Header affordance click → open the palette. */
|
|
845
1095
|
OpenOmnibar(initialQuery = '') {
|
|
846
1096
|
if (!this.ShowSearchBar) {
|
|
847
1097
|
return;
|
|
848
1098
|
}
|
|
849
|
-
this.
|
|
1099
|
+
this.OmnibarPalette?.Open(initialQuery);
|
|
850
1100
|
}
|
|
851
1101
|
// Tab container reference for thumbnail capture
|
|
852
|
-
|
|
1102
|
+
TabContainerRef;
|
|
1103
|
+
/** @deprecated Use {@link TabContainerRef}. */
|
|
1104
|
+
get tabContainerRef() {
|
|
1105
|
+
return this.TabContainerRef;
|
|
1106
|
+
}
|
|
1107
|
+
/** @deprecated Use {@link TabContainerRef}. */
|
|
1108
|
+
set tabContainerRef(value) {
|
|
1109
|
+
this.TabContainerRef = value;
|
|
1110
|
+
}
|
|
853
1111
|
// App access dialog
|
|
854
|
-
|
|
1112
|
+
AppAccessDialog;
|
|
1113
|
+
/** @deprecated Use {@link AppAccessDialog}. */
|
|
1114
|
+
get appAccessDialog() {
|
|
1115
|
+
return this.AppAccessDialog;
|
|
1116
|
+
}
|
|
1117
|
+
/** @deprecated Use {@link AppAccessDialog}. */
|
|
1118
|
+
set appAccessDialog(value) {
|
|
1119
|
+
this.AppAccessDialog = value;
|
|
1120
|
+
}
|
|
855
1121
|
pendingAppPath = null; // Store the app path we tried to access
|
|
856
1122
|
/**
|
|
857
1123
|
* Nav Bar apps positioned to the left of the app switcher.
|
|
@@ -860,26 +1126,42 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
860
1126
|
* this is bound directly in the shell nav template's @for/@if. Filters out apps
|
|
861
1127
|
* that have HideNavBarIconWhenActive=true and are currently active.
|
|
862
1128
|
*/
|
|
863
|
-
|
|
1129
|
+
LeftOfSwitcherApps = [];
|
|
1130
|
+
/** @deprecated Use {@link LeftOfSwitcherApps}. */
|
|
1131
|
+
get leftOfSwitcherApps() {
|
|
1132
|
+
return this.LeftOfSwitcherApps;
|
|
1133
|
+
}
|
|
1134
|
+
/** @deprecated Use {@link LeftOfSwitcherApps}. */
|
|
1135
|
+
set leftOfSwitcherApps(value) {
|
|
1136
|
+
this.LeftOfSwitcherApps = value;
|
|
1137
|
+
}
|
|
864
1138
|
/**
|
|
865
1139
|
* Nav Bar apps positioned to the left of the user menu.
|
|
866
1140
|
* Backing field recomputed only when the active app or nav-app list changes
|
|
867
1141
|
* (see {@link recomputeNavBarApps}). Filters out apps that have
|
|
868
1142
|
* HideNavBarIconWhenActive=true and are currently active.
|
|
869
1143
|
*/
|
|
870
|
-
|
|
1144
|
+
LeftOfUserMenuApps = [];
|
|
1145
|
+
/** @deprecated Use {@link LeftOfUserMenuApps}. */
|
|
1146
|
+
get leftOfUserMenuApps() {
|
|
1147
|
+
return this.LeftOfUserMenuApps;
|
|
1148
|
+
}
|
|
1149
|
+
/** @deprecated Use {@link LeftOfUserMenuApps}. */
|
|
1150
|
+
set leftOfUserMenuApps(value) {
|
|
1151
|
+
this.LeftOfUserMenuApps = value;
|
|
1152
|
+
}
|
|
871
1153
|
/**
|
|
872
1154
|
* Recompute the precomputed nav-bar app arrays. Called only when the active app
|
|
873
1155
|
* changes or the underlying nav-app list (re)loads — keeping the filtered arrays
|
|
874
1156
|
* (and their per-app UUIDsEqual checks) out of the per-CD-cycle hot path.
|
|
875
1157
|
*/
|
|
876
1158
|
recomputeNavBarApps() {
|
|
877
|
-
this.
|
|
878
|
-
.filter(app => !(app.HideNavBarIconWhenActive && UUIDsEqual(app.ID, this.
|
|
879
|
-
this.
|
|
880
|
-
.filter(app => !(app.HideNavBarIconWhenActive && UUIDsEqual(app.ID, this.
|
|
1159
|
+
this.LeftOfSwitcherApps = this.appManager.GetNavBarApps('Left of App Switcher')
|
|
1160
|
+
.filter(app => !(app.HideNavBarIconWhenActive && UUIDsEqual(app.ID, this.ActiveApp?.ID)));
|
|
1161
|
+
this.LeftOfUserMenuApps = this.appManager.GetNavBarApps('Left of User Menu')
|
|
1162
|
+
.filter(app => !(app.HideNavBarIconWhenActive && UUIDsEqual(app.ID, this.ActiveApp?.ID)));
|
|
881
1163
|
}
|
|
882
|
-
constructor(appManager, workspaceManager, layoutManager, tabService, navigationService, route, router, authBase, cdr, userAvatarService, userSharingCenterDialogService, aboutDialogService, profileDialogService, viewContainerRef, titleService,
|
|
1164
|
+
constructor(appManager, workspaceManager, layoutManager, tabService, navigationService, route, router, authBase, cdr, userAvatarService, userSharingCenterDialogService, aboutDialogService, profileDialogService, viewContainerRef, titleService, DeveloperModeService, startupValidationService, commandPaletteService, themeService, homePinService, fileOpenService, feedbackDialogService, feedbackService, activityService) {
|
|
883
1165
|
super();
|
|
884
1166
|
this.appManager = appManager;
|
|
885
1167
|
this.workspaceManager = workspaceManager;
|
|
@@ -896,7 +1178,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
896
1178
|
this.profileDialogService = profileDialogService;
|
|
897
1179
|
this.viewContainerRef = viewContainerRef;
|
|
898
1180
|
this.titleService = titleService;
|
|
899
|
-
this.
|
|
1181
|
+
this.DeveloperModeService = DeveloperModeService;
|
|
900
1182
|
this.startupValidationService = startupValidationService;
|
|
901
1183
|
this.commandPaletteService = commandPaletteService;
|
|
902
1184
|
this.themeService = themeService;
|
|
@@ -912,7 +1194,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
912
1194
|
const providerForServices = this.ProviderToUse;
|
|
913
1195
|
this.appManager.Provider = providerForServices;
|
|
914
1196
|
this.workspaceManager.Provider = providerForServices;
|
|
915
|
-
this.
|
|
1197
|
+
this.DeveloperModeService.Provider = providerForServices;
|
|
916
1198
|
this.startupValidationService.Provider = providerForServices;
|
|
917
1199
|
if (SharedService.Instance)
|
|
918
1200
|
SharedService.Instance.Provider = providerForServices;
|
|
@@ -921,49 +1203,57 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
921
1203
|
// Initialize animation based on theme configuration
|
|
922
1204
|
this.initializeAnimationFromTheme();
|
|
923
1205
|
// Set first message
|
|
924
|
-
this.
|
|
925
|
-
if (this.activeTheme.
|
|
1206
|
+
this.CurrentLoadingText = this.activeTheme.Messages[0];
|
|
1207
|
+
if (this.activeTheme.StaticColors) {
|
|
926
1208
|
// Standard theme: keep MJ blue, no gradient
|
|
927
|
-
this.
|
|
928
|
-
this.
|
|
929
|
-
this.
|
|
1209
|
+
this.CurrentLoadingColor = this.activeTheme.Colors[0];
|
|
1210
|
+
this.CurrentLoadingTextColor = '#757575'; // Default gray text
|
|
1211
|
+
this.CurrentLoadingGradient = null;
|
|
930
1212
|
}
|
|
931
1213
|
else {
|
|
932
1214
|
// Themed period: use theme colors and first gradient from the start
|
|
933
|
-
this.
|
|
934
|
-
this.
|
|
1215
|
+
this.CurrentLoadingColor = this.activeTheme.Colors[0];
|
|
1216
|
+
this.CurrentLoadingTextColor = this.activeTheme.Colors[0];
|
|
935
1217
|
// Set initial gradient if theme has gradients
|
|
936
|
-
if (this.activeTheme.
|
|
937
|
-
this.
|
|
1218
|
+
if (this.activeTheme.Gradients && this.activeTheme.Gradients.length > 0) {
|
|
1219
|
+
this.CurrentLoadingGradient = this.activeTheme.Gradients[0];
|
|
938
1220
|
}
|
|
939
1221
|
else {
|
|
940
|
-
this.
|
|
1222
|
+
this.CurrentLoadingGradient = null;
|
|
941
1223
|
}
|
|
942
1224
|
}
|
|
943
1225
|
}
|
|
1226
|
+
/** @deprecated Use {@link DeveloperModeService}. */
|
|
1227
|
+
get developerModeService() {
|
|
1228
|
+
return this.DeveloperModeService;
|
|
1229
|
+
}
|
|
1230
|
+
/** @deprecated Use {@link DeveloperModeService}. */
|
|
1231
|
+
set developerModeService(value) {
|
|
1232
|
+
this.DeveloperModeService = value;
|
|
1233
|
+
}
|
|
944
1234
|
async ngOnInit() {
|
|
945
1235
|
try {
|
|
946
1236
|
MJGlobal.Instance.GetEventListener(true).subscribe(async (loginEvent) => {
|
|
947
1237
|
if (loginEvent.event === MJEventType.LoggedIn) {
|
|
948
1238
|
if (this.authBase.initialPath === "/") {
|
|
949
1239
|
// Base route - no need to wait for NavigationEnd
|
|
950
|
-
await this.
|
|
1240
|
+
await this.InitializeShell();
|
|
951
1241
|
}
|
|
952
1242
|
else {
|
|
953
1243
|
// Deep link route - wait for NavigationEnd to ensure router URL is correct
|
|
954
1244
|
this.router.events.pipe(filter((event) => event instanceof NavigationEnd), filter(() => !this.initialNavigationComplete)).subscribe(async () => {
|
|
955
1245
|
this.initialNavigationComplete = true;
|
|
956
|
-
await this.
|
|
1246
|
+
await this.InitializeShell();
|
|
957
1247
|
});
|
|
958
1248
|
}
|
|
959
1249
|
}
|
|
960
1250
|
});
|
|
961
1251
|
}
|
|
962
1252
|
catch (error) {
|
|
963
|
-
this.
|
|
1253
|
+
this.Loading = false;
|
|
964
1254
|
}
|
|
965
1255
|
}
|
|
966
|
-
async
|
|
1256
|
+
async InitializeShell() {
|
|
967
1257
|
// Start the loading animation with cycling messages
|
|
968
1258
|
this.startLoadingAnimation();
|
|
969
1259
|
// Initialize application manager (subscribes to LoggedIn event)
|
|
@@ -995,26 +1285,26 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
995
1285
|
await this.workspaceManager.Initialize(user.ID);
|
|
996
1286
|
// Subscribe to tab bar visibility changes
|
|
997
1287
|
this.subscriptions.push(this.workspaceManager.TabBarVisible.subscribe(visible => {
|
|
998
|
-
this.
|
|
1288
|
+
this.TabBarVisible = visible;
|
|
999
1289
|
}));
|
|
1000
1290
|
// The host's chrome policy says its answer may have changed (org or plan switch)
|
|
1001
1291
|
this.subscriptions.push(this.chromePolicy.Changed.subscribe(() => this.onChromePolicyChanged()));
|
|
1002
1292
|
// Subscribe to the global Activity tracker (Run Pipeline, Sync, Cluster, …)
|
|
1003
1293
|
this.subscriptions.push(this.activityService.Activities$.subscribe(items => {
|
|
1004
|
-
this.
|
|
1005
|
-
this.
|
|
1294
|
+
this.ActivityItems = items;
|
|
1295
|
+
this.ActivityRunningCount = items.filter(i => i.Status === 'running').length;
|
|
1006
1296
|
if (items.length === 0)
|
|
1007
|
-
this.
|
|
1297
|
+
this.ActivityOpen = false;
|
|
1008
1298
|
this.cdr.detectChanges();
|
|
1009
1299
|
}));
|
|
1010
1300
|
// Subscribe to unread notification count changes
|
|
1011
1301
|
this.subscriptions.push(MJNotificationService.UnreadCount$.subscribe(count => {
|
|
1012
|
-
this.
|
|
1302
|
+
this.UnreadNotificationCount = count;
|
|
1013
1303
|
this.cdr.detectChanges();
|
|
1014
1304
|
}));
|
|
1015
1305
|
// Subscribe to active app changes
|
|
1016
1306
|
this.subscriptions.push(this.appManager.ActiveApp.subscribe(async (app) => {
|
|
1017
|
-
this.
|
|
1307
|
+
this.ActiveApp = app;
|
|
1018
1308
|
this.recomputeNavBarApps();
|
|
1019
1309
|
this.cdr.detectChanges();
|
|
1020
1310
|
// Create default tab when app is activated ONLY if:
|
|
@@ -1131,7 +1421,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1131
1421
|
}
|
|
1132
1422
|
// Subscribe to workspace configuration changes to sync URL and active app
|
|
1133
1423
|
this.subscriptions.push(this.workspaceManager.Configuration.subscribe(async (config) => {
|
|
1134
|
-
if (config && this.
|
|
1424
|
+
if (config && this.Initialized) {
|
|
1135
1425
|
this.latestSyncedConfig = config;
|
|
1136
1426
|
// Sync active app with active tab's application
|
|
1137
1427
|
await this.syncActiveAppWithTab(config);
|
|
@@ -1153,7 +1443,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1153
1443
|
}));
|
|
1154
1444
|
// Subscribe to router navigation events (for browser back/forward)
|
|
1155
1445
|
this.subscriptions.push(this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(event => {
|
|
1156
|
-
if (this.
|
|
1446
|
+
if (this.Initialized) {
|
|
1157
1447
|
this.syncWorkspaceWithUrl(event.urlAfterRedirects || event.url);
|
|
1158
1448
|
}
|
|
1159
1449
|
}));
|
|
@@ -1179,16 +1469,16 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1179
1469
|
this.subscriptions.push(MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
|
|
1180
1470
|
if (event.event === MJEventType.TenantChanged) {
|
|
1181
1471
|
if (event.eventCode === 'TenantChanging') {
|
|
1182
|
-
this.
|
|
1183
|
-
this.
|
|
1472
|
+
this.CurrentLoadingText = 'Switching organization...';
|
|
1473
|
+
this.Loading = true;
|
|
1184
1474
|
this.cdr.detectChanges();
|
|
1185
1475
|
}
|
|
1186
|
-
else if (event.eventCode === 'TenantChanged' && this.
|
|
1476
|
+
else if (event.eventCode === 'TenantChanged' && this.TabContainerRef) {
|
|
1187
1477
|
try {
|
|
1188
|
-
await this.
|
|
1478
|
+
await this.TabContainerRef.ReloadAllTabs();
|
|
1189
1479
|
}
|
|
1190
1480
|
finally {
|
|
1191
|
-
this.
|
|
1481
|
+
this.Loading = false;
|
|
1192
1482
|
this.cdr.detectChanges();
|
|
1193
1483
|
}
|
|
1194
1484
|
}
|
|
@@ -1196,7 +1486,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1196
1486
|
}));
|
|
1197
1487
|
// Load searchable entities for search functionality
|
|
1198
1488
|
await this.loadSearchableEntities();
|
|
1199
|
-
this.
|
|
1489
|
+
this.Initialized = true;
|
|
1200
1490
|
this.waitingForFirstResource = true;
|
|
1201
1491
|
// Decide whether to restore workspace state or honor the current URL.
|
|
1202
1492
|
// If the user navigated to a specific path (deep link, bookmark, typed URL),
|
|
@@ -1221,6 +1511,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1221
1511
|
// NG0100 ExpressionChangedAfterItHasBeenCheckedError in dev mode
|
|
1222
1512
|
this.cdr.detectChanges();
|
|
1223
1513
|
}
|
|
1514
|
+
/** @deprecated Use {@link InitializeShell}. */
|
|
1515
|
+
async initializeShell() {
|
|
1516
|
+
return this.InitializeShell();
|
|
1517
|
+
}
|
|
1224
1518
|
/**
|
|
1225
1519
|
* Handle deep links from URL
|
|
1226
1520
|
* Resource URLs like /resource/record/Companies/123 are handled by ResourceResolver
|
|
@@ -1256,7 +1550,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1256
1550
|
// 1. We're initialized (past the startup phase)
|
|
1257
1551
|
// 2. App is different from current
|
|
1258
1552
|
// 3. Either NOT in URL-based navigation mode, OR this IS a URL-based tab
|
|
1259
|
-
const shouldSetActiveApp = this.
|
|
1553
|
+
const shouldSetActiveApp = this.Initialized &&
|
|
1260
1554
|
app &&
|
|
1261
1555
|
!UUIDsEqual(currentActiveApp?.ID, request.ApplicationId) &&
|
|
1262
1556
|
(!this.urlBasedNavigation || isUrlBasedTab);
|
|
@@ -1297,7 +1591,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1297
1591
|
}
|
|
1298
1592
|
// Check if this is a system tab (not associated with a registered app)
|
|
1299
1593
|
if (tabAppId === SYSTEM_APP_ID) {
|
|
1300
|
-
this.
|
|
1594
|
+
this.IsViewingSystemTab = true;
|
|
1301
1595
|
this.cdr.detectChanges();
|
|
1302
1596
|
// Don't try to set active app - SYSTEM_APP_ID has no registered app
|
|
1303
1597
|
// Update browser title with just the tab title (no app context)
|
|
@@ -1305,7 +1599,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1305
1599
|
return;
|
|
1306
1600
|
}
|
|
1307
1601
|
// Not a system tab - clear the flag
|
|
1308
|
-
this.
|
|
1602
|
+
this.IsViewingSystemTab = false;
|
|
1309
1603
|
this.cdr.detectChanges();
|
|
1310
1604
|
// Records style: records-REGION tabs are a GLOBAL
|
|
1311
1605
|
// surface — viewing one must never flip the user's app context. Without
|
|
@@ -1315,7 +1609,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1315
1609
|
// stays wherever the user is. Records DOCKED to the workspace fall
|
|
1316
1610
|
// through: they are ordinary main-layout tabs and DO flip app context.
|
|
1317
1611
|
if (this.RecordTabsStyle && IsRecordsRegionTab(activeTab.configuration)) {
|
|
1318
|
-
this.titleService.setContext(this.
|
|
1612
|
+
this.titleService.setContext(this.ActiveApp?.Name || null, activeTab.title || null);
|
|
1319
1613
|
return;
|
|
1320
1614
|
}
|
|
1321
1615
|
// Check if active app needs to be updated
|
|
@@ -2037,15 +2331,23 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2037
2331
|
* user stays within their scoped app. Data access is still enforced
|
|
2038
2332
|
* server-side by the user's role; this is the UI-confinement layer.
|
|
2039
2333
|
*/
|
|
2040
|
-
get
|
|
2334
|
+
get AppSwitchingLocked() {
|
|
2041
2335
|
return !!this.authBase.GetSessionScope()?.restrictedToApplicationId;
|
|
2042
2336
|
}
|
|
2043
|
-
|
|
2337
|
+
/** @deprecated Use {@link AppSwitchingLocked}. */
|
|
2338
|
+
get appSwitchingLocked() {
|
|
2339
|
+
return this.AppSwitchingLocked;
|
|
2340
|
+
}
|
|
2341
|
+
OnFirstResourceLoadComplete() {
|
|
2044
2342
|
this.waitingForFirstResource = false;
|
|
2045
|
-
this.
|
|
2343
|
+
this.Loading = false;
|
|
2046
2344
|
this.stopLoadingAnimation();
|
|
2047
2345
|
this.cdr.detectChanges();
|
|
2048
2346
|
}
|
|
2347
|
+
/** @deprecated Use {@link OnFirstResourceLoadComplete}. */
|
|
2348
|
+
onFirstResourceLoadComplete() {
|
|
2349
|
+
return this.OnFirstResourceLoadComplete();
|
|
2350
|
+
}
|
|
2049
2351
|
/**
|
|
2050
2352
|
* Start the loading message cycling animation.
|
|
2051
2353
|
* Messages cycle every 2.5 seconds without repeating until all are used.
|
|
@@ -2102,7 +2404,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2102
2404
|
startLoadingResetTimer() {
|
|
2103
2405
|
this.cancelLoadingResetTimer();
|
|
2104
2406
|
this.loadingResetTimeout = setTimeout(() => {
|
|
2105
|
-
if (this.
|
|
2407
|
+
if (this.Loading) {
|
|
2106
2408
|
this.ShowResetOption = true;
|
|
2107
2409
|
this.cdr.detectChanges();
|
|
2108
2410
|
}
|
|
@@ -2168,24 +2470,24 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2168
2470
|
*/
|
|
2169
2471
|
initializeLoadingDisplay() {
|
|
2170
2472
|
// Set first message
|
|
2171
|
-
this.
|
|
2172
|
-
if (this.activeTheme.
|
|
2473
|
+
this.CurrentLoadingText = this.activeTheme.Messages[0];
|
|
2474
|
+
if (this.activeTheme.StaticColors) {
|
|
2173
2475
|
// Standard theme: keep MJ blue, no gradient
|
|
2174
|
-
this.
|
|
2175
|
-
this.
|
|
2176
|
-
this.
|
|
2476
|
+
this.CurrentLoadingColor = this.activeTheme.Colors[0];
|
|
2477
|
+
this.CurrentLoadingTextColor = '#757575'; // Default gray text
|
|
2478
|
+
this.CurrentLoadingGradient = null;
|
|
2177
2479
|
}
|
|
2178
2480
|
else {
|
|
2179
2481
|
// Themed period: use theme colors and first gradient from the start
|
|
2180
|
-
this.
|
|
2181
|
-
this.
|
|
2482
|
+
this.CurrentLoadingColor = this.activeTheme.Colors[0];
|
|
2483
|
+
this.CurrentLoadingTextColor = this.activeTheme.Colors[0];
|
|
2182
2484
|
// Set initial gradient if theme has gradients
|
|
2183
|
-
if (this.activeTheme.
|
|
2184
|
-
this.
|
|
2485
|
+
if (this.activeTheme.Gradients && this.activeTheme.Gradients.length > 0) {
|
|
2486
|
+
this.CurrentLoadingGradient = this.activeTheme.Gradients[0];
|
|
2185
2487
|
this.usedGradientIndices = [0];
|
|
2186
2488
|
}
|
|
2187
2489
|
else {
|
|
2188
|
-
this.
|
|
2490
|
+
this.CurrentLoadingGradient = null;
|
|
2189
2491
|
}
|
|
2190
2492
|
}
|
|
2191
2493
|
}
|
|
@@ -2195,7 +2497,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2195
2497
|
* For standard theme, colors remain static.
|
|
2196
2498
|
*/
|
|
2197
2499
|
cycleToNextMessage() {
|
|
2198
|
-
const messages = this.activeTheme.
|
|
2500
|
+
const messages = this.activeTheme.Messages;
|
|
2199
2501
|
this.messageCycleCount++;
|
|
2200
2502
|
// If we've used all messages, reset the used list (but exclude current to avoid immediate repeat)
|
|
2201
2503
|
if (this.usedMessageIndices.length >= messages.length) {
|
|
@@ -2210,10 +2512,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2210
2512
|
this.usedMessageIndices.push(randomIndex);
|
|
2211
2513
|
this.loadingMessageIndex = randomIndex;
|
|
2212
2514
|
// Update the message
|
|
2213
|
-
this.
|
|
2515
|
+
this.CurrentLoadingText = this.activeTheme.Messages[randomIndex];
|
|
2214
2516
|
// Check if it's time to change colors (every 2nd message = every 5 seconds)
|
|
2215
2517
|
// But only for non-static themes
|
|
2216
|
-
if (!this.activeTheme.
|
|
2518
|
+
if (!this.activeTheme.StaticColors && this.messageCycleCount % this.colorChangeEveryNMessages === 0) {
|
|
2217
2519
|
this.cycleToNextColor();
|
|
2218
2520
|
}
|
|
2219
2521
|
}
|
|
@@ -2224,8 +2526,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2224
2526
|
*/
|
|
2225
2527
|
cycleToNextColor() {
|
|
2226
2528
|
// Cycle to next gradient if available
|
|
2227
|
-
if (this.activeTheme.
|
|
2228
|
-
const gradients = this.activeTheme.
|
|
2529
|
+
if (this.activeTheme.Gradients && this.activeTheme.Gradients.length > 0) {
|
|
2530
|
+
const gradients = this.activeTheme.Gradients;
|
|
2229
2531
|
// If we've used all gradients, reset (but exclude current to avoid immediate repeat)
|
|
2230
2532
|
if (this.usedGradientIndices.length >= gradients.length) {
|
|
2231
2533
|
const lastUsed = this.usedGradientIndices[this.usedGradientIndices.length - 1];
|
|
@@ -2235,15 +2537,15 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2235
2537
|
const currentIndex = this.usedGradientIndices[this.usedGradientIndices.length - 1] ?? -1;
|
|
2236
2538
|
const nextIndex = (currentIndex + 1) % gradients.length;
|
|
2237
2539
|
this.usedGradientIndices.push(nextIndex);
|
|
2238
|
-
this.
|
|
2540
|
+
this.CurrentLoadingGradient = gradients[nextIndex];
|
|
2239
2541
|
}
|
|
2240
2542
|
// Also cycle text color through theme colors
|
|
2241
|
-
const colors = this.activeTheme.
|
|
2543
|
+
const colors = this.activeTheme.Colors;
|
|
2242
2544
|
if (colors.length > 1) {
|
|
2243
2545
|
// Get a random color from the theme for text
|
|
2244
2546
|
const randomIndex = Math.floor(Math.random() * colors.length);
|
|
2245
|
-
this.
|
|
2246
|
-
this.
|
|
2547
|
+
this.CurrentLoadingColor = colors[randomIndex];
|
|
2548
|
+
this.CurrentLoadingTextColor = colors[randomIndex];
|
|
2247
2549
|
}
|
|
2248
2550
|
}
|
|
2249
2551
|
/**
|
|
@@ -2252,22 +2554,22 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2252
2554
|
*/
|
|
2253
2555
|
initializeAnimationFromTheme() {
|
|
2254
2556
|
this.currentAnimationIndex = 0;
|
|
2255
|
-
const themeAnimations = this.activeTheme.
|
|
2557
|
+
const themeAnimations = this.activeTheme.Animations;
|
|
2256
2558
|
if (!themeAnimations) {
|
|
2257
2559
|
// No animation config - use random selection for non-standard themes
|
|
2258
2560
|
if (this.activeTheme.id === 'standard') {
|
|
2259
2561
|
// Standard theme defaults to pulse only
|
|
2260
|
-
this.animationSequence = [{
|
|
2562
|
+
this.animationSequence = [{ Type: 'pulse' }];
|
|
2261
2563
|
}
|
|
2262
2564
|
else {
|
|
2263
2565
|
// Random selection for themed holidays without explicit config
|
|
2264
2566
|
const randomType = this.allAnimationTypes[Math.floor(Math.random() * this.allAnimationTypes.length)];
|
|
2265
|
-
this.animationSequence = [{
|
|
2567
|
+
this.animationSequence = [{ Type: randomType }];
|
|
2266
2568
|
}
|
|
2267
2569
|
}
|
|
2268
2570
|
else if (typeof themeAnimations === 'string') {
|
|
2269
2571
|
// Single animation type specified
|
|
2270
|
-
this.animationSequence = [{
|
|
2572
|
+
this.animationSequence = [{ Type: themeAnimations }];
|
|
2271
2573
|
}
|
|
2272
2574
|
else {
|
|
2273
2575
|
// Array of animation steps
|
|
@@ -2275,7 +2577,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2275
2577
|
}
|
|
2276
2578
|
// Set initial animation
|
|
2277
2579
|
if (this.animationSequence.length > 0) {
|
|
2278
|
-
this.
|
|
2580
|
+
this.CurrentLoadingAnimation = this.animationSequence[0].Type;
|
|
2279
2581
|
}
|
|
2280
2582
|
}
|
|
2281
2583
|
/**
|
|
@@ -2299,7 +2601,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2299
2601
|
return;
|
|
2300
2602
|
}
|
|
2301
2603
|
const currentStep = this.animationSequence[this.currentAnimationIndex];
|
|
2302
|
-
const durationMs = currentStep.
|
|
2604
|
+
const durationMs = currentStep.DurationMs;
|
|
2303
2605
|
// If no duration specified (or 0), this step runs indefinitely
|
|
2304
2606
|
if (!durationMs || durationMs <= 0) {
|
|
2305
2607
|
return;
|
|
@@ -2319,7 +2621,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2319
2621
|
return;
|
|
2320
2622
|
}
|
|
2321
2623
|
const nextStep = this.animationSequence[this.currentAnimationIndex];
|
|
2322
|
-
this.
|
|
2624
|
+
this.CurrentLoadingAnimation = nextStep.Type;
|
|
2323
2625
|
this.cdr.detectChanges();
|
|
2324
2626
|
// Schedule the next transition if this step has a duration
|
|
2325
2627
|
this.scheduleNextAnimationStep();
|
|
@@ -2336,11 +2638,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2336
2638
|
/**
|
|
2337
2639
|
* Handle app switch from app switcher
|
|
2338
2640
|
*/
|
|
2339
|
-
async
|
|
2641
|
+
async OnAppSwitch(appId) {
|
|
2340
2642
|
// Clear the system tab flag since we're switching to a real app
|
|
2341
|
-
this.
|
|
2643
|
+
this.IsViewingSystemTab = false;
|
|
2342
2644
|
// Show loading indicator in app switcher
|
|
2343
|
-
this.
|
|
2645
|
+
this.LoadingAppId = appId;
|
|
2344
2646
|
this.cdr.detectChanges();
|
|
2345
2647
|
try {
|
|
2346
2648
|
const app = this.appManager.GetAppById(appId);
|
|
@@ -2349,11 +2651,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2349
2651
|
const systemApp = this.appManager.GetAllSystemApps().find(a => UUIDsEqual(a.ID, appId));
|
|
2350
2652
|
const appName = systemApp?.Name || 'this application';
|
|
2351
2653
|
// Clear loading indicator before showing dialog
|
|
2352
|
-
this.
|
|
2654
|
+
this.LoadingAppId = null;
|
|
2353
2655
|
this.cdr.detectChanges();
|
|
2354
2656
|
// Show "Add Application?" dialog
|
|
2355
|
-
if (this.
|
|
2356
|
-
this.
|
|
2657
|
+
if (this.AppAccessDialog) {
|
|
2658
|
+
this.AppAccessDialog.show({
|
|
2357
2659
|
type: 'not_installed',
|
|
2358
2660
|
appName: appName,
|
|
2359
2661
|
appId: appId
|
|
@@ -2405,10 +2707,14 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2405
2707
|
}
|
|
2406
2708
|
finally {
|
|
2407
2709
|
// Clear loading indicator
|
|
2408
|
-
this.
|
|
2710
|
+
this.LoadingAppId = null;
|
|
2409
2711
|
this.cdr.detectChanges();
|
|
2410
2712
|
}
|
|
2411
2713
|
}
|
|
2714
|
+
/** @deprecated Use {@link OnAppSwitch}. */
|
|
2715
|
+
async onAppSwitch(appId) {
|
|
2716
|
+
return this.OnAppSwitch(appId);
|
|
2717
|
+
}
|
|
2412
2718
|
/**
|
|
2413
2719
|
* Find an existing tab that matches the default nav item
|
|
2414
2720
|
*/
|
|
@@ -2436,22 +2742,30 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2436
2742
|
/**
|
|
2437
2743
|
* Handle Nav Bar app icon click (single click switches to app)
|
|
2438
2744
|
*/
|
|
2439
|
-
|
|
2745
|
+
OnNavBarAppClick(app, event) {
|
|
2440
2746
|
// If shift key is held, force new tab
|
|
2441
2747
|
if (event.shiftKey) {
|
|
2442
2748
|
this.openNavBarAppInNewTab(app);
|
|
2443
2749
|
}
|
|
2444
2750
|
else {
|
|
2445
|
-
this.
|
|
2751
|
+
this.OnAppSwitch(app.ID);
|
|
2446
2752
|
}
|
|
2447
2753
|
}
|
|
2754
|
+
/** @deprecated Use {@link OnNavBarAppClick}. */
|
|
2755
|
+
onNavBarAppClick(app, event) {
|
|
2756
|
+
return this.OnNavBarAppClick(app, event);
|
|
2757
|
+
}
|
|
2448
2758
|
/**
|
|
2449
2759
|
* Handle Nav Bar app icon double-click (opens in new tab)
|
|
2450
2760
|
*/
|
|
2451
|
-
|
|
2761
|
+
OnNavBarAppDblClick(app, event) {
|
|
2452
2762
|
event.preventDefault();
|
|
2453
2763
|
this.openNavBarAppInNewTab(app);
|
|
2454
2764
|
}
|
|
2765
|
+
/** @deprecated Use {@link OnNavBarAppDblClick}. */
|
|
2766
|
+
onNavBarAppDblClick(app, event) {
|
|
2767
|
+
return this.OnNavBarAppDblClick(app, event);
|
|
2768
|
+
}
|
|
2455
2769
|
/**
|
|
2456
2770
|
* Open a nav bar app's default content in a new tab
|
|
2457
2771
|
*/
|
|
@@ -2460,7 +2774,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2460
2774
|
const tabRequest = await app.CreateDefaultTab();
|
|
2461
2775
|
if (tabRequest) {
|
|
2462
2776
|
// Set the app as active first if it isn't already
|
|
2463
|
-
if (!UUIDsEqual(this.
|
|
2777
|
+
if (!UUIDsEqual(this.ActiveApp?.ID, app.ID)) {
|
|
2464
2778
|
await this.appManager.SetActiveApp(app.ID);
|
|
2465
2779
|
}
|
|
2466
2780
|
// Open in new tab by adding to workspace
|
|
@@ -2470,49 +2784,65 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2470
2784
|
/**
|
|
2471
2785
|
* Handle navigation item click with shift-key detection
|
|
2472
2786
|
*/
|
|
2473
|
-
|
|
2474
|
-
if (!this.
|
|
2787
|
+
OnNavItemClick(event) {
|
|
2788
|
+
if (!this.ActiveApp) {
|
|
2475
2789
|
return;
|
|
2476
2790
|
}
|
|
2477
2791
|
const { item, shiftKey } = event;
|
|
2478
2792
|
// Close mobile nav if open. Routed through closeMobileNav() rather than setting the flag
|
|
2479
2793
|
// directly so focus does not get dropped to <body> when the drawer goes inert — tapping a
|
|
2480
2794
|
// nav item is the most common way the drawer closes on a phone.
|
|
2481
|
-
this.
|
|
2795
|
+
this.CloseMobileNav();
|
|
2482
2796
|
// Use NavigationService with forceNewTab option if shift was pressed
|
|
2483
|
-
this.navigationService.OpenNavItem(this.
|
|
2797
|
+
this.navigationService.OpenNavItem(this.ActiveApp.ID, item, this.ActiveApp.GetColor(), { forceNewTab: shiftKey });
|
|
2798
|
+
}
|
|
2799
|
+
/** @deprecated Use {@link OnNavItemClick}. */
|
|
2800
|
+
onNavItemClick(event) {
|
|
2801
|
+
return this.OnNavItemClick(event);
|
|
2484
2802
|
}
|
|
2485
2803
|
/**
|
|
2486
2804
|
* Handle dismiss of a dynamic nav item (remove from recent stack)
|
|
2487
2805
|
*/
|
|
2488
|
-
|
|
2489
|
-
if (!this.
|
|
2806
|
+
OnNavItemDismiss(item) {
|
|
2807
|
+
if (!this.ActiveApp) {
|
|
2490
2808
|
return;
|
|
2491
2809
|
}
|
|
2492
2810
|
// Delegate to HomeApplication's RemoveDynamicNavItem if available
|
|
2493
|
-
const appWithRemove = this.
|
|
2811
|
+
const appWithRemove = this.ActiveApp;
|
|
2494
2812
|
if (typeof appWithRemove.RemoveDynamicNavItem === 'function') {
|
|
2495
2813
|
appWithRemove.RemoveDynamicNavItem(item);
|
|
2496
2814
|
}
|
|
2497
2815
|
}
|
|
2816
|
+
/** @deprecated Use {@link OnNavItemDismiss}. */
|
|
2817
|
+
onNavItemDismiss(item) {
|
|
2818
|
+
return this.OnNavItemDismiss(item);
|
|
2819
|
+
}
|
|
2498
2820
|
/**
|
|
2499
2821
|
* Toggle mobile navigation drawer
|
|
2500
2822
|
*/
|
|
2501
|
-
|
|
2502
|
-
this.
|
|
2503
|
-
if (!this.
|
|
2823
|
+
ToggleMobileNav() {
|
|
2824
|
+
this.MobileNavOpen = !this.MobileNavOpen;
|
|
2825
|
+
if (!this.MobileNavOpen) {
|
|
2504
2826
|
this.returnFocusFromMobileNav();
|
|
2505
2827
|
}
|
|
2506
2828
|
}
|
|
2829
|
+
/** @deprecated Use {@link ToggleMobileNav}. */
|
|
2830
|
+
toggleMobileNav() {
|
|
2831
|
+
return this.ToggleMobileNav();
|
|
2832
|
+
}
|
|
2507
2833
|
/**
|
|
2508
2834
|
* Close mobile navigation drawer
|
|
2509
2835
|
*/
|
|
2510
|
-
|
|
2511
|
-
if (this.
|
|
2512
|
-
this.
|
|
2836
|
+
CloseMobileNav() {
|
|
2837
|
+
if (this.MobileNavOpen) {
|
|
2838
|
+
this.MobileNavOpen = false;
|
|
2513
2839
|
this.returnFocusFromMobileNav();
|
|
2514
2840
|
}
|
|
2515
2841
|
}
|
|
2842
|
+
/** @deprecated Use {@link CloseMobileNav}. */
|
|
2843
|
+
closeMobileNav() {
|
|
2844
|
+
return this.CloseMobileNav();
|
|
2845
|
+
}
|
|
2516
2846
|
/**
|
|
2517
2847
|
* When the drawer closes it becomes `inert`, which drops focus to <body> if focus was
|
|
2518
2848
|
* inside it — the user's place in the page is gone and the next Tab restarts at the top.
|
|
@@ -2520,12 +2850,12 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2520
2850
|
* (e.g. the drawer was closed from a keyboard shortcut while the user was in the content).
|
|
2521
2851
|
*/
|
|
2522
2852
|
returnFocusFromMobileNav() {
|
|
2523
|
-
const drawer = this.
|
|
2853
|
+
const drawer = this.MobileNavDrawer?.nativeElement;
|
|
2524
2854
|
const active = document.activeElement;
|
|
2525
2855
|
if (!drawer || !active || !drawer.contains(active)) {
|
|
2526
2856
|
return;
|
|
2527
2857
|
}
|
|
2528
|
-
this.
|
|
2858
|
+
this.MobileNavToggle?.nativeElement.focus();
|
|
2529
2859
|
}
|
|
2530
2860
|
/**
|
|
2531
2861
|
* Accessible name for the avatar button. The name must never depend on the avatar image
|
|
@@ -2533,15 +2863,19 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2533
2863
|
* name of its own — so it is set on the BUTTON and falls back to a generic label before
|
|
2534
2864
|
* the user record has loaded.
|
|
2535
2865
|
*/
|
|
2536
|
-
get
|
|
2866
|
+
get AvatarAriaLabel() {
|
|
2537
2867
|
return this.userName ? `Account: ${this.userName}` : 'Account';
|
|
2538
2868
|
}
|
|
2869
|
+
/** @deprecated Use {@link AvatarAriaLabel}. */
|
|
2870
|
+
get avatarAriaLabel() {
|
|
2871
|
+
return this.AvatarAriaLabel;
|
|
2872
|
+
}
|
|
2539
2873
|
/**
|
|
2540
2874
|
* Skip link target handoff (WCAG 2.4.1). Focus is moved programmatically rather than left
|
|
2541
2875
|
* to the browser's fragment navigation: the shell's main region is a Golden Layout host,
|
|
2542
2876
|
* and letting the URL hash change here would collide with the deep-link/tab restore path.
|
|
2543
2877
|
*/
|
|
2544
|
-
|
|
2878
|
+
SkipToMainContent(event) {
|
|
2545
2879
|
event.preventDefault();
|
|
2546
2880
|
const main = document.getElementById('mj-main-content');
|
|
2547
2881
|
if (!main) {
|
|
@@ -2550,18 +2884,22 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2550
2884
|
main.focus();
|
|
2551
2885
|
main.scrollIntoView({ block: 'start' });
|
|
2552
2886
|
}
|
|
2887
|
+
/** @deprecated Use {@link SkipToMainContent}. */
|
|
2888
|
+
skipToMainContent(event) {
|
|
2889
|
+
return this.SkipToMainContent(event);
|
|
2890
|
+
}
|
|
2553
2891
|
/**
|
|
2554
2892
|
* Toggle user menu visibility
|
|
2555
2893
|
*/
|
|
2556
|
-
|
|
2894
|
+
ToggleUserMenu(event) {
|
|
2557
2895
|
event.stopPropagation();
|
|
2558
|
-
this.
|
|
2559
|
-
if (this.
|
|
2896
|
+
this.UserMenuVisible = !this.UserMenuVisible;
|
|
2897
|
+
if (this.UserMenuVisible) {
|
|
2560
2898
|
// Close menu when clicking outside. CAPTURE phase so clicks whose
|
|
2561
2899
|
// bubbling something stopped (e.g. the origin crumb's GL-focus
|
|
2562
2900
|
// stoppers) still dismiss the menu.
|
|
2563
2901
|
const closeHandler = () => {
|
|
2564
|
-
this.
|
|
2902
|
+
this.UserMenuVisible = false;
|
|
2565
2903
|
document.removeEventListener('click', closeHandler, true);
|
|
2566
2904
|
};
|
|
2567
2905
|
setTimeout(() => {
|
|
@@ -2569,6 +2907,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2569
2907
|
}, 0);
|
|
2570
2908
|
}
|
|
2571
2909
|
}
|
|
2910
|
+
/** @deprecated Use {@link ToggleUserMenu}. */
|
|
2911
|
+
toggleUserMenu(event) {
|
|
2912
|
+
return this.ToggleUserMenu(event);
|
|
2913
|
+
}
|
|
2572
2914
|
/**
|
|
2573
2915
|
* Initialize the user menu plugin system
|
|
2574
2916
|
*/
|
|
@@ -2581,7 +2923,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2581
2923
|
}
|
|
2582
2924
|
// Initialize developer mode service
|
|
2583
2925
|
if (this.userEntity) {
|
|
2584
|
-
await this.
|
|
2926
|
+
await this.DeveloperModeService.Initialize(this.userEntity);
|
|
2585
2927
|
}
|
|
2586
2928
|
// Check org-level feedback kill switch (defaults to enabled on error)
|
|
2587
2929
|
const feedbackEnabled = await this.feedbackService.IsEnabled();
|
|
@@ -2591,9 +2933,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2591
2933
|
userEntity: this.userEntity,
|
|
2592
2934
|
shell: this,
|
|
2593
2935
|
viewContainerRef: this.viewContainerRef,
|
|
2594
|
-
isDeveloper: this.
|
|
2595
|
-
developerModeEnabled: this.
|
|
2596
|
-
currentApplication: this.
|
|
2936
|
+
isDeveloper: this.DeveloperModeService.IsDeveloper,
|
|
2937
|
+
developerModeEnabled: this.DeveloperModeService.IsEnabled,
|
|
2938
|
+
currentApplication: this.ActiveApp,
|
|
2597
2939
|
workspaceManager: this.workspaceManager,
|
|
2598
2940
|
authService: this.authBase,
|
|
2599
2941
|
pinService: this.homePinService,
|
|
@@ -2610,11 +2952,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2610
2952
|
// Get initial menu elements
|
|
2611
2953
|
this.refreshMenuElements();
|
|
2612
2954
|
// Subscribe to developer mode changes to refresh menu
|
|
2613
|
-
this.
|
|
2955
|
+
this.DeveloperModeService.IsEnabled$.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
2614
2956
|
// Update context and refresh menu
|
|
2615
2957
|
if (this.userMenu) {
|
|
2616
2958
|
this.userMenu.UpdateContext({
|
|
2617
|
-
developerModeEnabled: this.
|
|
2959
|
+
developerModeEnabled: this.DeveloperModeService.IsEnabled
|
|
2618
2960
|
});
|
|
2619
2961
|
}
|
|
2620
2962
|
this.refreshMenuElements();
|
|
@@ -2638,13 +2980,13 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2638
2980
|
*/
|
|
2639
2981
|
refreshMenuElements() {
|
|
2640
2982
|
if (this.userMenu) {
|
|
2641
|
-
this.
|
|
2983
|
+
this.UserMenuElements = this.userMenu.GetMenuElements();
|
|
2642
2984
|
}
|
|
2643
2985
|
}
|
|
2644
2986
|
/**
|
|
2645
2987
|
* Handle user menu item click
|
|
2646
2988
|
*/
|
|
2647
|
-
async
|
|
2989
|
+
async OnUserMenuItemClick(itemId) {
|
|
2648
2990
|
if (!this.userMenu)
|
|
2649
2991
|
return;
|
|
2650
2992
|
const result = await this.userMenu.HandleItemClick(itemId);
|
|
@@ -2665,12 +3007,12 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2665
3007
|
}
|
|
2666
3008
|
}
|
|
2667
3009
|
if (result.message === 'reset-layout') {
|
|
2668
|
-
await this.
|
|
3010
|
+
await this.OnResetLayout();
|
|
2669
3011
|
return;
|
|
2670
3012
|
}
|
|
2671
3013
|
if (result.message === 'pin-to-home') {
|
|
2672
3014
|
// Close menu and show overlay immediately before any async work
|
|
2673
|
-
this.
|
|
3015
|
+
this.UserMenuVisible = false;
|
|
2674
3016
|
this.refreshMenuElements();
|
|
2675
3017
|
this.showPinProgress('Pinning...');
|
|
2676
3018
|
// Let the UI render the overlay before starting the work
|
|
@@ -2689,38 +3031,42 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2689
3031
|
return;
|
|
2690
3032
|
}
|
|
2691
3033
|
if (result.message === 'sharing-center') {
|
|
2692
|
-
this.
|
|
3034
|
+
this.UserMenuVisible = false;
|
|
2693
3035
|
this.userSharingCenterDialogService.open(this.viewContainerRef);
|
|
2694
3036
|
return;
|
|
2695
3037
|
}
|
|
2696
3038
|
if (result.message === 'submit-feedback') {
|
|
2697
|
-
this.
|
|
3039
|
+
this.UserMenuVisible = false;
|
|
2698
3040
|
this.ShowFeedbackDialog();
|
|
2699
3041
|
return;
|
|
2700
3042
|
}
|
|
2701
3043
|
if (result.message === 'about') {
|
|
2702
|
-
this.
|
|
3044
|
+
this.UserMenuVisible = false;
|
|
2703
3045
|
this.aboutDialogService.open(this.viewContainerRef, {
|
|
2704
|
-
avatarUrl: this.
|
|
2705
|
-
avatarIconClass: this.
|
|
3046
|
+
avatarUrl: this.UserImageURL || null,
|
|
3047
|
+
avatarIconClass: this.UserIconClass || null
|
|
2706
3048
|
});
|
|
2707
3049
|
return;
|
|
2708
3050
|
}
|
|
2709
3051
|
if (result.message === 'profile') {
|
|
2710
|
-
this.
|
|
3052
|
+
this.UserMenuVisible = false;
|
|
2711
3053
|
this.profileDialogService.open(this.viewContainerRef, {
|
|
2712
|
-
avatarUrl: this.
|
|
2713
|
-
avatarIconClass: this.
|
|
3054
|
+
avatarUrl: this.UserImageURL || null,
|
|
3055
|
+
avatarIconClass: this.UserIconClass || null
|
|
2714
3056
|
});
|
|
2715
3057
|
return;
|
|
2716
3058
|
}
|
|
2717
3059
|
if (result.closeMenu) {
|
|
2718
|
-
this.
|
|
3060
|
+
this.UserMenuVisible = false;
|
|
2719
3061
|
}
|
|
2720
3062
|
// Refresh menu elements (some items may have changed state)
|
|
2721
3063
|
this.refreshMenuElements();
|
|
2722
3064
|
this.cdr.detectChanges();
|
|
2723
3065
|
}
|
|
3066
|
+
/** @deprecated Use {@link OnUserMenuItemClick}. */
|
|
3067
|
+
async onUserMenuItemClick(itemId) {
|
|
3068
|
+
return this.OnUserMenuItemClick(itemId);
|
|
3069
|
+
}
|
|
2724
3070
|
/**
|
|
2725
3071
|
* Whether the currently applied theme is dark (used by the toggle switch in the template)
|
|
2726
3072
|
*/
|
|
@@ -2731,28 +3077,44 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2731
3077
|
/**
|
|
2732
3078
|
* Get user menu options for template
|
|
2733
3079
|
*/
|
|
2734
|
-
|
|
3080
|
+
GetUserMenuOptions() {
|
|
2735
3081
|
return this.userMenu?.GetOptions();
|
|
2736
3082
|
}
|
|
3083
|
+
/** @deprecated Use {@link GetUserMenuOptions}. */
|
|
3084
|
+
getUserMenuOptions() {
|
|
3085
|
+
return this.GetUserMenuOptions();
|
|
3086
|
+
}
|
|
2737
3087
|
/**
|
|
2738
3088
|
* Get user display info for template
|
|
2739
3089
|
*/
|
|
2740
|
-
|
|
3090
|
+
GetUserDisplayInfo() {
|
|
2741
3091
|
return this.userMenu?.GetUserDisplayInfo();
|
|
2742
3092
|
}
|
|
3093
|
+
/** @deprecated Use {@link GetUserDisplayInfo}. */
|
|
3094
|
+
getUserDisplayInfo() {
|
|
3095
|
+
return this.GetUserDisplayInfo();
|
|
3096
|
+
}
|
|
2743
3097
|
/**
|
|
2744
3098
|
* Check if an element is a divider (for template)
|
|
2745
3099
|
*/
|
|
2746
|
-
|
|
3100
|
+
IsMenuDivider(element) {
|
|
2747
3101
|
return isUserMenuDivider(element);
|
|
2748
3102
|
}
|
|
3103
|
+
/** @deprecated Use {@link IsMenuDivider}. */
|
|
3104
|
+
isMenuDivider(element) {
|
|
3105
|
+
return this.IsMenuDivider(element);
|
|
3106
|
+
}
|
|
2749
3107
|
/**
|
|
2750
3108
|
* Cast element to UserMenuItem (for template type safety)
|
|
2751
3109
|
* Call this only after checking !isMenuDivider(element)
|
|
2752
3110
|
*/
|
|
2753
|
-
|
|
3111
|
+
AsMenuItem(element) {
|
|
2754
3112
|
return element;
|
|
2755
3113
|
}
|
|
3114
|
+
/** @deprecated Use {@link AsMenuItem}. */
|
|
3115
|
+
asMenuItem(element) {
|
|
3116
|
+
return this.AsMenuItem(element);
|
|
3117
|
+
}
|
|
2756
3118
|
/**
|
|
2757
3119
|
* Pin the currently active resource to the Home dashboard
|
|
2758
3120
|
*/
|
|
@@ -2779,10 +3141,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2779
3141
|
}
|
|
2780
3142
|
// Resolve nav item icon for Custom pins
|
|
2781
3143
|
let pinIcon;
|
|
2782
|
-
if (resourceType === 'Custom' && this.
|
|
3144
|
+
if (resourceType === 'Custom' && this.ActiveApp) {
|
|
2783
3145
|
const navItemName = activeTab.configuration?.['navItemName'];
|
|
2784
3146
|
if (navItemName) {
|
|
2785
|
-
const navItems = await this.
|
|
3147
|
+
const navItems = await this.ActiveApp.GetNavItems();
|
|
2786
3148
|
const navItem = navItems.find(ni => ni.Label === navItemName);
|
|
2787
3149
|
pinIcon = navItem?.Icon || undefined;
|
|
2788
3150
|
}
|
|
@@ -2791,9 +3153,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2791
3153
|
DisplayName: displayName,
|
|
2792
3154
|
ResourceType: resourceType,
|
|
2793
3155
|
ApplicationID: activeTab.applicationId,
|
|
2794
|
-
ApplicationName: this.
|
|
3156
|
+
ApplicationName: this.ActiveApp?.Name,
|
|
2795
3157
|
Icon: pinIcon,
|
|
2796
|
-
Color: this.
|
|
3158
|
+
Color: this.ActiveApp?.GetColor() || undefined,
|
|
2797
3159
|
Configuration: activeTab.configuration
|
|
2798
3160
|
});
|
|
2799
3161
|
if (added) {
|
|
@@ -2813,9 +3175,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2813
3175
|
*/
|
|
2814
3176
|
async captureAndAttachThumbnail(tab, resourceType) {
|
|
2815
3177
|
try {
|
|
2816
|
-
if (!this.
|
|
3178
|
+
if (!this.TabContainerRef)
|
|
2817
3179
|
return;
|
|
2818
|
-
const thumbnail = await this.
|
|
3180
|
+
const thumbnail = await this.TabContainerRef.CaptureActiveThumbnail();
|
|
2819
3181
|
if (thumbnail) {
|
|
2820
3182
|
const pin = this.homePinService.FindPin(resourceType, tab.configuration);
|
|
2821
3183
|
if (pin) {
|
|
@@ -2887,19 +3249,23 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2887
3249
|
/**
|
|
2888
3250
|
* Log current workspace configuration to console (debug)
|
|
2889
3251
|
*/
|
|
2890
|
-
|
|
3252
|
+
OnLogLayout() {
|
|
2891
3253
|
const config = this.workspaceManager.GetConfiguration();
|
|
2892
3254
|
console.log('📋 Workspace Configuration:', JSON.stringify(config, null, 2));
|
|
2893
3255
|
console.log('📋 Workspace Configuration (object):', config);
|
|
2894
|
-
this.
|
|
3256
|
+
this.UserMenuVisible = false;
|
|
3257
|
+
}
|
|
3258
|
+
/** @deprecated Use {@link OnLogLayout}. */
|
|
3259
|
+
onLogLayout() {
|
|
3260
|
+
return this.OnLogLayout();
|
|
2895
3261
|
}
|
|
2896
3262
|
/**
|
|
2897
3263
|
* Reset workspace layout - clears all tabs and switches to single-resource mode
|
|
2898
3264
|
*/
|
|
2899
|
-
async
|
|
2900
|
-
this.
|
|
3265
|
+
async OnResetLayout() {
|
|
3266
|
+
this.UserMenuVisible = false;
|
|
2901
3267
|
// Get current active app to create a fresh default tab
|
|
2902
|
-
const currentApp = this.
|
|
3268
|
+
const currentApp = this.ActiveApp;
|
|
2903
3269
|
if (!currentApp) {
|
|
2904
3270
|
console.warn('No active app to reset to');
|
|
2905
3271
|
return;
|
|
@@ -2943,42 +3309,22 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2943
3309
|
// Update workspace configuration
|
|
2944
3310
|
this.workspaceManager.UpdateConfiguration(freshConfig);
|
|
2945
3311
|
}
|
|
3312
|
+
/** @deprecated Use {@link OnResetLayout}. */
|
|
3313
|
+
async onResetLayout() {
|
|
3314
|
+
return this.OnResetLayout();
|
|
3315
|
+
}
|
|
2946
3316
|
/**
|
|
2947
3317
|
* Logout user and clear authentication data
|
|
2948
3318
|
*/
|
|
2949
|
-
async
|
|
2950
|
-
this.
|
|
3319
|
+
async OnLogout() {
|
|
3320
|
+
this.UserMenuVisible = false;
|
|
2951
3321
|
this.authBase.logout();
|
|
2952
3322
|
localStorage.removeItem('auth');
|
|
2953
3323
|
localStorage.removeItem('claims');
|
|
2954
3324
|
}
|
|
2955
|
-
/**
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
*/
|
|
2959
|
-
handleGlobalKeyboardShortcuts(event) {
|
|
2960
|
-
// Skip if user is typing in an input/textarea — EXCEPT when the omnibar is on:
|
|
2961
|
-
// a modal palette is summonable from anywhere (incl. the chat composer), like
|
|
2962
|
-
// Slack/Linear. The legacy path keeps the guard (its binding steals focus).
|
|
2963
|
-
const target = event.target;
|
|
2964
|
-
const inEditable = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
2965
|
-
if (inEditable && !this.UseOmnibar) {
|
|
2966
|
-
return;
|
|
2967
|
-
}
|
|
2968
|
-
// Platform detection
|
|
2969
|
-
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
2970
|
-
const isCtrlOrCmd = isMac ? event.metaKey : event.ctrlKey;
|
|
2971
|
-
// Cmd+/ or Ctrl+/ opens the palette (omnibar '/' mode when enabled, legacy otherwise)
|
|
2972
|
-
if (isCtrlOrCmd && event.key === '/') {
|
|
2973
|
-
event.preventDefault();
|
|
2974
|
-
event.stopPropagation();
|
|
2975
|
-
if (this.UseOmnibar) {
|
|
2976
|
-
this.OpenOmnibar('/');
|
|
2977
|
-
}
|
|
2978
|
-
else {
|
|
2979
|
-
this.commandPaletteService.Open();
|
|
2980
|
-
}
|
|
2981
|
-
}
|
|
3325
|
+
/** @deprecated Use {@link OnLogout}. */
|
|
3326
|
+
async onLogout() {
|
|
3327
|
+
return this.OnLogout();
|
|
2982
3328
|
}
|
|
2983
3329
|
/**
|
|
2984
3330
|
* Load user avatar from database, auto-sync from auth provider if needed
|
|
@@ -2987,7 +3333,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2987
3333
|
try {
|
|
2988
3334
|
const md = this.ProviderToUse;
|
|
2989
3335
|
this.userName = currentUserInfo.FirstLast || currentUserInfo.Name || 'User';
|
|
2990
|
-
this.
|
|
3336
|
+
this.UserEmail = currentUserInfo.Email || '';
|
|
2991
3337
|
// Load the full MJUserEntity to access avatar fields
|
|
2992
3338
|
const currentUserEntity = await md.GetEntityObject('MJ: Users');
|
|
2993
3339
|
await currentUserEntity.Load(currentUserInfo.ID);
|
|
@@ -3007,8 +3353,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3007
3353
|
catch (error) {
|
|
3008
3354
|
console.warn('Could not load user avatar:', error);
|
|
3009
3355
|
// Use fallback
|
|
3010
|
-
this.
|
|
3011
|
-
this.
|
|
3356
|
+
this.UserImageURL = '';
|
|
3357
|
+
this.UserIconClass = null;
|
|
3012
3358
|
}
|
|
3013
3359
|
}
|
|
3014
3360
|
/**
|
|
@@ -3035,17 +3381,17 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3035
3381
|
*/
|
|
3036
3382
|
applyUserAvatar(user) {
|
|
3037
3383
|
if (user.UserImageURL) {
|
|
3038
|
-
this.
|
|
3039
|
-
this.
|
|
3384
|
+
this.UserImageURL = user.UserImageURL;
|
|
3385
|
+
this.UserIconClass = null;
|
|
3040
3386
|
}
|
|
3041
3387
|
else if (user.UserImageIconClass) {
|
|
3042
|
-
this.
|
|
3043
|
-
this.
|
|
3388
|
+
this.UserIconClass = user.UserImageIconClass;
|
|
3389
|
+
this.UserImageURL = '';
|
|
3044
3390
|
}
|
|
3045
3391
|
else {
|
|
3046
3392
|
// Default fallback - show icon
|
|
3047
|
-
this.
|
|
3048
|
-
this.
|
|
3393
|
+
this.UserImageURL = '';
|
|
3394
|
+
this.UserIconClass = null;
|
|
3049
3395
|
}
|
|
3050
3396
|
this.cdr.detectChanges();
|
|
3051
3397
|
}
|
|
@@ -3057,7 +3403,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3057
3403
|
// Find the active tab
|
|
3058
3404
|
const activeTab = config.tabs?.find(tab => tab.id === config.activeTabId);
|
|
3059
3405
|
// Get app name
|
|
3060
|
-
const appName = this.
|
|
3406
|
+
const appName = this.ActiveApp?.Name || null;
|
|
3061
3407
|
// Get resource name from active tab
|
|
3062
3408
|
let resourceName = null;
|
|
3063
3409
|
if (activeTab) {
|
|
@@ -3074,15 +3420,15 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3074
3420
|
*/
|
|
3075
3421
|
async loadSearchableEntities() {
|
|
3076
3422
|
const md = this.ProviderToUse;
|
|
3077
|
-
this.
|
|
3078
|
-
if (this.
|
|
3079
|
-
this.
|
|
3423
|
+
this.SearchableEntities = md.Entities.filter((e) => e.AllowUserSearchAPI).sort((a, b) => a.Name.localeCompare(b.Name));
|
|
3424
|
+
if (this.SearchableEntities.length > 0) {
|
|
3425
|
+
this.SelectedEntity = this.SearchableEntities[0];
|
|
3080
3426
|
}
|
|
3081
3427
|
}
|
|
3082
3428
|
/**
|
|
3083
3429
|
* Toggle search popup visibility
|
|
3084
3430
|
*/
|
|
3085
|
-
|
|
3431
|
+
ToggleSearch() {
|
|
3086
3432
|
if (!this.ShowSearchBar) {
|
|
3087
3433
|
return;
|
|
3088
3434
|
}
|
|
@@ -3090,36 +3436,44 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3090
3436
|
this.OpenOmnibar();
|
|
3091
3437
|
return;
|
|
3092
3438
|
}
|
|
3093
|
-
this.
|
|
3439
|
+
this.IsSearchOpen = !this.IsSearchOpen;
|
|
3094
3440
|
// Focus on search input when opened
|
|
3095
|
-
if (this.
|
|
3441
|
+
if (this.IsSearchOpen) {
|
|
3096
3442
|
setTimeout(() => {
|
|
3097
|
-
if (this.
|
|
3098
|
-
this.
|
|
3443
|
+
if (this.SearchInput && this.SearchInput.nativeElement) {
|
|
3444
|
+
this.SearchInput.nativeElement.focus();
|
|
3099
3445
|
}
|
|
3100
3446
|
}, 100);
|
|
3101
3447
|
}
|
|
3102
3448
|
}
|
|
3449
|
+
/** @deprecated Use {@link ToggleSearch}. */
|
|
3450
|
+
toggleSearch() {
|
|
3451
|
+
return this.ToggleSearch();
|
|
3452
|
+
}
|
|
3103
3453
|
/**
|
|
3104
3454
|
* Close search popup
|
|
3105
3455
|
*/
|
|
3456
|
+
CloseSearch() {
|
|
3457
|
+
this.IsSearchOpen = false;
|
|
3458
|
+
}
|
|
3459
|
+
/** @deprecated Use {@link CloseSearch}. */
|
|
3106
3460
|
closeSearch() {
|
|
3107
|
-
this.
|
|
3461
|
+
return this.CloseSearch();
|
|
3108
3462
|
}
|
|
3109
3463
|
/**
|
|
3110
3464
|
* Handle search submission
|
|
3111
3465
|
*/
|
|
3112
|
-
|
|
3113
|
-
if (!this.
|
|
3466
|
+
OnSearch(event) {
|
|
3467
|
+
if (!this.SearchInput) {
|
|
3114
3468
|
return;
|
|
3115
3469
|
}
|
|
3116
|
-
const inputValue = this.
|
|
3470
|
+
const inputValue = this.SearchInput.nativeElement.value;
|
|
3117
3471
|
if (inputValue && inputValue.length > 0 && inputValue.trim().length > 2) {
|
|
3118
|
-
this.
|
|
3119
|
-
this.
|
|
3472
|
+
this.SearchInput.nativeElement.value = ''; // Clear input
|
|
3473
|
+
this.IsSearchOpen = false; // Close search popup
|
|
3120
3474
|
// Navigate to search results
|
|
3121
|
-
if (this.
|
|
3122
|
-
this.router.navigate(['resource', 'search', inputValue], { queryParams: { Entity: this.
|
|
3475
|
+
if (this.SelectedEntity) {
|
|
3476
|
+
this.router.navigate(['resource', 'search', inputValue], { queryParams: { Entity: this.SelectedEntity.Name } });
|
|
3123
3477
|
}
|
|
3124
3478
|
}
|
|
3125
3479
|
else {
|
|
@@ -3136,23 +3490,60 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3136
3490
|
});
|
|
3137
3491
|
}
|
|
3138
3492
|
}
|
|
3493
|
+
/** @deprecated Use {@link OnSearch}. */
|
|
3494
|
+
onSearch(event) {
|
|
3495
|
+
return this.OnSearch(event);
|
|
3496
|
+
}
|
|
3139
3497
|
// ========================================
|
|
3140
3498
|
// UNIVERSAL SEARCH EVENT HANDLERS
|
|
3141
3499
|
// ========================================
|
|
3500
|
+
/**
|
|
3501
|
+
* The shell's ONLY `document:keydown` listener — handles every global chord.
|
|
3502
|
+
*
|
|
3503
|
+
* ⚠️ Do NOT add a second `@HostListener('document:keydown')` to this component.
|
|
3504
|
+
* Angular's compiler collects host listeners into an object keyed by event name,
|
|
3505
|
+
* so a second declaration for the same event SILENTLY REPLACES the first — no
|
|
3506
|
+
* build error, no warning, the earlier handler simply never registers. That is
|
|
3507
|
+
* exactly what happened here: a separate `handleGlobalKeyboardShortcuts` owned
|
|
3508
|
+
* Ctrl+/ and was overwritten by this method, so the command palette shortcut was
|
|
3509
|
+
* dead for its entire life. Every new global chord belongs in this one method.
|
|
3510
|
+
*
|
|
3511
|
+
* - **Ctrl/Cmd+K** summons search. Omnibar on: the modal palette opens from
|
|
3512
|
+
* anywhere, even while typing (explicit chord, Slack/Linear semantics). Omnibar
|
|
3513
|
+
* off: the chord FOCUSES the inline header composite on desktop (results attach
|
|
3514
|
+
* beneath it), so keep the don't-steal-focus-mid-typing guard; on mobile (no
|
|
3515
|
+
* composite rendered) it opens the Spotlight overlay instead.
|
|
3516
|
+
* - **Ctrl/Cmd+/** opens the command palette, even from an input — the chord can
|
|
3517
|
+
* never be mistaken for ordinary typing. It yields only when an element under
|
|
3518
|
+
* focus already handled it (`defaultPrevented`): CodeMirror binds Mod-/ to toggle
|
|
3519
|
+
* comment and calls `preventDefault()` without stopping propagation.
|
|
3520
|
+
*/
|
|
3142
3521
|
OnGlobalKeydown(event) {
|
|
3143
|
-
// Ctrl/Cmd+K summons search. Omnibar: the modal palette opens from anywhere,
|
|
3144
|
-
// even while typing (explicit chord, Slack/Linear semantics). Legacy: the
|
|
3145
|
-
// chord FOCUSES the inline header composite on desktop (results attach
|
|
3146
|
-
// beneath it), so keep the don't-steal-focus-mid-typing guard; on mobile
|
|
3147
|
-
// (no composite rendered) it opens the Spotlight overlay instead.
|
|
3148
|
-
const target = event.target;
|
|
3149
|
-
const inEditable = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
3150
|
-
if (inEditable && !this.UseOmnibar) {
|
|
3151
|
-
return;
|
|
3152
|
-
}
|
|
3153
3522
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
3154
3523
|
const isCtrlOrCmd = isMac ? event.metaKey : event.ctrlKey;
|
|
3155
|
-
if (isCtrlOrCmd
|
|
3524
|
+
if (!isCtrlOrCmd) {
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
if (event.key === '/') {
|
|
3528
|
+
if (event.defaultPrevented) {
|
|
3529
|
+
return;
|
|
3530
|
+
}
|
|
3531
|
+
event.preventDefault();
|
|
3532
|
+
event.stopPropagation();
|
|
3533
|
+
if (this.UseOmnibar) {
|
|
3534
|
+
this.OpenOmnibar('/');
|
|
3535
|
+
}
|
|
3536
|
+
else {
|
|
3537
|
+
this.commandPaletteService.Open();
|
|
3538
|
+
}
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
if (event.key === 'k') {
|
|
3542
|
+
const target = event.target;
|
|
3543
|
+
const inEditable = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
3544
|
+
if (inEditable && !this.UseOmnibar) {
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3156
3547
|
event.preventDefault();
|
|
3157
3548
|
event.stopPropagation();
|
|
3158
3549
|
this.OnHeaderSearchClick();
|
|
@@ -3172,10 +3563,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3172
3563
|
if (this.UseOmnibar) {
|
|
3173
3564
|
this.OpenOmnibar();
|
|
3174
3565
|
}
|
|
3175
|
-
else if (!isMobile && this.
|
|
3566
|
+
else if (!isMobile && this.ShellSearchComposite?.Focus) {
|
|
3176
3567
|
// Omnibar-off desktop: the inline composite is on screen — focus it and
|
|
3177
3568
|
// let its attached suggest dropdown do the work.
|
|
3178
|
-
this.
|
|
3569
|
+
this.ShellSearchComposite.Focus();
|
|
3179
3570
|
}
|
|
3180
3571
|
else {
|
|
3181
3572
|
// Omnibar-off mobile: no visible inline composite — open the Spotlight overlay.
|
|
@@ -3217,12 +3608,20 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3217
3608
|
this.LegacySearchOpen = false;
|
|
3218
3609
|
this.navigationService.OpenSearch(query);
|
|
3219
3610
|
}
|
|
3611
|
+
/** Command palette "Search everything" row → full Search Results workspace. Does
|
|
3612
|
+
nothing while the chrome hides search, like the other search entry points. */
|
|
3613
|
+
OnCommandPaletteSearch(query) {
|
|
3614
|
+
if (!this.ShowSearchBar || !query.trim()) {
|
|
3615
|
+
return;
|
|
3616
|
+
}
|
|
3617
|
+
this.navigationService.OpenSearch(query);
|
|
3618
|
+
}
|
|
3220
3619
|
/** Inline composite submit (Enter) → full Search Results workspace, carrying the
|
|
3221
3620
|
composite's relevance/scope selections. */
|
|
3222
3621
|
OnSearchSubmitted(query) {
|
|
3223
3622
|
if (query && query.trim().length >= 2) {
|
|
3224
|
-
const minRelevance = this.
|
|
3225
|
-
const scopeIDs = this.
|
|
3623
|
+
const minRelevance = this.ShellSearchComposite?.MinRelevancePercent;
|
|
3624
|
+
const scopeIDs = this.ShellSearchComposite?.SelectedScopeIDs;
|
|
3226
3625
|
const opts = {};
|
|
3227
3626
|
if (minRelevance)
|
|
3228
3627
|
opts.minRelevance = minRelevance;
|
|
@@ -3242,19 +3641,27 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3242
3641
|
* Show notifications page as a tab
|
|
3243
3642
|
*/
|
|
3244
3643
|
/** Toggle the global Activity drawer. */
|
|
3245
|
-
|
|
3644
|
+
ToggleActivity(event) {
|
|
3246
3645
|
event.stopPropagation();
|
|
3247
|
-
this.
|
|
3646
|
+
this.ActivityOpen = !this.ActivityOpen;
|
|
3248
3647
|
this.cdr.detectChanges();
|
|
3249
3648
|
}
|
|
3649
|
+
/** @deprecated Use {@link ToggleActivity}. */
|
|
3650
|
+
toggleActivity(event) {
|
|
3651
|
+
return this.ToggleActivity(event);
|
|
3652
|
+
}
|
|
3250
3653
|
/** Clear finished activities from the tracker. */
|
|
3251
|
-
|
|
3654
|
+
ClearFinishedActivity() {
|
|
3252
3655
|
this.activityService.ClearFinished();
|
|
3253
3656
|
}
|
|
3657
|
+
/** @deprecated Use {@link ClearFinishedActivity}. */
|
|
3658
|
+
clearFinishedActivity() {
|
|
3659
|
+
return this.ClearFinishedActivity();
|
|
3660
|
+
}
|
|
3254
3661
|
/** Close the Activity drawer when clicking anywhere outside it. */
|
|
3255
3662
|
onDocumentClickCloseActivity() {
|
|
3256
|
-
if (this.
|
|
3257
|
-
this.
|
|
3663
|
+
if (this.ActivityOpen) {
|
|
3664
|
+
this.ActivityOpen = false;
|
|
3258
3665
|
this.cdr.detectChanges();
|
|
3259
3666
|
}
|
|
3260
3667
|
}
|
|
@@ -3304,8 +3711,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3304
3711
|
async captureAndAttachFeedbackScreenshot() {
|
|
3305
3712
|
try {
|
|
3306
3713
|
let screenshot;
|
|
3307
|
-
if (this.
|
|
3308
|
-
screenshot = await this.
|
|
3714
|
+
if (this.TabContainerRef) {
|
|
3715
|
+
screenshot = await this.TabContainerRef.CaptureActiveThumbnail();
|
|
3309
3716
|
}
|
|
3310
3717
|
if (!screenshot) {
|
|
3311
3718
|
const contentEl = document.querySelector('.shell-content')
|
|
@@ -3351,8 +3758,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3351
3758
|
// Show the dialog on top of the loading screen
|
|
3352
3759
|
// Use setTimeout to ensure the dialog component is ready after view init
|
|
3353
3760
|
setTimeout(() => {
|
|
3354
|
-
if (this.
|
|
3355
|
-
this.
|
|
3761
|
+
if (this.AppAccessDialog) {
|
|
3762
|
+
this.AppAccessDialog.show(dialogConfig);
|
|
3356
3763
|
}
|
|
3357
3764
|
else {
|
|
3358
3765
|
// Fallback if dialog not available - redirect to a working app
|
|
@@ -3410,24 +3817,24 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3410
3817
|
LogStatus('User has no applications available');
|
|
3411
3818
|
// Stop loading animation and show the dialog
|
|
3412
3819
|
this.stopLoadingAnimation();
|
|
3413
|
-
this.
|
|
3820
|
+
this.Loading = false;
|
|
3414
3821
|
this.cdr.detectChanges();
|
|
3415
3822
|
setTimeout(() => {
|
|
3416
|
-
if (this.
|
|
3417
|
-
this.
|
|
3823
|
+
if (this.AppAccessDialog) {
|
|
3824
|
+
this.AppAccessDialog.show({ type: 'no_apps' });
|
|
3418
3825
|
}
|
|
3419
3826
|
}, 0);
|
|
3420
3827
|
}
|
|
3421
3828
|
/**
|
|
3422
3829
|
* Handle Golden Layout initialization failure
|
|
3423
3830
|
*/
|
|
3424
|
-
|
|
3831
|
+
HandleLayoutError() {
|
|
3425
3832
|
LogStatus('Golden Layout initialization failed');
|
|
3426
3833
|
const availableApps = this.appManager.GetAllApps();
|
|
3427
3834
|
if (availableApps.length > 0) {
|
|
3428
3835
|
setTimeout(() => {
|
|
3429
|
-
if (this.
|
|
3430
|
-
this.
|
|
3836
|
+
if (this.AppAccessDialog) {
|
|
3837
|
+
this.AppAccessDialog.show({ type: 'layout_error' });
|
|
3431
3838
|
}
|
|
3432
3839
|
else {
|
|
3433
3840
|
// Direct redirect if dialog not available
|
|
@@ -3436,10 +3843,14 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3436
3843
|
}, 0);
|
|
3437
3844
|
}
|
|
3438
3845
|
}
|
|
3846
|
+
/** @deprecated Use {@link HandleLayoutError}. */
|
|
3847
|
+
handleLayoutError() {
|
|
3848
|
+
return this.HandleLayoutError();
|
|
3849
|
+
}
|
|
3439
3850
|
/**
|
|
3440
3851
|
* Handle dialog result (install, enable, or redirect)
|
|
3441
3852
|
*/
|
|
3442
|
-
async
|
|
3853
|
+
async OnAppAccessDialogResult(result) {
|
|
3443
3854
|
const availableApps = this.appManager.GetAllApps();
|
|
3444
3855
|
switch (result.action) {
|
|
3445
3856
|
case 'install':
|
|
@@ -3459,6 +3870,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3459
3870
|
break;
|
|
3460
3871
|
}
|
|
3461
3872
|
}
|
|
3873
|
+
/** @deprecated Use {@link OnAppAccessDialogResult}. */
|
|
3874
|
+
async onAppAccessDialogResult(result) {
|
|
3875
|
+
return this.OnAppAccessDialogResult(result);
|
|
3876
|
+
}
|
|
3462
3877
|
/**
|
|
3463
3878
|
* Install an app for the user and navigate to it
|
|
3464
3879
|
*/
|
|
@@ -3475,13 +3890,13 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3475
3890
|
}
|
|
3476
3891
|
else {
|
|
3477
3892
|
console.error('[ShellComponent] Failed to add application');
|
|
3478
|
-
this.
|
|
3893
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3479
3894
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3480
3895
|
}
|
|
3481
3896
|
}
|
|
3482
3897
|
catch (error) {
|
|
3483
3898
|
console.error('Error adding app:', error);
|
|
3484
|
-
this.
|
|
3899
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3485
3900
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3486
3901
|
}
|
|
3487
3902
|
}
|
|
@@ -3496,13 +3911,13 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3496
3911
|
await this.waitForAppAndNavigate(appId);
|
|
3497
3912
|
}
|
|
3498
3913
|
else {
|
|
3499
|
-
this.
|
|
3914
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3500
3915
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3501
3916
|
}
|
|
3502
3917
|
}
|
|
3503
3918
|
catch (error) {
|
|
3504
3919
|
console.error('Error enabling app:', error);
|
|
3505
|
-
this.
|
|
3920
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3506
3921
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3507
3922
|
}
|
|
3508
3923
|
}
|
|
@@ -3518,7 +3933,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3518
3933
|
const app = this.appManager.GetAppById(appId);
|
|
3519
3934
|
if (app) {
|
|
3520
3935
|
await this.navigateToApp(app);
|
|
3521
|
-
this.
|
|
3936
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3522
3937
|
return;
|
|
3523
3938
|
}
|
|
3524
3939
|
// Wait a bit before checking again
|
|
@@ -3528,11 +3943,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3528
3943
|
const systemApp = this.appManager.GetAllSystemApps().find(a => UUIDsEqual(a.ID, appId));
|
|
3529
3944
|
if (systemApp) {
|
|
3530
3945
|
await this.navigateToApp(systemApp);
|
|
3531
|
-
this.
|
|
3946
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3532
3947
|
}
|
|
3533
3948
|
else {
|
|
3534
3949
|
console.warn(`[ShellComponent] App ${appId} not found after waiting, redirecting to fallback app`);
|
|
3535
|
-
this.
|
|
3950
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3536
3951
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3537
3952
|
}
|
|
3538
3953
|
}
|
|
@@ -3553,7 +3968,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3553
3968
|
}
|
|
3554
3969
|
/** Case-insensitive UUID check whether an app is the currently active app. */
|
|
3555
3970
|
IsActiveApp(app) {
|
|
3556
|
-
return UUIDsEqual(app.ID, this.
|
|
3971
|
+
return UUIDsEqual(app.ID, this.ActiveApp?.ID);
|
|
3557
3972
|
}
|
|
3558
3973
|
/**
|
|
3559
3974
|
* Candidate order for landing and fallback navigation: the declared-default app
|
|
@@ -3614,7 +4029,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3614
4029
|
}
|
|
3615
4030
|
}
|
|
3616
4031
|
// No apps available - this shouldn't happen, but handle gracefully
|
|
3617
|
-
this.
|
|
4032
|
+
this.Loading = false;
|
|
3618
4033
|
this.cdr.detectChanges();
|
|
3619
4034
|
}
|
|
3620
4035
|
static ɵfac = function ShellComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ShellComponent)(i0.ɵɵdirectiveInject(i1.ApplicationManager), i0.ɵɵdirectiveInject(i1.WorkspaceStateManager), i0.ɵɵdirectiveInject(i1.GoldenLayoutManager), i0.ɵɵdirectiveInject(i1.TabService), i0.ɵɵdirectiveInject(i2.NavigationService), i0.ɵɵdirectiveInject(i3.ActivatedRoute), i0.ɵɵdirectiveInject(i3.Router), i0.ɵɵdirectiveInject(i4.MJAuthBase), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i5.UserAvatarService), i0.ɵɵdirectiveInject(i6.UserSharingCenterDialogService), i0.ɵɵdirectiveInject(i7.AboutDialogService), i0.ɵɵdirectiveInject(i8.ProfileDialogService), i0.ɵɵdirectiveInject(i0.ViewContainerRef), i0.ɵɵdirectiveInject(i2.TitleService), i0.ɵɵdirectiveInject(i2.DeveloperModeService), i0.ɵɵdirectiveInject(i9.StartupValidationService), i0.ɵɵdirectiveInject(i10.CommandPaletteService), i0.ɵɵdirectiveInject(i2.ThemeService), i0.ɵɵdirectiveInject(i2.HomeAppPinService), i0.ɵɵdirectiveInject(i11.FileOpenService), i0.ɵɵdirectiveInject(i12.FeedbackDialogService), i0.ɵɵdirectiveInject(i12.FeedbackService), i0.ɵɵdirectiveInject(i2.ActivityService)); };
|
|
@@ -3622,16 +4037,16 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3622
4037
|
i0.ɵɵviewQuery(_c0, 5)(_c1, 5)(_c2, 5)(_c3, 5)(_c4, 5)(TabContainerComponent, 5)(_c5, 5);
|
|
3623
4038
|
} if (rf & 2) {
|
|
3624
4039
|
let _t;
|
|
3625
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3626
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3627
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3628
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3629
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3630
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3631
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
4040
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.SearchInput = _t.first);
|
|
4041
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.MobileNavDrawer = _t.first);
|
|
4042
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.MobileNavToggle = _t.first);
|
|
4043
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.OmnibarPalette = _t.first);
|
|
4044
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.ShellSearchComposite = _t.first);
|
|
4045
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.TabContainerRef = _t.first);
|
|
4046
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.AppAccessDialog = _t.first);
|
|
3632
4047
|
} }, hostBindings: function ShellComponent_HostBindings(rf, ctx) { if (rf & 1) {
|
|
3633
4048
|
i0.ɵɵlistener("keydown", function ShellComponent_keydown_HostBindingHandler($event) { return ctx.OnGlobalKeydown($event); }, i0.ɵɵresolveDocument)("click", function ShellComponent_click_HostBindingHandler() { return ctx.onDocumentClickCloseActivity(); }, i0.ɵɵresolveDocument);
|
|
3634
|
-
} }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 55, vars:
|
|
4049
|
+
} }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 55, vars: 36, consts: [["mobileNavToggle", ""], ["searchInput", ""], ["mobileNavDrawer", ""], ["appAccessDialog", ""], ["shellSearchComposite", ""], ["omnibarPalette", ""], [1, "shell-container"], ["href", "#mj-main-content", 1, "mj-skip-link", 3, "click"], [1, "shell-header"], ["title", "MemberJunction", 1, "mj-logo"], ["title", "Menu", "aria-label", "Menu", "aria-controls", "mj-mobile-nav-drawer", 1, "hamburger-btn", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-bars"], [1, "nav-bar-apps", "left-of-switcher"], [3, "activeApp", "isViewingSystemTab", "loadingAppId", "SwitcherStyle", "Provider"], [1, "desktop-nav", 3, "app", "OverflowEnabled"], [1, "desktop-nav"], [1, "spacer"], [1, "header-actions"], [1, "activity-wrap"], ["title", "Notifications", 1, "icon-btn", "desktop-only", "notification-btn"], [1, "nav-bar-apps", "left-of-user-menu"], [1, "user-menu"], [1, "avatar-btn", 3, "click"], ["alt", "", "aria-hidden", "true", 1, "avatar-img", 3, "src"], ["aria-hidden", "true", 1, "icon-fallback"], [1, "user-context-menu", 3, "menu-fade", "menu-slide"], [1, "search-popup-overlay"], [1, "search-popup"], [1, "search-popup-content", 3, "click"], ["TextField", "Name", "ValueField", "ID", "AriaLabel", "Search in", 1, "search-entity-dropdown", 3, "ngModelChange", "Data", "ngModel"], ["type", "text", "placeholder", "Search...", "aria-label", "Search", 1, "mj-input", "search-input", 3, "keydown.enter"], ["title", "Search", 1, "search-submit-btn", 3, "click"], [1, "fa-solid", "fa-search"], [1, "mobile-nav-overlay"], ["id", "mj-mobile-nav-drawer", 1, "mobile-nav-drawer"], [1, "mobile-nav-header"], ["aria-label", "Close navigation", 1, "close-btn", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-xmark"], [1, "mobile-nav-content"], [1, "mobile-nav-records"], [1, "mobile-nav-footer"], ["title", "Search", 1, "mobile-nav-action", 3, "click"], ["title", "Notifications", 1, "mobile-nav-action"], ["id", "mj-main-content", "role", "main", "tabindex", "-1", 3, "firstResourceLoadComplete", "layoutInitError"], [1, "shell-loading"], [3, "result"], [3, "AppSelected", "KnowledgeSearchRequested", "ShowSearch"], ["Placeholder", "Search everything...", 3, "IsOpen", "EnableGlobalShortcut", "ShowPromo", "PromoText"], [1, "pin-progress-backdrop"], [1, "nav-bar-app-btn", 3, "active", "title", "--app-color"], [1, "nav-bar-app-btn", 3, "click", "dblclick", "title"], [3, "appSelected", "activeApp", "isViewingSystemTab", "loadingAppId", "SwitcherStyle", "Provider"], [1, "desktop-nav", 3, "navItemClick", "navItemDismiss", "app", "OverflowEnabled"], ["title", "Search everything", 1, "search-btn"], ["title", "Search everything", 1, "search-btn", 3, "click"], [1, "fa-solid", "fa-magnifying-glass"], [1, "search-btn-text", "desktop-only"], ["aria-hidden", "true", 1, "search-btn-kbd", "desktop-only"], [1, "shell-search-bar", "desktop-only"], ["Placeholder", "Search everything...", 3, "ResultSelected", "SearchSubmitted", "SeeAllRequested", "RecentRecordSelected", "PromoAccepted", "PromoDismissed", "ShowShortcutHint", "EnablePreview", "EnableRecent", "MaxPreviewResults", "ShowPromo", "PromoText"], ["title", "Search everything", 1, "search-btn", "search-btn-mobile", 3, "click"], ["title", "Activity", 1, "icon-btn", "desktop-only", "activity-btn", 3, "click"], [1, "fa-solid", "fa-wave-square"], [1, "activity-badge"], [1, "activity-drawer"], [1, "activity-drawer", 3, "click"], [1, "activity-drawer-head"], ["title", "Clear finished", 1, "activity-clear", 3, "click"], [1, "fa-solid", "fa-broom"], [1, "activity-list"], [1, "activity-row", 3, "is-error"], [1, "activity-row"], [1, "activity-row-icon"], [1, "activity-row-body"], [1, "activity-row-label"], [1, "activity-row-detail"], [1, "activity-bar"], [1, "activity-bar-fill"], ["title", "Notifications", 1, "icon-btn", "desktop-only", "notification-btn", 3, "click"], [1, "fa-solid", "fa-bell"], [1, "notification-badge"], [1, "user-context-menu"], [1, "user-menu-header"], [1, "user-info"], [1, "user-name"], [1, "user-email"], [1, "user-subtitle"], [1, "user-menu-divider"], [1, "user-menu-item", "theme-toggle-item", 3, "title"], [1, "user-menu-item", 3, "disabled", "danger", "color", "title"], [1, "user-menu-item", "theme-toggle-item", 3, "click", "title"], [1, "menu-label"], [1, "theme-toggle-track"], [1, "theme-toggle-thumb"], [1, "theme-toggle-icon"], [1, "user-menu-item", 3, "click", "title"], [1, "menu-shortcut"], [1, "search-popup-overlay", 3, "click"], [1, "mobile-nav-overlay", 3, "click"], [1, "mobile-nav-section-title"], [3, "navItemClick", "navItemDismiss", "app", "OverflowEnabled"], [3, "Activated"], ["title", "Notifications", 1, "mobile-nav-action", 3, "click"], [1, "notification-badge-mobile"], ["size", "large", 3, "text", "textColor", "logoColor", "logoGradient", "animation"], [1, "loading-reset-panel"], [1, "loading-version"], [1, "loading-reset-message"], [1, "fa-regular", "fa-clock"], [1, "loading-reset-hint"], [1, "loading-reset-btn", 3, "click"], [1, "fa-solid", "fa-arrows-rotate"], [3, "SettingsRequested"], ["Placeholder", "Search everything...", 3, "IsOpenChange", "ResultSelected", "SeeAllRequested", "PromoAccepted", "PromoDismissed", "IsOpen", "EnableGlobalShortcut", "ShowPromo", "PromoText"], [1, "pin-progress-modal"], ["size", "small", 3, "showText"], [1, "pin-progress-text"]], template: function ShellComponent_Template(rf, ctx) { if (rf & 1) {
|
|
3635
4050
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
3636
4051
|
i0.ɵɵelementStart(0, "div", 6)(1, "a", 7);
|
|
3637
4052
|
i0.ɵɵlistener("click", function ShellComponent_Template_a_click_1_listener($event) { return ctx.skipToMainContent($event); });
|
|
@@ -3697,7 +4112,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3697
4112
|
i0.ɵɵlistener("result", function ShellComponent_Template_mj_app_access_dialog_result_49_listener($event) { return ctx.onAppAccessDialogResult($event); });
|
|
3698
4113
|
i0.ɵɵelementEnd();
|
|
3699
4114
|
i0.ɵɵelementStart(51, "mj-command-palette", 46);
|
|
3700
|
-
i0.ɵɵlistener("AppSelected", function ShellComponent_Template_mj_command_palette_AppSelected_51_listener($event) { return ctx.onAppSwitch($event); });
|
|
4115
|
+
i0.ɵɵlistener("AppSelected", function ShellComponent_Template_mj_command_palette_AppSelected_51_listener($event) { return ctx.onAppSwitch($event); })("KnowledgeSearchRequested", function ShellComponent_Template_mj_command_palette_KnowledgeSearchRequested_51_listener($event) { return ctx.OnCommandPaletteSearch($event); });
|
|
3701
4116
|
i0.ɵɵelementEnd();
|
|
3702
4117
|
i0.ɵɵconditionalCreate(52, ShellComponent_Conditional_52_Template, 2, 0, "mj-omnibar-palette");
|
|
3703
4118
|
i0.ɵɵconditionalCreate(53, ShellComponent_Conditional_53_Template, 1, 4, "mj-search-overlay", 47);
|
|
@@ -3749,7 +4164,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3749
4164
|
i0.ɵɵclassProp("hide-tab-bar", !ctx.tabBarVisible);
|
|
3750
4165
|
i0.ɵɵadvance();
|
|
3751
4166
|
i0.ɵɵconditional(ctx.loading ? 48 : -1);
|
|
3752
|
-
i0.ɵɵadvance(
|
|
4167
|
+
i0.ɵɵadvance(3);
|
|
4168
|
+
i0.ɵɵproperty("ShowSearch", ctx.ShowSearchBar);
|
|
4169
|
+
i0.ɵɵadvance();
|
|
3753
4170
|
i0.ɵɵconditional(ctx.ShowSearchBar ? 52 : -1);
|
|
3754
4171
|
i0.ɵɵadvance();
|
|
3755
4172
|
i0.ɵɵconditional(ctx.ShowSearchBar && !ctx.UseOmnibar ? 53 : -1);
|
|
@@ -3759,31 +4176,28 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3759
4176
|
}
|
|
3760
4177
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ShellComponent, [{
|
|
3761
4178
|
type: Component,
|
|
3762
|
-
args: [{ standalone: false, selector: 'mj-shell', template: "<div class=\"shell-container\" [class.hidden]=\"loading\" [class.tabs-visible]=\"tabBarVisible\">\n <!-- Skip link (WCAG 2.4.1) \u2014 MUST stay the first focusable element in the shell.\n Consuming apps that added their own skip link should remove it on upgrade. -->\n <a class=\"mj-skip-link\" href=\"#mj-main-content\" (click)=\"skipToMainContent($event)\">Skip to main content</a>\n\n <!-- Header -->\n <header class=\"shell-header\">\n <!-- MJ Logo -->\n <div class=\"mj-logo\" title=\"MemberJunction\"></div>\n\n <!-- Mobile Hamburger Button -->\n <button class=\"hamburger-btn\" #mobileNavToggle (click)=\"toggleMobileNav()\" title=\"Menu\"\n aria-label=\"Menu\" [attr.aria-expanded]=\"mobileNavOpen\" aria-controls=\"mj-mobile-nav-drawer\">\n <i class=\"fa-solid fa-bars\" aria-hidden=\"true\"></i>\n </button>\n\n <!-- Nav Bar Apps: Left of App Switcher -->\n @if (leftOfSwitcherApps.length > 0 && !appSwitchingLocked) {\n <div class=\"nav-bar-apps left-of-switcher\">\n @for (app of leftOfSwitcherApps; track app) {\n <button\n class=\"nav-bar-app-btn\"\n [class.active]=\"IsActiveApp(app)\"\n [attr.aria-current]=\"IsActiveApp(app) ? 'true' : null\"\n [title]=\"app.Name\"\n [style.--app-color]=\"app.GetColor()\"\n (click)=\"onNavBarAppClick(app, $event)\"\n (dblclick)=\"onNavBarAppDblClick(app, $event)\">\n <i [class]=\"app.Icon\"></i>\n </button>\n }\n </div>\n }\n\n <!-- App Switcher (hidden for app-locked sessions, e.g. magic-link, and\n removable per deployment via 'Shell.AppSwitcher.Enabled') -->\n @if (!appSwitchingLocked && ShowAppSwitcher) {\n <mj-app-switcher\n [activeApp]=\"activeApp\"\n [isViewingSystemTab]=\"isViewingSystemTab\"\n [loadingAppId]=\"loadingAppId\"\n [SwitcherStyle]=\"AppSwitcherStyle\"\n [Provider]=\"Provider\"\n (appSelected)=\"onAppSwitch($event)\">\n </mj-app-switcher>\n }\n\n <!-- App Navigation (desktop only; removable per deployment via 'Shell.AppNav.Enabled') -->\n @if (activeApp && ShowAppNav) {\n <mj-app-nav\n class=\"desktop-nav\"\n [app]=\"activeApp\"\n [OverflowEnabled]=\"true\"\n (navItemClick)=\"onNavItemClick($event)\"\n (navItemDismiss)=\"onNavItemDismiss($event)\">\n <!-- Records style: persistent Records pill \u2014 projected into the nav\n row's trailing slot so it sits pipe-separated right after the\n app's own items, styled as a nav pill. Appears only while any\n record is open; resumes the last-viewed record on click. -->\n @if (RecordTabsStyle) {\n <mj-records-hub-pill></mj-records-hub-pill>\n }\n </mj-app-nav>\n } @else if (RecordTabsStyle) {\n <!-- Reachability: when the app nav doesn't render (AppNav disabled per\n deployment, or no active app), open records must STILL have a way\n back \u2014 render the pill standalone in the header. -->\n <mj-records-hub-pill class=\"desktop-nav\"></mj-records-hub-pill>\n }\n\n <!-- Spacer -->\n <div class=\"spacer\"></div>\n <!-- Actions (activity, notifications, user menu) -->\n <div class=\"header-actions\">\n <!-- Universal Search \u2014 right-anchored with the action cluster, static width\n (2026-07 review consensus: visible when space permits, never resizes).\n Omnibar ON: a button styled as an input \u2014 clicking (or Ctrl/Cmd+K) opens\n the modal palette; typing happens there. Collapses to the icon circle at\n the mobile breakpoint.\n Omnibar OFF: a REAL inline input (mj-search-composite) \u2014 results attach\n beneath it as a suggest dropdown with a \"See all results\" footer. On\n mobile the composite hides and an icon button opens the Spotlight\n overlay instead (same slot, same collapse point). -->\n @if (ShowSearchBar) {\n @if (UseOmnibar) {\n <button class=\"search-btn\" title=\"Search everything\"\n [attr.aria-label]=\"'Search everything (' + OmnibarShortcutLabel + ')'\"\n (click)=\"OnHeaderSearchClick()\">\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n <span class=\"search-btn-text desktop-only\">Search everything</span>\n <kbd class=\"search-btn-kbd desktop-only\" aria-hidden=\"true\">{{ OmnibarShortcutLabel }}</kbd>\n </button>\n } @else {\n <div class=\"shell-search-bar desktop-only\">\n <mj-search-composite\n #shellSearchComposite\n Placeholder=\"Search everything...\"\n [ShowShortcutHint]=\"true\"\n [EnablePreview]=\"ShowSearchPreview\"\n [EnableRecent]=\"true\"\n [MaxPreviewResults]=\"8\"\n [ShowPromo]=\"ShowOmnibarPromo\"\n [PromoText]=\"OmnibarPromoText\"\n (ResultSelected)=\"OnSearchResultSelected($event)\"\n (SearchSubmitted)=\"OnSearchSubmitted($event)\"\n (SeeAllRequested)=\"OnSeeAllSearch($event)\"\n (RecentRecordSelected)=\"OnSearchResultSelected($event)\"\n (PromoAccepted)=\"OnOmnibarPromoAccepted($event)\"\n (PromoDismissed)=\"OnOmnibarPromoDismissed()\">\n </mj-search-composite>\n </div>\n <button class=\"search-btn search-btn-mobile\" title=\"Search everything\"\n [attr.aria-label]=\"'Search everything (' + OmnibarShortcutLabel + ')'\"\n (click)=\"OnHeaderSearchClick()\">\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n </button>\n }\n }\n <!-- Global Activity indicator -->\n @if (activityItems.length > 0) {\n <div class=\"activity-wrap\">\n <button class=\"icon-btn desktop-only activity-btn\" title=\"Activity\" (click)=\"toggleActivity($event)\">\n <i class=\"fa-solid fa-wave-square\" [class.activity-pulse]=\"activityRunningCount > 0\"></i>\n @if (activityRunningCount > 0) {\n <span class=\"activity-badge\">{{ activityRunningCount }}</span>\n }\n </button>\n @if (activityOpen) {\n <div class=\"activity-drawer\" (click)=\"$event.stopPropagation()\">\n <div class=\"activity-drawer-head\">\n <span>Activity</span>\n <button class=\"activity-clear\" (click)=\"clearFinishedActivity()\" title=\"Clear finished\">\n <i class=\"fa-solid fa-broom\"></i>\n </button>\n </div>\n <div class=\"activity-list\">\n @for (a of activityItems; track a.ID) {\n <div class=\"activity-row\" [class.is-error]=\"a.Status === 'error'\">\n <i class=\"activity-row-icon\"\n [class]=\"a.Status === 'running' ? 'fa-solid fa-spinner fa-spin'\n : a.Status === 'error' ? 'fa-solid fa-circle-exclamation'\n : 'fa-solid fa-circle-check'\"></i>\n <div class=\"activity-row-body\">\n <div class=\"activity-row-label\">{{ a.Label }}</div>\n @if (a.Detail) { <div class=\"activity-row-detail\">{{ a.Detail }}</div> }\n @if (a.Status === 'running' && a.Progress != null) {\n <div class=\"activity-bar\"><div class=\"activity-bar-fill\" [style.width.%]=\"a.Progress\"></div></div>\n }\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n @if (ShowNotifications) {\n <button class=\"icon-btn desktop-only notification-btn\" title=\"Notifications\" (click)=\"showNotifications()\">\n <i class=\"fa-solid fa-bell\"></i>\n @if (unreadNotificationCount > 0) {\n <span class=\"notification-badge\">\n {{ unreadNotificationCount > 99 ? '99+' : unreadNotificationCount }}\n </span>\n }\n </button>\n }\n <!-- Nav Bar Apps: Left of User Menu -->\n @if (leftOfUserMenuApps.length > 0) {\n <div class=\"nav-bar-apps left-of-user-menu\">\n @for (app of leftOfUserMenuApps; track app) {\n <button\n class=\"nav-bar-app-btn\"\n [class.active]=\"IsActiveApp(app)\"\n [attr.aria-current]=\"IsActiveApp(app) ? 'true' : null\"\n [title]=\"app.Name\"\n [style.--app-color]=\"app.GetColor()\"\n (click)=\"onNavBarAppClick(app, $event)\"\n (dblclick)=\"onNavBarAppDblClick(app, $event)\">\n <i [class]=\"app.Icon\"></i>\n </button>\n }\n </div>\n }\n\n <div class=\"user-menu\">\n <button class=\"avatar-btn\" (click)=\"toggleUserMenu($event)\"\n [attr.aria-label]=\"avatarAriaLabel\"\n [attr.aria-expanded]=\"userMenuVisible\">\n @if (userImageURL) {\n <img [src]=\"userImageURL\" alt=\"\" aria-hidden=\"true\" class=\"avatar-img\" />\n } @else {\n <div class=\"icon-fallback\" aria-hidden=\"true\">\n <i [class]=\"userIconClass || 'fa-solid fa-user'\"></i>\n </div>\n }\n </button>\n <!-- User Context Menu (Dynamic) -->\n @if (userMenuVisible) {\n <div class=\"user-context-menu\"\n [class.menu-fade]=\"getUserMenuOptions()?.animationStyle === 'fade'\"\n [class.menu-slide]=\"getUserMenuOptions()?.animationStyle === 'slide'\">\n <!-- User Header -->\n @if (getUserMenuOptions()?.showUserName) {\n <div class=\"user-menu-header\">\n <div class=\"user-info\">\n <span class=\"user-name\">{{ getUserDisplayInfo()?.name || userName }}</span>\n @if (getUserMenuOptions()?.showUserEmail && getUserDisplayInfo()?.email) {\n <span class=\"user-email\">\n {{ getUserDisplayInfo()?.email }}\n </span>\n }\n @if (getUserDisplayInfo()?.subtitle) {\n <span class=\"user-subtitle\">{{ getUserDisplayInfo()?.subtitle }}</span>\n }\n </div>\n </div>\n <div class=\"user-menu-divider\"></div>\n }\n <!-- Dynamic Menu Items -->\n @for (element of userMenuElements; track element) {\n <!-- Divider -->\n @if (isMenuDivider(element)) {\n <div class=\"user-menu-divider\"></div>\n }\n <!-- Menu Item -->\n @if (!isMenuDivider(element)) {\n @if (asMenuItem(element); as item) {\n @if (item.id === 'toggle-theme') {\n <div class=\"user-menu-item theme-toggle-item\"\n [title]=\"item.tooltip || ''\"\n (click)=\"onUserMenuItemClick(item.id)\">\n <i [class]=\"item.icon\"></i>\n <span class=\"menu-label\">{{ item.label }}</span>\n <div class=\"theme-toggle-track\" [class.dark]=\"IsDarkMode\">\n <div class=\"theme-toggle-thumb\">\n <i [class]=\"IsDarkMode ? 'fa-solid fa-moon' : 'fa-solid fa-sun'\" class=\"theme-toggle-icon\"></i>\n </div>\n </div>\n </div>\n } @else {\n <div class=\"user-menu-item\"\n [class.disabled]=\"!item.enabled\"\n [class.danger]=\"item.cssClass === 'danger'\"\n [style.color]=\"item.color || null\"\n [title]=\"item.tooltip || ''\"\n (click)=\"item.enabled && onUserMenuItemClick(item.id)\">\n <i [class]=\"item.icon\"></i>\n <span class=\"menu-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"menu-shortcut\">{{ item.shortcut }}</span>\n }\n </div>\n }\n }\n }\n }\n </div>\n }\n </div>\n </div>\n </header>\n\n <!-- Search Popup -->\n @if (isSearchOpen) {\n <div class=\"search-popup-overlay\" (click)=\"closeSearch()\"></div>\n }\n <div class=\"search-popup\" [class.open]=\"isSearchOpen\" [attr.inert]=\"isSearchOpen ? null : ''\">\n <div class=\"search-popup-content\" (click)=\"$event.stopPropagation()\">\n <mj-dropdown\n [Data]=\"searchableEntities\"\n TextField=\"Name\"\n ValueField=\"ID\"\n AriaLabel=\"Search in\"\n class=\"search-entity-dropdown\"\n [(ngModel)]=\"selectedEntity\">\n </mj-dropdown>\n <input\n type=\"text\"\n #searchInput\n placeholder=\"Search...\"\n aria-label=\"Search\"\n class=\"mj-input search-input\"\n (keydown.enter)=\"onSearch($event)\"\n />\n <button class=\"search-submit-btn\" (click)=\"onSearch($event)\" title=\"Search\">\n <i class=\"fa-solid fa-search\"></i>\n </button>\n </div>\n </div>\n\n <!-- Mobile Navigation Drawer -->\n @if (mobileNavOpen) {\n <div class=\"mobile-nav-overlay\" (click)=\"closeMobileNav()\"></div>\n }\n <div class=\"mobile-nav-drawer\" id=\"mj-mobile-nav-drawer\" #mobileNavDrawer\n [class.open]=\"mobileNavOpen\" [attr.inert]=\"mobileNavOpen ? null : ''\">\n <div class=\"mobile-nav-header\">\n <span>Navigation</span>\n <button class=\"close-btn\" (click)=\"closeMobileNav()\" aria-label=\"Close navigation\">\n <i class=\"fa-solid fa-xmark\" aria-hidden=\"true\"></i>\n </button>\n </div>\n @if (activeApp && ShowAppNav) {\n <div class=\"mobile-nav-content\">\n <div class=\"mobile-nav-section-title\">{{ activeApp.Name }}</div>\n <mj-app-nav\n [app]=\"activeApp\"\n [OverflowEnabled]=\"false\"\n (navItemClick)=\"onNavItemClick($event)\"\n (navItemDismiss)=\"onNavItemDismiss($event)\">\n </mj-app-nav>\n </div>\n }\n <!-- Records style: the desktop pill lives in the (mobile-hidden) app nav,\n so the drawer carries its own entry \u2014 without it, open records are\n unreachable on mobile except by re-opening them. Resumes the\n last-viewed record and closes the drawer. -->\n @if (RecordTabsStyle) {\n <div class=\"mobile-nav-records\">\n <mj-records-hub-pill (Activated)=\"closeMobileNav()\"></mj-records-hub-pill>\n </div>\n }\n <div class=\"mobile-nav-footer\">\n <button class=\"mobile-nav-action\" title=\"Search\" (click)=\"toggleSearch(); closeMobileNav()\">\n <i class=\"fa-solid fa-search\"></i>\n <span>Search</span>\n </button>\n @if (ShowNotifications) {\n <button class=\"mobile-nav-action\" title=\"Notifications\" (click)=\"showNotifications(); closeMobileNav()\">\n <i class=\"fa-solid fa-bell\"></i>\n <span>Notifications</span>\n @if (unreadNotificationCount > 0) {\n <span class=\"notification-badge-mobile\">\n {{ unreadNotificationCount > 99 ? '99+' : unreadNotificationCount }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n\n <!-- Tab Container - with dynamic tab bar visibility -->\n <mj-tab-container\n id=\"mj-main-content\"\n role=\"main\"\n tabindex=\"-1\"\n [class.hide-tab-bar]=\"!tabBarVisible\"\n (firstResourceLoadComplete)=\"onFirstResourceLoadComplete()\"\n (layoutInitError)=\"handleLayoutError()\">\n </mj-tab-container>\n</div>\n\n<!-- Loading State -->\n@if (loading) {\n <div class=\"shell-loading\">\n <mj-loading\n [text]=\"currentLoadingText\"\n [textColor]=\"currentLoadingTextColor\"\n [logoColor]=\"currentLoadingColor\"\n [logoGradient]=\"currentLoadingGradient\"\n [animation]=\"currentLoadingAnimation\"\n size=\"large\">\n </mj-loading>\n @if (ShowResetOption) {\n <div class=\"loading-reset-panel\">\n <p class=\"loading-reset-message\">\n <i class=\"fa-regular fa-clock\"></i>\n Taking longer than expected\n </p>\n <p class=\"loading-reset-hint\">\n This can happen after updates or due to cached data issues.\n </p>\n <button class=\"loading-reset-btn\" (click)=\"ResetApplication()\">\n <i class=\"fa-solid fa-arrows-rotate\"></i>\n Reset\n </button>\n </div>\n } @else {\n <div class=\"loading-version\">MemberJunction \u00B7 v{{ MJVersion }}</div>\n }\n </div>\n}\n\n<!-- App Access Error Dialog -->\n<mj-app-access-dialog\n #appAccessDialog\n (result)=\"onAppAccessDialogResult($event)\">\n</mj-app-access-dialog>\n\n<!-- Command Palette -->\n<mj-command-palette (AppSelected)=\"onAppSwitch($event)\"></mj-command-palette>\n\n<!-- Unified command palette (Shell.Omnibar.Enabled). Rendered only while the search bar is:\n when Instance Config or the host's chrome policy hides search, the palette must not\n remain reachable through Ctrl/Cmd+K. -->\n@if (ShowSearchBar) {\n <mj-omnibar-palette #omnibarPalette (SettingsRequested)=\"OnPaletteSettingsRequested()\"></mj-omnibar-palette>\n}\n\n<!-- Legacy universal search as a Spotlight-style overlay \u2014 omnibar-off MOBILE only\n (on desktop the inline header composite owns the experience; this opens from\n the mobile icon or Ctrl/Cmd+K when the composite isn't visible).\n The shell owns the Ctrl/Cmd+K chord, so the overlay's own global shortcut is off. -->\n@if (ShowSearchBar && !UseOmnibar) {\n <mj-search-overlay\n [(IsOpen)]=\"LegacySearchOpen\"\n Placeholder=\"Search everything...\"\n [EnableGlobalShortcut]=\"false\"\n [ShowPromo]=\"ShowOmnibarPromo\"\n [PromoText]=\"OmnibarPromoText\"\n (ResultSelected)=\"OnOverlayResultSelected($event)\"\n (SeeAllRequested)=\"OnOverlaySeeAll($event)\"\n (PromoAccepted)=\"OnOmnibarPromoAccepted($event)\"\n (PromoDismissed)=\"OnOmnibarPromoDismissed()\">\n </mj-search-overlay>\n}\n\n<!-- Pin Progress Overlay -->\n@if (PinProgressVisible) {\n <div class=\"pin-progress-backdrop\">\n <div class=\"pin-progress-modal\">\n <mj-loading [showText]=\"false\" size=\"small\"></mj-loading>\n <span class=\"pin-progress-text\">{{ PinProgressText }}</span>\n </div>\n </div>\n}\n", styles: [":host {\n display: block;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n/* MJ Logo - uses design tokens for theme switching & white-labeling.\n Geometry follows --mj-logo-width/-height so a differently-shaped org mark fits;\n --mj-logo-plate-bg is set by the theme engine only when a single-variant logo needs\n a legibility plate in the mode it wasn't designed for (org-theming Phase 4). */\n.mj-logo {\n width: var(--mj-logo-width, 32px);\n height: var(--mj-logo-height, 18px);\n background-image: var(--mj-logo-mark);\n background-color: var(--mj-logo-plate-bg, transparent);\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border-radius: var(--mj-radius-sm, 4px);\n flex-shrink: 0;\n}\n\n/* Skip link (WCAG 2.4.1) \u2014 off-screen until focused, then pinned over the header.\n It is the first focusable element in the shell; consuming apps should drop their\n own skip link on upgrade rather than stacking a second one in front of this. */\n.mj-skip-link {\n position: absolute;\n top: var(--mj-space-2);\n left: var(--mj-space-2);\n z-index: 100000;\n padding: var(--mj-space-2) var(--mj-space-4);\n background: var(--mj-bg-surface-elevated);\n color: var(--mj-text-primary);\n border: 1px solid var(--mj-border-strong);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-lg);\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-semibold);\n text-decoration: none;\n transform: translateY(-200%);\n transition: transform var(--mj-transition-fast);\n}\n.mj-skip-link:focus,\n.mj-skip-link:focus-visible {\n transform: translateY(0);\n outline: var(--mj-ring-width) solid var(--mj-focus-ring-color);\n outline-offset: var(--mj-ring-offset);\n}\n\n/* The main landmark is also the skip-link target; focusing it programmatically must\n not paint a ring on a region the user did not tab to. */\nmj-tab-container:focus {\n outline: none;\n}\n\n.shell-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n/* Hide shell container while loading - allows tab container to render and load\n first resource in background while shell loading indicator is visible */\n.shell-container.hidden {\n visibility: hidden;\n position: absolute;\n pointer-events: none;\n}\n\nmj-tab-container {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n transition: all 0.2s ease-in-out;\n}\n\n/* Hide Golden Layout tab headers when only one tab \u2014 scoped to the MAIN\n region: the RECORDS region is a separate Golden Layout whose tab strip is\n its whole point (switching between open records) and must never be hidden\n by the main bar's visibility policy. */\nmj-tab-container.hide-tab-bar ::ng-deep .main-region .lm_header {\n display: none;\n}\n\n/* Show tab headers when multiple tabs */\nmj-tab-container:not(.hide-tab-bar) ::ng-deep .main-region .lm_header {\n opacity: 1;\n max-height: 40px;\n transition: opacity 0.2s ease-in-out, max-height 0.2s ease-in-out;\n}\n\n/* Adjust content area height when tabs hidden */\nmj-tab-container.hide-tab-bar ::ng-deep .main-region .lm_content {\n height: 100% !important;\n}\n\n/* Ensure smooth transitions */\nmj-tab-container ::ng-deep .lm_stack {\n transition: all 0.2s ease-in-out;\n}\n\n.shell-header {\n height: 60px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n display: flex;\n align-items: center;\n padding: 0 var(--mj-space-4);\n gap: var(--mj-space-4);\n box-shadow: var(--mj-shadow-sm);\n flex-shrink: 0;\n /* Creates a stacking context so children (search bar z-index: 99999, dropdown)\n are contained within this level. Modal overlays (z-index: 1000+) sit above. */\n position: relative;\n z-index: 500;\n}\n\n/* Nav Bar Apps - permanent app icons in the header */\n.nav-bar-apps {\n display: flex;\n align-items: center;\n gap: var(--mj-space-1);\n}\n\n.nav-bar-apps.left-of-switcher {\n /* No extra margin - header gap handles spacing */\n /* This prevents the app switcher from shifting when icons are hidden */\n}\n\n.nav-bar-apps.left-of-user-menu {\n margin-right: var(--mj-space-2);\n}\n\n.nav-bar-app-btn {\n --app-color: var(--mj-text-muted);\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-md);\n border: none;\n background: transparent;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-secondary);\n font-size: var(--mj-text-lg);\n transition: all var(--mj-transition-fast);\n position: relative;\n}\n\n.nav-bar-app-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n/* Active = brand-primary (matches nav pills + switcher items); the app's\n metadata color is reserved for identity accents, never state.\n Shared active-tint recipe. */\n.nav-bar-app-btn.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--mj-brand-primary);\n}\n\n.nav-bar-app-btn i {\n transition: transform var(--mj-transition-fast);\n margin-top: var(--mj-space-0-5); /* Align with app switcher icon */\n}\n\n.nav-bar-app-btn:hover i {\n transform: scale(1.1);\n}\n\n.spacer {\n flex: 1;\n}\n\n/* Priority+ overflow support: on desktop the app nav owns the header's free\n middle space (so it knows exactly how much room its items have) and the\n spacer stands down. On mobile the nav is display:none'd, so the spacer's\n flex: 1 keeps the action cluster right-aligned. When the nav isn't rendered\n at all (app-locked sessions, Shell.AppNav.Enabled off), the sibling selector\n doesn't match and the spacer behaves as before. */\n@media (min-width: 769px) {\n mj-app-nav.desktop-nav {\n flex: 1 1 auto;\n min-width: 0;\n }\n mj-app-nav.desktop-nav + .spacer {\n flex: 0 0 0px;\n }\n}\n\n.header-actions {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n}\n\n.icon-btn {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n border: none;\n background: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-lg);\n transition: background var(--mj-transition-fast);\n}\n\n.icon-btn:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n/* Keyboard focus parity for all header controls (the search bar already has\n its own ring at .search-btn:focus-visible). */\n.nav-bar-app-btn:focus-visible,\n.icon-btn:focus-visible,\n.hamburger-btn:focus-visible,\n.user-menu .avatar-btn:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n\n/* Universal search: a static-width bar styled as an input, anchored on the\n right with the action cluster. Deliberately NOT resizable on hover/focus \u2014\n chrome that shifts on interaction was rejected in the 2026-07 review. It's a\n button (typing happens in the overlay it opens), so no focus/caret styling. */\n.search-btn {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n width: 260px;\n height: 36px;\n padding: 0 var(--mj-space-2-5) 0 var(--mj-space-3-5);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n font-family: inherit;\n cursor: pointer;\n transition: var(--mj-transition-colors);\n}\n\n.search-btn:hover {\n border-color: var(--mj-border-strong);\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n}\n\n.search-btn:focus-visible {\n outline: none;\n border-color: var(--mj-border-focus);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n/* Omnibar-off mobile trigger: hidden on desktop (the inline composite renders\n instead); the 768px block flips it on as the icon circle. */\n.search-btn-mobile {\n display: none;\n}\n\n/* Omnibar-off desktop: the inline search composite, same slot + width as the\n omnibar bar so the opt-in toggle doesn't move the header. */\n.shell-search-bar {\n position: relative;\n width: 260px;\n z-index: 99999;\n}\n\n/* The suggest dropdown needs more width than its 260px anchor. Pin it to the\n anchor's right edge and let it grow leftward \u2014 the bar is right-anchored, so\n leftward growth stays on screen. */\n.shell-search-bar ::ng-deep .suggest-dropdown {\n left: auto;\n right: 0;\n width: min(560px, calc(100vw - var(--mj-space-6)));\n}\n\n.search-btn i {\n font-size: var(--mj-text-sm);\n flex-shrink: 0;\n}\n\n.search-btn .search-btn-text {\n flex: 1;\n min-width: 0;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* kbd chip: deliberately mirrors the omnibar palette's .ob-kbd chrome\n (10.5px / 5px radius / bottom-heavy border) so the affordance and the\n surface it summons read as one system. */\n.search-btn .search-btn-kbd {\n flex-shrink: 0;\n white-space: nowrap;\n font-family: inherit;\n font-size: 10.5px;\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong);\n border-bottom-width: 2px;\n border-radius: 5px;\n padding: 1.5px var(--mj-space-1-5);\n}\n\n/* Notification button with badge */\n.notification-btn {\n position: relative;\n}\n\n.notification-badge {\n position: absolute;\n top: 4px;\n right: 4px;\n min-width: 16px;\n height: 16px;\n padding: 0 var(--mj-space-1);\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\n font-size: 10px;\n font-weight: var(--mj-font-semibold);\n border-radius: var(--mj-radius-full);\n display: flex;\n align-items: center;\n justify-content: center;\n line-height: var(--mj-leading-none);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.user-menu {\n position: relative;\n}\n\n.user-menu .avatar-btn {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n border: none;\n background: transparent;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: box-shadow var(--mj-transition-fast), transform var(--mj-transition-fast);\n padding: 0;\n overflow: hidden;\n}\n\n.user-menu .avatar-btn:hover {\n transform: scale(1.05);\n}\n\n/* Icon fallback styling - shows gray circle with icon */\n.user-menu .avatar-btn .icon-fallback {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n border: 2px solid var(--mj-border-default);\n background: var(--mj-bg-surface-hover);\n display: flex;\n align-items: center;\n justify-content: center;\n transition: border-color var(--mj-transition-fast);\n}\n\n.user-menu .avatar-btn:hover .icon-fallback {\n border-color: var(--mj-brand-primary);\n}\n\n.user-menu .avatar-btn .icon-fallback i {\n color: var(--mj-text-muted);\n font-size: var(--mj-text-base);\n}\n\n/* Avatar image - replaces the gray circle entirely */\n.user-menu .avatar-btn .avatar-img {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n object-fit: cover;\n box-shadow: var(--mj-shadow-sm);\n}\n\n.user-menu .avatar-btn:hover .avatar-img {\n box-shadow: var(--mj-shadow-md);\n}\n\n/* User Menu Header */\n.user-menu-header {\n padding: var(--mj-space-3) var(--mj-space-4);\n background: var(--mj-bg-surface-sunken);\n}\n\n.user-menu-header .user-info {\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-0-5);\n}\n\n.user-menu-header .user-name {\n font-weight: var(--mj-font-semibold);\n font-size: var(--mj-text-sm);\n color: var(--mj-text-primary);\n}\n\n.user-menu-header .user-email {\n font-size: var(--mj-text-xs);\n color: var(--mj-text-muted);\n}\n\n.user-menu-header .user-subtitle {\n font-size: 11px;\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n/* User Context Menu */\n.user-context-menu {\n position: absolute;\n top: 100%;\n right: 0;\n margin-top: var(--mj-space-2);\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-lg);\n min-width: 180px;\n /* Never wider than the viewport, never taller than what is left below the 60px header:\n a short window (or a phone) used to leave the bottom items unreachable, because the\n menu had no height limit and clipped instead of scrolling. */\n max-width: min(360px, calc(100vw - 2 * var(--mj-space-4)));\n max-height: calc(100vh - 60px - 3 * var(--mj-space-2));\n max-height: calc(100dvh - 60px - 3 * var(--mj-space-2));\n z-index: 10000;\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.user-menu-item {\n display: flex;\n align-items: center;\n gap: var(--mj-space-3);\n padding: var(--mj-space-3) var(--mj-space-4);\n cursor: pointer;\n font-size: var(--mj-text-sm);\n color: var(--mj-text-primary);\n transition: background var(--mj-transition-fast);\n}\n\n.user-menu-item i {\n width: 18px;\n text-align: center;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n}\n\n.user-menu-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.user-menu-item.danger {\n color: var(--mj-status-error);\n}\n\n.user-menu-item.danger i {\n color: var(--mj-status-error);\n}\n\n.user-menu-item.danger:hover {\n background: color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface-elevated));\n}\n\n.user-menu-divider {\n height: 1px;\n background: var(--mj-border-default);\n margin: var(--mj-space-1) 0;\n}\n\n/* Menu item disabled state */\n.user-menu-item.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.user-menu-item.disabled:hover {\n background: transparent;\n}\n\n/* Theme Toggle Switch */\n.theme-toggle-item {\n cursor: pointer;\n}\n\n.theme-toggle-track {\n position: relative;\n width: 40px;\n height: 22px;\n border-radius: var(--mj-radius-full);\n background: var(--mj-bg-surface-active);\n margin-left: auto;\n transition: background var(--mj-transition-base);\n flex-shrink: 0;\n}\n\n.theme-toggle-track.dark {\n background: var(--mj-brand-primary);\n}\n\n.theme-toggle-thumb {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: var(--mj-bg-surface);\n box-shadow: var(--mj-shadow-sm);\n display: flex;\n align-items: center;\n justify-content: center;\n transition: transform var(--mj-transition-base);\n}\n\n.theme-toggle-track.dark .theme-toggle-thumb {\n transform: translateX(18px);\n}\n\n.theme-toggle-icon {\n font-size: 10px;\n color: var(--mj-text-muted);\n}\n\n.theme-toggle-track.dark .theme-toggle-icon {\n color: var(--mj-text-inverse);\n}\n\n/* Menu shortcut hint */\n.user-menu-item .menu-shortcut {\n margin-left: auto;\n font-size: 11px;\n color: var(--mj-text-disabled);\n font-family: monospace;\n}\n\n/* Menu animations */\n.user-context-menu.menu-fade {\n animation: menuFadeIn 0.15s ease-out;\n}\n\n.user-context-menu.menu-slide {\n animation: menuSlideIn 0.2s ease-out;\n}\n\n@keyframes menuFadeIn {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes menuSlideIn {\n from {\n opacity: 0;\n transform: translateY(-16px) scale(0.95);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.shell-loading {\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: 99999;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding-bottom: 15vh;\n background: var(--mj-bg-page);\n}\n\n.loading-version {\n margin-top: var(--mj-space-8, 24px);\n font-size: 11px;\n color: var(--mj-text-muted);\n letter-spacing: 0.4px;\n opacity: 0.6;\n user-select: none;\n}\n\n/* Loading Recovery Reset Panel */\n.loading-reset-panel {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--mj-space-3);\n margin-top: var(--mj-space-10);\n padding: var(--mj-space-8);\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n box-shadow: var(--mj-shadow-md);\n max-width: 420px;\n animation: resetPanelFadeIn 0.4s ease-out;\n}\n\n.loading-reset-message {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n margin: 0;\n font-size: var(--mj-text-base);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-primary);\n}\n\n.loading-reset-message i {\n font-size: var(--mj-text-lg);\n color: var(--mj-status-warning);\n}\n\n.loading-reset-hint {\n margin: 0;\n font-size: var(--mj-text-sm);\n color: var(--mj-text-secondary);\n text-align: center;\n line-height: var(--mj-leading-relaxed);\n}\n\n.loading-reset-btn {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n margin-top: var(--mj-space-3);\n padding: var(--mj-space-3) var(--mj-space-6);\n font-size: var(--mj-text-base);\n font-weight: var(--mj-font-medium);\n font-family: var(--mj-font-family);\n color: var(--mj-text-inverse);\n background: var(--mj-brand-primary);\n border: 1px solid var(--mj-brand-primary);\n border-radius: var(--mj-radius-md);\n cursor: pointer;\n transition: var(--mj-transition-colors);\n}\n\n.loading-reset-btn:hover {\n background: var(--mj-brand-primary-hover);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary-hover);\n box-shadow: var(--mj-shadow-md);\n}\n\n.loading-reset-btn:active {\n background: var(--mj-brand-primary-active);\n color: var(--mj-text-inverse);\n transform: scale(0.98);\n}\n\n.loading-reset-btn i {\n font-size: var(--mj-text-sm);\n}\n\n@keyframes resetPanelFadeIn {\n from {\n opacity: 0;\n transform: translateY(12px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Hamburger button - hidden on desktop */\n.hamburger-btn {\n display: none;\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-md);\n border: none;\n background: none;\n cursor: pointer;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-secondary);\n font-size: var(--mj-text-xl);\n transition: background var(--mj-transition-fast);\n}\n.hamburger-btn:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n/* Mobile Navigation Overlay */\n.mobile-nav-overlay {\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: var(--mj-bg-overlay);\n z-index: 9998;\n}\n\n/* Mobile Navigation Drawer */\n/* `visibility` is transitioned alongside `transform` so the closed drawer is genuinely\n hidden \u2014 off-screen alone leaves its controls hit-testable and visible to\n checkVisibility(), which is what let 6 of its 8 controls keep taking focus. The\n delay holds visibility through the slide-out, then drops it; opening is instant\n (0s) so the drawer is present for the whole slide-in. `inert` (set in the template)\n is the actual focus guard; this keeps the CSS from disagreeing with it. */\n.mobile-nav-drawer {\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n width: 280px;\n max-width: 85vw;\n background: var(--mj-bg-surface);\n box-shadow: var(--mj-shadow-xl);\n z-index: 9999;\n flex-direction: column;\n transform: translateX(-100%);\n visibility: hidden;\n transition: transform var(--mj-transition-slow), visibility 0s linear 300ms;\n}\n.mobile-nav-drawer.open {\n transform: translateX(0);\n visibility: visible;\n transition: transform var(--mj-transition-slow), visibility 0s linear 0s;\n}\n\n/* Reduced motion: no slide, so nothing to hold visibility open for. */\n@media (prefers-reduced-motion: reduce) {\n .mobile-nav-drawer,\n .mobile-nav-drawer.open {\n transition: none;\n }\n}\n\n.mobile-nav-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--mj-space-4) var(--mj-space-5);\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n}\n.mobile-nav-header span {\n font-weight: var(--mj-font-semibold);\n font-size: var(--mj-text-base);\n color: var(--mj-text-primary);\n}\n.mobile-nav-header .close-btn {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n border: none;\n background: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-lg);\n transition: background var(--mj-transition-fast);\n}\n.mobile-nav-header .close-btn:hover {\n background: var(--mj-bg-surface-active);\n}\n\n.mobile-nav-content {\n flex: 1;\n overflow-y: auto;\n padding: var(--mj-space-4) 0;\n}\n\n.mobile-nav-section-title {\n padding: var(--mj-space-2) var(--mj-space-5);\n font-size: var(--mj-text-xs);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n/* Records entry in the mobile drawer (records-style record opens) \u2014\n the desktop pill lives in the mobile-hidden app nav, so the drawer\n carries its own touch-friendly instance. */\n.mobile-nav-records {\n padding: var(--mj-space-2) var(--mj-space-3);\n border-top: 1px solid var(--mj-border-default);\n}\n.mobile-nav-records .records-pill {\n width: 100%;\n min-height: 48px; /* Touch-target parity with drawer nav items */\n justify-content: flex-start;\n}\n\n.mobile-nav-footer {\n border-top: 1px solid var(--mj-border-default);\n padding: var(--mj-space-3) var(--mj-space-4);\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-1);\n}\n\n.mobile-nav-action {\n position: relative;\n display: flex;\n align-items: center;\n gap: var(--mj-space-3);\n padding: var(--mj-space-3) var(--mj-space-4);\n border: none;\n background: none;\n cursor: pointer;\n border-radius: var(--mj-radius-md);\n color: var(--mj-text-secondary);\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-medium);\n transition: background var(--mj-transition-fast);\n width: 100%;\n text-align: left;\n}\n.mobile-nav-action:hover {\n background: var(--mj-bg-surface-hover);\n}\n.mobile-nav-action i {\n font-size: var(--mj-text-base);\n width: 20px;\n text-align: center;\n}\n\n/* Mobile Responsive Styles */\n@media (max-width: 768px) {\n .hamburger-btn {\n display: flex;\n }\n\n .desktop-nav {\n display: none !important;\n }\n\n .desktop-only {\n display: none !important;\n }\n\n .mobile-nav-overlay {\n display: block;\n }\n\n .mobile-nav-drawer {\n display: flex;\n }\n\n .shell-header {\n padding: 0 var(--mj-space-3);\n gap: var(--mj-space-2);\n }\n\n /* Keep the right-edge cluster (chat/nav-bar app icons + avatar) on one row.\n Without pinning this, the icons can wrap and the avatar drops onto a second\n line, stacking under the chat icon. */\n .header-actions {\n flex-direction: row;\n flex-wrap: nowrap;\n }\n .header-actions .user-menu,\n .header-actions .nav-bar-apps,\n .header-actions .search-btn {\n flex-shrink: 0;\n }\n\n /* Search bar collapses to the icon-only circle on small viewports \u2014\n the placeholder text + kbd chip are already hidden via .desktop-only.\n The omnibar-off mobile trigger (.search-btn-mobile) becomes visible here;\n its .desktop-only composite sibling is hidden by the rule above. */\n .search-btn {\n display: flex;\n width: 40px;\n height: 40px;\n padding: 0;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: none;\n }\n .search-btn i {\n font-size: var(--mj-text-lg);\n }\n .search-btn:hover {\n background: var(--mj-bg-surface-hover);\n border: none;\n }\n}\n\n/* Settings Full-Screen Window Styles */\n::ng-deep .settings-fullscreen-window {\n /* Remove window chrome for full-screen feel */\n box-shadow: none !important;\n border: none !important;\n}\n\n::ng-deep .settings-fullscreen-window .k-window-titlebar {\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n padding: var(--mj-space-3) var(--mj-space-4);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-title {\n font-size: var(--mj-text-lg);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-primary);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-titlebar-actions {\n gap: var(--mj-space-1);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action {\n width: 32px;\n height: 32px;\n border-radius: var(--mj-radius-md);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-content {\n padding: 0;\n overflow: auto;\n background: var(--mj-bg-page);\n}\n\n/* Hide minimize/maximize buttons - only show close */\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action[title=\"Minimize\"],\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action[title=\"Maximize\"],\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action .k-i-window-minimize,\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action .k-i-window-maximize,\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action .k-i-window {\n display: none !important;\n}\n\n/* Universal search is a right-anchored bar affordance in header-actions\n (2026-07 design review round 2): both experiences still open as overlays \u2014\n the bar is a trigger, not an inline input. Icon-only under 768px. */\n\n/* ========================================\n SEARCH POPUP\n ======================================== */\n\n.search-popup-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: var(--mj-bg-overlay);\n z-index: 9999;\n animation: fadeIn 0.15s ease;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.search-popup {\n position: absolute;\n top: 60px; /* Below header */\n right: 16px;\n width: 480px;\n max-width: calc(100vw - 32px);\n background: var(--mj-bg-surface-elevated);\n border-radius: var(--mj-radius-lg);\n box-shadow: var(--mj-shadow-xl);\n z-index: 10000;\n opacity: 0;\n transform: translateY(-10px);\n pointer-events: none;\n visibility: hidden;\n transition: opacity var(--mj-transition-base), transform var(--mj-transition-base), visibility 0s linear 200ms;\n}\n\n.search-popup.open {\n opacity: 1;\n transform: translateY(0);\n pointer-events: all;\n visibility: visible;\n transition: opacity var(--mj-transition-base), transform var(--mj-transition-base), visibility 0s linear 0s;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .search-popup,\n .search-popup.open {\n transition: none;\n }\n}\n\n.search-popup-content {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n padding: var(--mj-space-3);\n}\n\n.search-entity-dropdown {\n min-width: 140px;\n max-width: 180px;\n flex-shrink: 0;\n}\n\n.search-input {\n flex: 1;\n min-width: 0;\n height: 40px;\n padding: var(--mj-space-2) var(--mj-space-3);\n font-size: var(--mj-text-sm);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm);\n outline: none;\n display: block !important;\n box-sizing: border-box;\n}\n\n.search-input:focus {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.search-submit-btn {\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-md);\n border: none;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: var(--mj-text-base);\n transition: background var(--mj-transition-fast);\n flex-shrink: 0;\n}\n\n.search-submit-btn:hover {\n background: var(--mj-brand-primary-hover);\n}\n\n.search-submit-btn:active {\n background: var(--mj-brand-primary-hover);\n}\n\n/* Mobile notification badge */\n.notification-badge-mobile {\n position: absolute;\n top: 8px;\n right: 8px;\n min-width: 18px;\n height: 18px;\n padding: 0 5px;\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\n font-size: 11px;\n font-weight: var(--mj-font-semibold);\n border-radius: var(--mj-radius-full);\n display: flex;\n align-items: center;\n justify-content: center;\n line-height: var(--mj-leading-none);\n box-shadow: var(--mj-shadow-sm);\n}\n\n/* Mobile-specific search popup adjustments */\n@media (max-width: 768px) {\n .search-popup {\n top: 60px;\n left: 16px;\n right: 16px;\n width: auto;\n max-width: none;\n }\n\n .search-popup-content {\n flex-wrap: wrap;\n }\n\n .search-entity-dropdown {\n width: 100%;\n }\n\n .search-input {\n flex: 1;\n min-width: 0;\n }\n}\n\n/* ========================================\n PIN PROGRESS OVERLAY\n ======================================== */\n.pin-progress-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: color-mix(in srgb, var(--mj-text-primary) 40%, transparent);\n z-index: 100000;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: pinBackdropIn 0.2s ease;\n}\n\n@keyframes pinBackdropIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.pin-progress-modal {\n background: var(--mj-bg-surface);\n border-radius: var(--mj-radius-xl);\n padding: var(--mj-space-7) var(--mj-space-10);\n display: flex;\n align-items: center;\n gap: var(--mj-space-4);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--mj-text-primary) 30%, transparent);\n animation: pinModalIn 0.25s ease;\n}\n\n@keyframes pinModalIn {\n from { opacity: 0; transform: scale(0.9) translateY(8px); }\n to { opacity: 1; transform: scale(1) translateY(0); }\n}\n\n.pin-progress-text {\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-medium);\n color: var(--mj-text-secondary);\n white-space: nowrap;\n}\n/* ============================================================\n Global Activity indicator (P3)\n ============================================================ */\n.activity-wrap { position: relative; display: inline-flex; }\n.activity-btn { position: relative; }\n.activity-btn .activity-pulse { color: var(--mj-brand-primary); }\n.activity-badge {\n position: absolute;\n top: 2px;\n right: 2px;\n min-width: 16px;\n height: 16px;\n padding: 0 var(--mj-space-1);\n border-radius: var(--mj-radius-md);\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 10px;\n font-weight: var(--mj-font-bold);\n line-height: 16px;\n text-align: center;\n}\n.activity-drawer {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n width: 320px;\n max-height: 420px;\n display: flex;\n flex-direction: column;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: 0 8px 28px color-mix(in srgb, var(--mj-text-primary) 18%, transparent);\n z-index: 1000;\n overflow: hidden;\n}\n.activity-drawer-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--mj-space-2-5) var(--mj-space-3);\n font-weight: var(--mj-font-bold);\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n border-bottom: 1px solid var(--mj-border-default);\n}\n.activity-clear {\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 0.8rem;\n padding: var(--mj-space-1) var(--mj-space-1-5);\n border-radius: var(--mj-radius-md);\n}\n.activity-clear:hover { color: var(--mj-text-primary); background: var(--mj-bg-surface-hover); }\n.activity-list { overflow-y: auto; padding: var(--mj-space-1-5); }\n.activity-row {\n display: flex;\n gap: var(--mj-space-2-5);\n padding: var(--mj-space-2);\n border-radius: var(--mj-radius-md);\n}\n.activity-row:hover { background: var(--mj-bg-surface-hover); }\n.activity-row-icon { margin-top: var(--mj-space-0-5); color: var(--mj-brand-primary); font-size: 0.85rem; }\n.activity-row.is-error .activity-row-icon { color: var(--mj-status-error); }\n.activity-row-body { flex: 1; min-width: 0; }\n.activity-row-label { font-size: 0.82rem; font-weight: var(--mj-font-semibold); color: var(--mj-text-primary); }\n.activity-row-detail { font-size: 0.72rem; color: var(--mj-text-muted); margin-top: var(--mj-space-px); }\n.activity-bar {\n margin-top: var(--mj-space-1-5);\n height: 4px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 2px;\n overflow: hidden;\n}\n.activity-bar-fill { height: 100%; background: var(--mj-brand-primary); transition: width var(--mj-transition-slow); }\n\n"] }]
|
|
3763
|
-
}], () => [{ type: i1.ApplicationManager }, { type: i1.WorkspaceStateManager }, { type: i1.GoldenLayoutManager }, { type: i1.TabService }, { type: i2.NavigationService }, { type: i3.ActivatedRoute }, { type: i3.Router }, { type: i4.MJAuthBase }, { type: i0.ChangeDetectorRef }, { type: i5.UserAvatarService }, { type: i6.UserSharingCenterDialogService }, { type: i7.AboutDialogService }, { type: i8.ProfileDialogService }, { type: i0.ViewContainerRef }, { type: i2.TitleService }, { type: i2.DeveloperModeService }, { type: i9.StartupValidationService }, { type: i10.CommandPaletteService }, { type: i2.ThemeService }, { type: i2.HomeAppPinService }, { type: i11.FileOpenService }, { type: i12.FeedbackDialogService }, { type: i12.FeedbackService }, { type: i2.ActivityService }], {
|
|
4179
|
+
args: [{ standalone: false, selector: 'mj-shell', template: "<div class=\"shell-container\" [class.hidden]=\"loading\" [class.tabs-visible]=\"tabBarVisible\">\n <!-- Skip link (WCAG 2.4.1) \u2014 MUST stay the first focusable element in the shell.\n Consuming apps that added their own skip link should remove it on upgrade. -->\n <a class=\"mj-skip-link\" href=\"#mj-main-content\" (click)=\"skipToMainContent($event)\">Skip to main content</a>\n\n <!-- Header -->\n <header class=\"shell-header\">\n <!-- MJ Logo -->\n <div class=\"mj-logo\" title=\"MemberJunction\"></div>\n\n <!-- Mobile Hamburger Button -->\n <button class=\"hamburger-btn\" #mobileNavToggle (click)=\"toggleMobileNav()\" title=\"Menu\"\n aria-label=\"Menu\" [attr.aria-expanded]=\"mobileNavOpen\" aria-controls=\"mj-mobile-nav-drawer\">\n <i class=\"fa-solid fa-bars\" aria-hidden=\"true\"></i>\n </button>\n\n <!-- Nav Bar Apps: Left of App Switcher -->\n @if (leftOfSwitcherApps.length > 0 && !appSwitchingLocked) {\n <div class=\"nav-bar-apps left-of-switcher\">\n @for (app of leftOfSwitcherApps; track app) {\n <button\n class=\"nav-bar-app-btn\"\n [class.active]=\"IsActiveApp(app)\"\n [attr.aria-current]=\"IsActiveApp(app) ? 'true' : null\"\n [title]=\"app.Name\"\n [style.--app-color]=\"app.GetColor()\"\n (click)=\"onNavBarAppClick(app, $event)\"\n (dblclick)=\"onNavBarAppDblClick(app, $event)\">\n <i [class]=\"app.Icon\"></i>\n </button>\n }\n </div>\n }\n\n <!-- App Switcher (hidden for app-locked sessions, e.g. magic-link, and\n removable per deployment via 'Shell.AppSwitcher.Enabled') -->\n @if (!appSwitchingLocked && ShowAppSwitcher) {\n <mj-app-switcher\n [activeApp]=\"activeApp\"\n [isViewingSystemTab]=\"isViewingSystemTab\"\n [loadingAppId]=\"loadingAppId\"\n [SwitcherStyle]=\"AppSwitcherStyle\"\n [Provider]=\"Provider\"\n (appSelected)=\"onAppSwitch($event)\">\n </mj-app-switcher>\n }\n\n <!-- App Navigation (desktop only; removable per deployment via 'Shell.AppNav.Enabled') -->\n @if (activeApp && ShowAppNav) {\n <mj-app-nav\n class=\"desktop-nav\"\n [app]=\"activeApp\"\n [OverflowEnabled]=\"true\"\n (navItemClick)=\"onNavItemClick($event)\"\n (navItemDismiss)=\"onNavItemDismiss($event)\">\n <!-- Records style: persistent Records pill \u2014 projected into the nav\n row's trailing slot so it sits pipe-separated right after the\n app's own items, styled as a nav pill. Appears only while any\n record is open; resumes the last-viewed record on click. -->\n @if (RecordTabsStyle) {\n <mj-records-hub-pill></mj-records-hub-pill>\n }\n </mj-app-nav>\n } @else if (RecordTabsStyle) {\n <!-- Reachability: when the app nav doesn't render (AppNav disabled per\n deployment, or no active app), open records must STILL have a way\n back \u2014 render the pill standalone in the header. -->\n <mj-records-hub-pill class=\"desktop-nav\"></mj-records-hub-pill>\n }\n\n <!-- Spacer -->\n <div class=\"spacer\"></div>\n <!-- Actions (activity, notifications, user menu) -->\n <div class=\"header-actions\">\n <!-- Universal Search \u2014 right-anchored with the action cluster, static width\n (2026-07 review consensus: visible when space permits, never resizes).\n Omnibar ON: a button styled as an input \u2014 clicking (or Ctrl/Cmd+K) opens\n the modal palette; typing happens there. Collapses to the icon circle at\n the mobile breakpoint.\n Omnibar OFF: a REAL inline input (mj-search-composite) \u2014 results attach\n beneath it as a suggest dropdown with a \"See all results\" footer. On\n mobile the composite hides and an icon button opens the Spotlight\n overlay instead (same slot, same collapse point). -->\n @if (ShowSearchBar) {\n @if (UseOmnibar) {\n <button class=\"search-btn\" title=\"Search everything\"\n [attr.aria-label]=\"'Search everything (' + OmnibarShortcutLabel + ')'\"\n (click)=\"OnHeaderSearchClick()\">\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n <span class=\"search-btn-text desktop-only\">Search everything</span>\n <kbd class=\"search-btn-kbd desktop-only\" aria-hidden=\"true\">{{ OmnibarShortcutLabel }}</kbd>\n </button>\n } @else {\n <div class=\"shell-search-bar desktop-only\">\n <mj-search-composite\n #shellSearchComposite\n Placeholder=\"Search everything...\"\n [ShowShortcutHint]=\"true\"\n [EnablePreview]=\"ShowSearchPreview\"\n [EnableRecent]=\"true\"\n [MaxPreviewResults]=\"8\"\n [ShowPromo]=\"ShowOmnibarPromo\"\n [PromoText]=\"OmnibarPromoText\"\n (ResultSelected)=\"OnSearchResultSelected($event)\"\n (SearchSubmitted)=\"OnSearchSubmitted($event)\"\n (SeeAllRequested)=\"OnSeeAllSearch($event)\"\n (RecentRecordSelected)=\"OnSearchResultSelected($event)\"\n (PromoAccepted)=\"OnOmnibarPromoAccepted($event)\"\n (PromoDismissed)=\"OnOmnibarPromoDismissed()\">\n </mj-search-composite>\n </div>\n <button class=\"search-btn search-btn-mobile\" title=\"Search everything\"\n [attr.aria-label]=\"'Search everything (' + OmnibarShortcutLabel + ')'\"\n (click)=\"OnHeaderSearchClick()\">\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n </button>\n }\n }\n <!-- Global Activity indicator -->\n @if (activityItems.length > 0) {\n <div class=\"activity-wrap\">\n <button class=\"icon-btn desktop-only activity-btn\" title=\"Activity\" (click)=\"toggleActivity($event)\">\n <i class=\"fa-solid fa-wave-square\" [class.activity-pulse]=\"activityRunningCount > 0\"></i>\n @if (activityRunningCount > 0) {\n <span class=\"activity-badge\">{{ activityRunningCount }}</span>\n }\n </button>\n @if (activityOpen) {\n <div class=\"activity-drawer\" (click)=\"$event.stopPropagation()\">\n <div class=\"activity-drawer-head\">\n <span>Activity</span>\n <button class=\"activity-clear\" (click)=\"clearFinishedActivity()\" title=\"Clear finished\">\n <i class=\"fa-solid fa-broom\"></i>\n </button>\n </div>\n <div class=\"activity-list\">\n @for (a of activityItems; track a.ID) {\n <div class=\"activity-row\" [class.is-error]=\"a.Status === 'error'\">\n <i class=\"activity-row-icon\"\n [class]=\"a.Status === 'running' ? 'fa-solid fa-spinner fa-spin'\n : a.Status === 'error' ? 'fa-solid fa-circle-exclamation'\n : 'fa-solid fa-circle-check'\"></i>\n <div class=\"activity-row-body\">\n <div class=\"activity-row-label\">{{ a.Label }}</div>\n @if (a.Detail) { <div class=\"activity-row-detail\">{{ a.Detail }}</div> }\n @if (a.Status === 'running' && a.Progress != null) {\n <div class=\"activity-bar\"><div class=\"activity-bar-fill\" [style.width.%]=\"a.Progress\"></div></div>\n }\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n @if (ShowNotifications) {\n <button class=\"icon-btn desktop-only notification-btn\" title=\"Notifications\" (click)=\"showNotifications()\">\n <i class=\"fa-solid fa-bell\"></i>\n @if (unreadNotificationCount > 0) {\n <span class=\"notification-badge\">\n {{ unreadNotificationCount > 99 ? '99+' : unreadNotificationCount }}\n </span>\n }\n </button>\n }\n <!-- Nav Bar Apps: Left of User Menu -->\n @if (leftOfUserMenuApps.length > 0) {\n <div class=\"nav-bar-apps left-of-user-menu\">\n @for (app of leftOfUserMenuApps; track app) {\n <button\n class=\"nav-bar-app-btn\"\n [class.active]=\"IsActiveApp(app)\"\n [attr.aria-current]=\"IsActiveApp(app) ? 'true' : null\"\n [title]=\"app.Name\"\n [style.--app-color]=\"app.GetColor()\"\n (click)=\"onNavBarAppClick(app, $event)\"\n (dblclick)=\"onNavBarAppDblClick(app, $event)\">\n <i [class]=\"app.Icon\"></i>\n </button>\n }\n </div>\n }\n\n <div class=\"user-menu\">\n <button class=\"avatar-btn\" (click)=\"toggleUserMenu($event)\"\n [attr.aria-label]=\"avatarAriaLabel\"\n [attr.aria-expanded]=\"userMenuVisible\">\n @if (userImageURL) {\n <img [src]=\"userImageURL\" alt=\"\" aria-hidden=\"true\" class=\"avatar-img\" />\n } @else {\n <div class=\"icon-fallback\" aria-hidden=\"true\">\n <i [class]=\"userIconClass || 'fa-solid fa-user'\"></i>\n </div>\n }\n </button>\n <!-- User Context Menu (Dynamic) -->\n @if (userMenuVisible) {\n <div class=\"user-context-menu\"\n [class.menu-fade]=\"getUserMenuOptions()?.animationStyle === 'fade'\"\n [class.menu-slide]=\"getUserMenuOptions()?.animationStyle === 'slide'\">\n <!-- User Header -->\n @if (getUserMenuOptions()?.showUserName) {\n <div class=\"user-menu-header\">\n <div class=\"user-info\">\n <span class=\"user-name\">{{ getUserDisplayInfo()?.name || userName }}</span>\n @if (getUserMenuOptions()?.showUserEmail && getUserDisplayInfo()?.email) {\n <span class=\"user-email\">\n {{ getUserDisplayInfo()?.email }}\n </span>\n }\n @if (getUserDisplayInfo()?.subtitle) {\n <span class=\"user-subtitle\">{{ getUserDisplayInfo()?.subtitle }}</span>\n }\n </div>\n </div>\n <div class=\"user-menu-divider\"></div>\n }\n <!-- Dynamic Menu Items -->\n @for (element of userMenuElements; track element) {\n <!-- Divider -->\n @if (isMenuDivider(element)) {\n <div class=\"user-menu-divider\"></div>\n }\n <!-- Menu Item -->\n @if (!isMenuDivider(element)) {\n @if (asMenuItem(element); as item) {\n @if (item.id === 'toggle-theme') {\n <div class=\"user-menu-item theme-toggle-item\"\n [title]=\"item.tooltip || ''\"\n (click)=\"onUserMenuItemClick(item.id)\">\n <i [class]=\"item.icon\"></i>\n <span class=\"menu-label\">{{ item.label }}</span>\n <div class=\"theme-toggle-track\" [class.dark]=\"IsDarkMode\">\n <div class=\"theme-toggle-thumb\">\n <i [class]=\"IsDarkMode ? 'fa-solid fa-moon' : 'fa-solid fa-sun'\" class=\"theme-toggle-icon\"></i>\n </div>\n </div>\n </div>\n } @else {\n <div class=\"user-menu-item\"\n [class.disabled]=\"!item.enabled\"\n [class.danger]=\"item.cssClass === 'danger'\"\n [style.color]=\"item.color || null\"\n [title]=\"item.tooltip || ''\"\n (click)=\"item.enabled && onUserMenuItemClick(item.id)\">\n <i [class]=\"item.icon\"></i>\n <span class=\"menu-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"menu-shortcut\">{{ item.shortcut }}</span>\n }\n </div>\n }\n }\n }\n }\n </div>\n }\n </div>\n </div>\n </header>\n\n <!-- Search Popup -->\n @if (isSearchOpen) {\n <div class=\"search-popup-overlay\" (click)=\"closeSearch()\"></div>\n }\n <div class=\"search-popup\" [class.open]=\"isSearchOpen\" [attr.inert]=\"isSearchOpen ? null : ''\">\n <div class=\"search-popup-content\" (click)=\"$event.stopPropagation()\">\n <mj-dropdown\n [Data]=\"searchableEntities\"\n TextField=\"Name\"\n ValueField=\"ID\"\n AriaLabel=\"Search in\"\n class=\"search-entity-dropdown\"\n [(ngModel)]=\"selectedEntity\">\n </mj-dropdown>\n <input\n type=\"text\"\n #searchInput\n placeholder=\"Search...\"\n aria-label=\"Search\"\n class=\"mj-input search-input\"\n (keydown.enter)=\"onSearch($event)\"\n />\n <button class=\"search-submit-btn\" (click)=\"onSearch($event)\" title=\"Search\">\n <i class=\"fa-solid fa-search\"></i>\n </button>\n </div>\n </div>\n\n <!-- Mobile Navigation Drawer -->\n @if (mobileNavOpen) {\n <div class=\"mobile-nav-overlay\" (click)=\"closeMobileNav()\"></div>\n }\n <div class=\"mobile-nav-drawer\" id=\"mj-mobile-nav-drawer\" #mobileNavDrawer\n [class.open]=\"mobileNavOpen\" [attr.inert]=\"mobileNavOpen ? null : ''\">\n <div class=\"mobile-nav-header\">\n <span>Navigation</span>\n <button class=\"close-btn\" (click)=\"closeMobileNav()\" aria-label=\"Close navigation\">\n <i class=\"fa-solid fa-xmark\" aria-hidden=\"true\"></i>\n </button>\n </div>\n @if (activeApp && ShowAppNav) {\n <div class=\"mobile-nav-content\">\n <div class=\"mobile-nav-section-title\">{{ activeApp.Name }}</div>\n <mj-app-nav\n [app]=\"activeApp\"\n [OverflowEnabled]=\"false\"\n (navItemClick)=\"onNavItemClick($event)\"\n (navItemDismiss)=\"onNavItemDismiss($event)\">\n </mj-app-nav>\n </div>\n }\n <!-- Records style: the desktop pill lives in the (mobile-hidden) app nav,\n so the drawer carries its own entry \u2014 without it, open records are\n unreachable on mobile except by re-opening them. Resumes the\n last-viewed record and closes the drawer. -->\n @if (RecordTabsStyle) {\n <div class=\"mobile-nav-records\">\n <mj-records-hub-pill (Activated)=\"closeMobileNav()\"></mj-records-hub-pill>\n </div>\n }\n <div class=\"mobile-nav-footer\">\n <button class=\"mobile-nav-action\" title=\"Search\" (click)=\"toggleSearch(); closeMobileNav()\">\n <i class=\"fa-solid fa-search\"></i>\n <span>Search</span>\n </button>\n @if (ShowNotifications) {\n <button class=\"mobile-nav-action\" title=\"Notifications\" (click)=\"showNotifications(); closeMobileNav()\">\n <i class=\"fa-solid fa-bell\"></i>\n <span>Notifications</span>\n @if (unreadNotificationCount > 0) {\n <span class=\"notification-badge-mobile\">\n {{ unreadNotificationCount > 99 ? '99+' : unreadNotificationCount }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n\n <!-- Tab Container - with dynamic tab bar visibility -->\n <mj-tab-container\n id=\"mj-main-content\"\n role=\"main\"\n tabindex=\"-1\"\n [class.hide-tab-bar]=\"!tabBarVisible\"\n (firstResourceLoadComplete)=\"onFirstResourceLoadComplete()\"\n (layoutInitError)=\"handleLayoutError()\">\n </mj-tab-container>\n</div>\n\n<!-- Loading State -->\n@if (loading) {\n <div class=\"shell-loading\">\n <mj-loading\n [text]=\"currentLoadingText\"\n [textColor]=\"currentLoadingTextColor\"\n [logoColor]=\"currentLoadingColor\"\n [logoGradient]=\"currentLoadingGradient\"\n [animation]=\"currentLoadingAnimation\"\n size=\"large\">\n </mj-loading>\n @if (ShowResetOption) {\n <div class=\"loading-reset-panel\">\n <p class=\"loading-reset-message\">\n <i class=\"fa-regular fa-clock\"></i>\n Taking longer than expected\n </p>\n <p class=\"loading-reset-hint\">\n This can happen after updates or due to cached data issues.\n </p>\n <button class=\"loading-reset-btn\" (click)=\"ResetApplication()\">\n <i class=\"fa-solid fa-arrows-rotate\"></i>\n Reset\n </button>\n </div>\n } @else {\n <div class=\"loading-version\">MemberJunction \u00B7 v{{ MJVersion }}</div>\n }\n </div>\n}\n\n<!-- App Access Error Dialog -->\n<mj-app-access-dialog\n #appAccessDialog\n (result)=\"onAppAccessDialogResult($event)\">\n</mj-app-access-dialog>\n\n<!-- Command Palette -->\n<mj-command-palette\n [ShowSearch]=\"ShowSearchBar\"\n (AppSelected)=\"onAppSwitch($event)\"\n (KnowledgeSearchRequested)=\"OnCommandPaletteSearch($event)\"></mj-command-palette>\n\n<!-- Unified command palette (Shell.Omnibar.Enabled). Rendered only while the search bar is:\n when Instance Config or the host's chrome policy hides search, the palette must not\n remain reachable through Ctrl/Cmd+K. -->\n@if (ShowSearchBar) {\n <mj-omnibar-palette #omnibarPalette (SettingsRequested)=\"OnPaletteSettingsRequested()\"></mj-omnibar-palette>\n}\n\n<!-- Legacy universal search as a Spotlight-style overlay \u2014 omnibar-off MOBILE only\n (on desktop the inline header composite owns the experience; this opens from\n the mobile icon or Ctrl/Cmd+K when the composite isn't visible).\n The shell owns the Ctrl/Cmd+K chord, so the overlay's own global shortcut is off. -->\n@if (ShowSearchBar && !UseOmnibar) {\n <mj-search-overlay\n [(IsOpen)]=\"LegacySearchOpen\"\n Placeholder=\"Search everything...\"\n [EnableGlobalShortcut]=\"false\"\n [ShowPromo]=\"ShowOmnibarPromo\"\n [PromoText]=\"OmnibarPromoText\"\n (ResultSelected)=\"OnOverlayResultSelected($event)\"\n (SeeAllRequested)=\"OnOverlaySeeAll($event)\"\n (PromoAccepted)=\"OnOmnibarPromoAccepted($event)\"\n (PromoDismissed)=\"OnOmnibarPromoDismissed()\">\n </mj-search-overlay>\n}\n\n<!-- Pin Progress Overlay -->\n@if (PinProgressVisible) {\n <div class=\"pin-progress-backdrop\">\n <div class=\"pin-progress-modal\">\n <mj-loading [showText]=\"false\" size=\"small\"></mj-loading>\n <span class=\"pin-progress-text\">{{ PinProgressText }}</span>\n </div>\n </div>\n}\n", styles: [":host {\n display: block;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n/* MJ Logo - uses design tokens for theme switching & white-labeling.\n Geometry follows --mj-logo-width/-height so a differently-shaped org mark fits;\n --mj-logo-plate-bg is set by the theme engine only when a single-variant logo needs\n a legibility plate in the mode it wasn't designed for (org-theming Phase 4). */\n.mj-logo {\n width: var(--mj-logo-width, 32px);\n height: var(--mj-logo-height, 18px);\n background-image: var(--mj-logo-mark);\n background-color: var(--mj-logo-plate-bg, transparent);\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border-radius: var(--mj-radius-sm, 4px);\n flex-shrink: 0;\n}\n\n/* Skip link (WCAG 2.4.1) \u2014 off-screen until focused, then pinned over the header.\n It is the first focusable element in the shell; consuming apps should drop their\n own skip link on upgrade rather than stacking a second one in front of this. */\n.mj-skip-link {\n position: absolute;\n top: var(--mj-space-2);\n left: var(--mj-space-2);\n z-index: 100000;\n padding: var(--mj-space-2) var(--mj-space-4);\n background: var(--mj-bg-surface-elevated);\n color: var(--mj-text-primary);\n border: 1px solid var(--mj-border-strong);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-lg);\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-semibold);\n text-decoration: none;\n transform: translateY(-200%);\n transition: transform var(--mj-transition-fast);\n}\n.mj-skip-link:focus,\n.mj-skip-link:focus-visible {\n transform: translateY(0);\n outline: var(--mj-ring-width) solid var(--mj-focus-ring-color);\n outline-offset: var(--mj-ring-offset);\n}\n\n/* The main landmark is also the skip-link target; focusing it programmatically must\n not paint a ring on a region the user did not tab to. */\nmj-tab-container:focus {\n outline: none;\n}\n\n.shell-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n/* Hide shell container while loading - allows tab container to render and load\n first resource in background while shell loading indicator is visible */\n.shell-container.hidden {\n visibility: hidden;\n position: absolute;\n pointer-events: none;\n}\n\nmj-tab-container {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n transition: all 0.2s ease-in-out;\n}\n\n/* Hide Golden Layout tab headers when only one tab \u2014 scoped to the MAIN\n region: the RECORDS region is a separate Golden Layout whose tab strip is\n its whole point (switching between open records) and must never be hidden\n by the main bar's visibility policy. */\nmj-tab-container.hide-tab-bar ::ng-deep .main-region .lm_header {\n display: none;\n}\n\n/* Show tab headers when multiple tabs */\nmj-tab-container:not(.hide-tab-bar) ::ng-deep .main-region .lm_header {\n opacity: 1;\n max-height: 40px;\n transition: opacity 0.2s ease-in-out, max-height 0.2s ease-in-out;\n}\n\n/* Adjust content area height when tabs hidden */\nmj-tab-container.hide-tab-bar ::ng-deep .main-region .lm_content {\n height: 100% !important;\n}\n\n/* Ensure smooth transitions */\nmj-tab-container ::ng-deep .lm_stack {\n transition: all 0.2s ease-in-out;\n}\n\n.shell-header {\n height: 60px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n display: flex;\n align-items: center;\n padding: 0 var(--mj-space-4);\n gap: var(--mj-space-4);\n box-shadow: var(--mj-shadow-sm);\n flex-shrink: 0;\n /* Creates a stacking context so children (search bar z-index: 99999, dropdown)\n are contained within this level. Modal overlays (z-index: 1000+) sit above. */\n position: relative;\n z-index: 500;\n}\n\n/* Nav Bar Apps - permanent app icons in the header */\n.nav-bar-apps {\n display: flex;\n align-items: center;\n gap: var(--mj-space-1);\n}\n\n.nav-bar-apps.left-of-switcher {\n /* No extra margin - header gap handles spacing */\n /* This prevents the app switcher from shifting when icons are hidden */\n}\n\n.nav-bar-apps.left-of-user-menu {\n margin-right: var(--mj-space-2);\n}\n\n.nav-bar-app-btn {\n --app-color: var(--mj-text-muted);\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-md);\n border: none;\n background: transparent;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-secondary);\n font-size: var(--mj-text-lg);\n transition: all var(--mj-transition-fast);\n position: relative;\n}\n\n.nav-bar-app-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n/* Active = brand-primary (matches nav pills + switcher items); the app's\n metadata color is reserved for identity accents, never state.\n Shared active-tint recipe. */\n.nav-bar-app-btn.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--mj-brand-primary);\n}\n\n.nav-bar-app-btn i {\n transition: transform var(--mj-transition-fast);\n margin-top: var(--mj-space-0-5); /* Align with app switcher icon */\n}\n\n.nav-bar-app-btn:hover i {\n transform: scale(1.1);\n}\n\n.spacer {\n flex: 1;\n}\n\n/* Priority+ overflow support: on desktop the app nav owns the header's free\n middle space (so it knows exactly how much room its items have) and the\n spacer stands down. On mobile the nav is display:none'd, so the spacer's\n flex: 1 keeps the action cluster right-aligned. When the nav isn't rendered\n at all (app-locked sessions, Shell.AppNav.Enabled off), the sibling selector\n doesn't match and the spacer behaves as before. */\n@media (min-width: 769px) {\n mj-app-nav.desktop-nav {\n flex: 1 1 auto;\n min-width: 0;\n }\n mj-app-nav.desktop-nav + .spacer {\n flex: 0 0 0px;\n }\n}\n\n.header-actions {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n}\n\n.icon-btn {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n border: none;\n background: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-lg);\n transition: background var(--mj-transition-fast);\n}\n\n.icon-btn:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n/* Keyboard focus parity for all header controls (the search bar already has\n its own ring at .search-btn:focus-visible). */\n.nav-bar-app-btn:focus-visible,\n.icon-btn:focus-visible,\n.hamburger-btn:focus-visible,\n.user-menu .avatar-btn:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n\n/* Universal search: a static-width bar styled as an input, anchored on the\n right with the action cluster. Deliberately NOT resizable on hover/focus \u2014\n chrome that shifts on interaction was rejected in the 2026-07 review. It's a\n button (typing happens in the overlay it opens), so no focus/caret styling. */\n.search-btn {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n width: 260px;\n height: 36px;\n padding: 0 var(--mj-space-2-5) 0 var(--mj-space-3-5);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n font-family: inherit;\n cursor: pointer;\n transition: var(--mj-transition-colors);\n}\n\n.search-btn:hover {\n border-color: var(--mj-border-strong);\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n}\n\n.search-btn:focus-visible {\n outline: none;\n border-color: var(--mj-border-focus);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n/* Omnibar-off mobile trigger: hidden on desktop (the inline composite renders\n instead); the 768px block flips it on as the icon circle. */\n.search-btn-mobile {\n display: none;\n}\n\n/* Omnibar-off desktop: the inline search composite, same slot + width as the\n omnibar bar so the opt-in toggle doesn't move the header. */\n.shell-search-bar {\n position: relative;\n width: 260px;\n z-index: 99999;\n}\n\n/* The suggest dropdown needs more width than its 260px anchor. Pin it to the\n anchor's right edge and let it grow leftward \u2014 the bar is right-anchored, so\n leftward growth stays on screen. */\n.shell-search-bar ::ng-deep .suggest-dropdown {\n left: auto;\n right: 0;\n width: min(560px, calc(100vw - var(--mj-space-6)));\n}\n\n.search-btn i {\n font-size: var(--mj-text-sm);\n flex-shrink: 0;\n}\n\n.search-btn .search-btn-text {\n flex: 1;\n min-width: 0;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* kbd chip: deliberately mirrors the omnibar palette's .ob-kbd chrome\n (10.5px / 5px radius / bottom-heavy border) so the affordance and the\n surface it summons read as one system. */\n.search-btn .search-btn-kbd {\n flex-shrink: 0;\n white-space: nowrap;\n font-family: inherit;\n font-size: 10.5px;\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong);\n border-bottom-width: 2px;\n border-radius: 5px;\n padding: 1.5px var(--mj-space-1-5);\n}\n\n/* Notification button with badge */\n.notification-btn {\n position: relative;\n}\n\n.notification-badge {\n position: absolute;\n top: 4px;\n right: 4px;\n min-width: 16px;\n height: 16px;\n padding: 0 var(--mj-space-1);\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\n font-size: 10px;\n font-weight: var(--mj-font-semibold);\n border-radius: var(--mj-radius-full);\n display: flex;\n align-items: center;\n justify-content: center;\n line-height: var(--mj-leading-none);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.user-menu {\n position: relative;\n}\n\n.user-menu .avatar-btn {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n border: none;\n background: transparent;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: box-shadow var(--mj-transition-fast), transform var(--mj-transition-fast);\n padding: 0;\n overflow: hidden;\n}\n\n.user-menu .avatar-btn:hover {\n transform: scale(1.05);\n}\n\n/* Icon fallback styling - shows gray circle with icon */\n.user-menu .avatar-btn .icon-fallback {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n border: 2px solid var(--mj-border-default);\n background: var(--mj-bg-surface-hover);\n display: flex;\n align-items: center;\n justify-content: center;\n transition: border-color var(--mj-transition-fast);\n}\n\n.user-menu .avatar-btn:hover .icon-fallback {\n border-color: var(--mj-brand-primary);\n}\n\n.user-menu .avatar-btn .icon-fallback i {\n color: var(--mj-text-muted);\n font-size: var(--mj-text-base);\n}\n\n/* Avatar image - replaces the gray circle entirely */\n.user-menu .avatar-btn .avatar-img {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n object-fit: cover;\n box-shadow: var(--mj-shadow-sm);\n}\n\n.user-menu .avatar-btn:hover .avatar-img {\n box-shadow: var(--mj-shadow-md);\n}\n\n/* User Menu Header */\n.user-menu-header {\n padding: var(--mj-space-3) var(--mj-space-4);\n background: var(--mj-bg-surface-sunken);\n}\n\n.user-menu-header .user-info {\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-0-5);\n}\n\n.user-menu-header .user-name {\n font-weight: var(--mj-font-semibold);\n font-size: var(--mj-text-sm);\n color: var(--mj-text-primary);\n}\n\n.user-menu-header .user-email {\n font-size: var(--mj-text-xs);\n color: var(--mj-text-muted);\n}\n\n.user-menu-header .user-subtitle {\n font-size: 11px;\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n/* User Context Menu */\n.user-context-menu {\n position: absolute;\n top: 100%;\n right: 0;\n margin-top: var(--mj-space-2);\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-lg);\n min-width: 180px;\n /* Never wider than the viewport, never taller than what is left below the 60px header:\n a short window (or a phone) used to leave the bottom items unreachable, because the\n menu had no height limit and clipped instead of scrolling. */\n max-width: min(360px, calc(100vw - 2 * var(--mj-space-4)));\n max-height: calc(100vh - 60px - 3 * var(--mj-space-2));\n max-height: calc(100dvh - 60px - 3 * var(--mj-space-2));\n z-index: 10000;\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.user-menu-item {\n display: flex;\n align-items: center;\n gap: var(--mj-space-3);\n padding: var(--mj-space-3) var(--mj-space-4);\n cursor: pointer;\n font-size: var(--mj-text-sm);\n color: var(--mj-text-primary);\n transition: background var(--mj-transition-fast);\n}\n\n.user-menu-item i {\n width: 18px;\n text-align: center;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-sm);\n}\n\n.user-menu-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.user-menu-item.danger {\n color: var(--mj-status-error);\n}\n\n.user-menu-item.danger i {\n color: var(--mj-status-error);\n}\n\n.user-menu-item.danger:hover {\n background: color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface-elevated));\n}\n\n.user-menu-divider {\n height: 1px;\n background: var(--mj-border-default);\n margin: var(--mj-space-1) 0;\n}\n\n/* Menu item disabled state */\n.user-menu-item.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.user-menu-item.disabled:hover {\n background: transparent;\n}\n\n/* Theme Toggle Switch */\n.theme-toggle-item {\n cursor: pointer;\n}\n\n.theme-toggle-track {\n position: relative;\n width: 40px;\n height: 22px;\n border-radius: var(--mj-radius-full);\n background: var(--mj-bg-surface-active);\n margin-left: auto;\n transition: background var(--mj-transition-base);\n flex-shrink: 0;\n}\n\n.theme-toggle-track.dark {\n background: var(--mj-brand-primary);\n}\n\n.theme-toggle-thumb {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n background: var(--mj-bg-surface);\n box-shadow: var(--mj-shadow-sm);\n display: flex;\n align-items: center;\n justify-content: center;\n transition: transform var(--mj-transition-base);\n}\n\n.theme-toggle-track.dark .theme-toggle-thumb {\n transform: translateX(18px);\n}\n\n.theme-toggle-icon {\n font-size: 10px;\n color: var(--mj-text-muted);\n}\n\n.theme-toggle-track.dark .theme-toggle-icon {\n color: var(--mj-text-inverse);\n}\n\n/* Menu shortcut hint */\n.user-menu-item .menu-shortcut {\n margin-left: auto;\n font-size: 11px;\n color: var(--mj-text-disabled);\n font-family: monospace;\n}\n\n/* Menu animations */\n.user-context-menu.menu-fade {\n animation: menuFadeIn 0.15s ease-out;\n}\n\n.user-context-menu.menu-slide {\n animation: menuSlideIn 0.2s ease-out;\n}\n\n@keyframes menuFadeIn {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes menuSlideIn {\n from {\n opacity: 0;\n transform: translateY(-16px) scale(0.95);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.shell-loading {\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: 99999;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding-bottom: 15vh;\n background: var(--mj-bg-page);\n}\n\n.loading-version {\n margin-top: var(--mj-space-8, 24px);\n font-size: 11px;\n color: var(--mj-text-muted);\n letter-spacing: 0.4px;\n opacity: 0.6;\n user-select: none;\n}\n\n/* Loading Recovery Reset Panel */\n.loading-reset-panel {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--mj-space-3);\n margin-top: var(--mj-space-10);\n padding: var(--mj-space-8);\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg);\n box-shadow: var(--mj-shadow-md);\n max-width: 420px;\n animation: resetPanelFadeIn 0.4s ease-out;\n}\n\n.loading-reset-message {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n margin: 0;\n font-size: var(--mj-text-base);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-primary);\n}\n\n.loading-reset-message i {\n font-size: var(--mj-text-lg);\n color: var(--mj-status-warning);\n}\n\n.loading-reset-hint {\n margin: 0;\n font-size: var(--mj-text-sm);\n color: var(--mj-text-secondary);\n text-align: center;\n line-height: var(--mj-leading-relaxed);\n}\n\n.loading-reset-btn {\n display: inline-flex;\n align-items: center;\n gap: var(--mj-space-2);\n margin-top: var(--mj-space-3);\n padding: var(--mj-space-3) var(--mj-space-6);\n font-size: var(--mj-text-base);\n font-weight: var(--mj-font-medium);\n font-family: var(--mj-font-family);\n color: var(--mj-text-inverse);\n background: var(--mj-brand-primary);\n border: 1px solid var(--mj-brand-primary);\n border-radius: var(--mj-radius-md);\n cursor: pointer;\n transition: var(--mj-transition-colors);\n}\n\n.loading-reset-btn:hover {\n background: var(--mj-brand-primary-hover);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary-hover);\n box-shadow: var(--mj-shadow-md);\n}\n\n.loading-reset-btn:active {\n background: var(--mj-brand-primary-active);\n color: var(--mj-text-inverse);\n transform: scale(0.98);\n}\n\n.loading-reset-btn i {\n font-size: var(--mj-text-sm);\n}\n\n@keyframes resetPanelFadeIn {\n from {\n opacity: 0;\n transform: translateY(12px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Hamburger button - hidden on desktop */\n.hamburger-btn {\n display: none;\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-md);\n border: none;\n background: none;\n cursor: pointer;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-secondary);\n font-size: var(--mj-text-xl);\n transition: background var(--mj-transition-fast);\n}\n.hamburger-btn:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n/* Mobile Navigation Overlay */\n.mobile-nav-overlay {\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: var(--mj-bg-overlay);\n z-index: 9998;\n}\n\n/* Mobile Navigation Drawer */\n/* `visibility` is transitioned alongside `transform` so the closed drawer is genuinely\n hidden \u2014 off-screen alone leaves its controls hit-testable and visible to\n checkVisibility(), which is what let 6 of its 8 controls keep taking focus. The\n delay holds visibility through the slide-out, then drops it; opening is instant\n (0s) so the drawer is present for the whole slide-in. `inert` (set in the template)\n is the actual focus guard; this keeps the CSS from disagreeing with it. */\n.mobile-nav-drawer {\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n width: 280px;\n max-width: 85vw;\n background: var(--mj-bg-surface);\n box-shadow: var(--mj-shadow-xl);\n z-index: 9999;\n flex-direction: column;\n transform: translateX(-100%);\n visibility: hidden;\n transition: transform var(--mj-transition-slow), visibility 0s linear 300ms;\n}\n.mobile-nav-drawer.open {\n transform: translateX(0);\n visibility: visible;\n transition: transform var(--mj-transition-slow), visibility 0s linear 0s;\n}\n\n/* Reduced motion: no slide, so nothing to hold visibility open for. */\n@media (prefers-reduced-motion: reduce) {\n .mobile-nav-drawer,\n .mobile-nav-drawer.open {\n transition: none;\n }\n}\n\n.mobile-nav-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--mj-space-4) var(--mj-space-5);\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n}\n.mobile-nav-header span {\n font-weight: var(--mj-font-semibold);\n font-size: var(--mj-text-base);\n color: var(--mj-text-primary);\n}\n.mobile-nav-header .close-btn {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n border: none;\n background: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n font-size: var(--mj-text-lg);\n transition: background var(--mj-transition-fast);\n}\n.mobile-nav-header .close-btn:hover {\n background: var(--mj-bg-surface-active);\n}\n\n.mobile-nav-content {\n flex: 1;\n overflow-y: auto;\n padding: var(--mj-space-4) 0;\n}\n\n.mobile-nav-section-title {\n padding: var(--mj-space-2) var(--mj-space-5);\n font-size: var(--mj-text-xs);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-disabled);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n/* Records entry in the mobile drawer (records-style record opens) \u2014\n the desktop pill lives in the mobile-hidden app nav, so the drawer\n carries its own touch-friendly instance. */\n.mobile-nav-records {\n padding: var(--mj-space-2) var(--mj-space-3);\n border-top: 1px solid var(--mj-border-default);\n}\n.mobile-nav-records .records-pill {\n width: 100%;\n min-height: 48px; /* Touch-target parity with drawer nav items */\n justify-content: flex-start;\n}\n\n.mobile-nav-footer {\n border-top: 1px solid var(--mj-border-default);\n padding: var(--mj-space-3) var(--mj-space-4);\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-1);\n}\n\n.mobile-nav-action {\n position: relative;\n display: flex;\n align-items: center;\n gap: var(--mj-space-3);\n padding: var(--mj-space-3) var(--mj-space-4);\n border: none;\n background: none;\n cursor: pointer;\n border-radius: var(--mj-radius-md);\n color: var(--mj-text-secondary);\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-medium);\n transition: background var(--mj-transition-fast);\n width: 100%;\n text-align: left;\n}\n.mobile-nav-action:hover {\n background: var(--mj-bg-surface-hover);\n}\n.mobile-nav-action i {\n font-size: var(--mj-text-base);\n width: 20px;\n text-align: center;\n}\n\n/* Mobile Responsive Styles */\n@media (max-width: 768px) {\n .hamburger-btn {\n display: flex;\n }\n\n .desktop-nav {\n display: none !important;\n }\n\n .desktop-only {\n display: none !important;\n }\n\n .mobile-nav-overlay {\n display: block;\n }\n\n .mobile-nav-drawer {\n display: flex;\n }\n\n .shell-header {\n padding: 0 var(--mj-space-3);\n gap: var(--mj-space-2);\n }\n\n /* Keep the right-edge cluster (chat/nav-bar app icons + avatar) on one row.\n Without pinning this, the icons can wrap and the avatar drops onto a second\n line, stacking under the chat icon. */\n .header-actions {\n flex-direction: row;\n flex-wrap: nowrap;\n }\n .header-actions .user-menu,\n .header-actions .nav-bar-apps,\n .header-actions .search-btn {\n flex-shrink: 0;\n }\n\n /* Search bar collapses to the icon-only circle on small viewports \u2014\n the placeholder text + kbd chip are already hidden via .desktop-only.\n The omnibar-off mobile trigger (.search-btn-mobile) becomes visible here;\n its .desktop-only composite sibling is hidden by the rule above. */\n .search-btn {\n display: flex;\n width: 40px;\n height: 40px;\n padding: 0;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: none;\n }\n .search-btn i {\n font-size: var(--mj-text-lg);\n }\n .search-btn:hover {\n background: var(--mj-bg-surface-hover);\n border: none;\n }\n}\n\n/* Settings Full-Screen Window Styles */\n::ng-deep .settings-fullscreen-window {\n /* Remove window chrome for full-screen feel */\n box-shadow: none !important;\n border: none !important;\n}\n\n::ng-deep .settings-fullscreen-window .k-window-titlebar {\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n padding: var(--mj-space-3) var(--mj-space-4);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-title {\n font-size: var(--mj-text-lg);\n font-weight: var(--mj-font-semibold);\n color: var(--mj-text-primary);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-titlebar-actions {\n gap: var(--mj-space-1);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action {\n width: 32px;\n height: 32px;\n border-radius: var(--mj-radius-md);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n::ng-deep .settings-fullscreen-window .k-window-content {\n padding: 0;\n overflow: auto;\n background: var(--mj-bg-page);\n}\n\n/* Hide minimize/maximize buttons - only show close */\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action[title=\"Minimize\"],\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action[title=\"Maximize\"],\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action .k-i-window-minimize,\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action .k-i-window-maximize,\n::ng-deep .settings-fullscreen-window .k-window-titlebar-action .k-i-window {\n display: none !important;\n}\n\n/* Universal search is a right-anchored bar affordance in header-actions\n (2026-07 design review round 2): both experiences still open as overlays \u2014\n the bar is a trigger, not an inline input. Icon-only under 768px. */\n\n/* ========================================\n SEARCH POPUP\n ======================================== */\n\n.search-popup-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: var(--mj-bg-overlay);\n z-index: 9999;\n animation: fadeIn 0.15s ease;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.search-popup {\n position: absolute;\n top: 60px; /* Below header */\n right: 16px;\n width: 480px;\n max-width: calc(100vw - 32px);\n background: var(--mj-bg-surface-elevated);\n border-radius: var(--mj-radius-lg);\n box-shadow: var(--mj-shadow-xl);\n z-index: 10000;\n opacity: 0;\n transform: translateY(-10px);\n pointer-events: none;\n visibility: hidden;\n transition: opacity var(--mj-transition-base), transform var(--mj-transition-base), visibility 0s linear 200ms;\n}\n\n.search-popup.open {\n opacity: 1;\n transform: translateY(0);\n pointer-events: all;\n visibility: visible;\n transition: opacity var(--mj-transition-base), transform var(--mj-transition-base), visibility 0s linear 0s;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .search-popup,\n .search-popup.open {\n transition: none;\n }\n}\n\n.search-popup-content {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n padding: var(--mj-space-3);\n}\n\n.search-entity-dropdown {\n min-width: 140px;\n max-width: 180px;\n flex-shrink: 0;\n}\n\n.search-input {\n flex: 1;\n min-width: 0;\n height: 40px;\n padding: var(--mj-space-2) var(--mj-space-3);\n font-size: var(--mj-text-sm);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm);\n outline: none;\n display: block !important;\n box-sizing: border-box;\n}\n\n.search-input:focus {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.search-submit-btn {\n width: 40px;\n height: 40px;\n border-radius: var(--mj-radius-md);\n border: none;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: var(--mj-text-base);\n transition: background var(--mj-transition-fast);\n flex-shrink: 0;\n}\n\n.search-submit-btn:hover {\n background: var(--mj-brand-primary-hover);\n}\n\n.search-submit-btn:active {\n background: var(--mj-brand-primary-hover);\n}\n\n/* Mobile notification badge */\n.notification-badge-mobile {\n position: absolute;\n top: 8px;\n right: 8px;\n min-width: 18px;\n height: 18px;\n padding: 0 5px;\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\n font-size: 11px;\n font-weight: var(--mj-font-semibold);\n border-radius: var(--mj-radius-full);\n display: flex;\n align-items: center;\n justify-content: center;\n line-height: var(--mj-leading-none);\n box-shadow: var(--mj-shadow-sm);\n}\n\n/* Mobile-specific search popup adjustments */\n@media (max-width: 768px) {\n .search-popup {\n top: 60px;\n left: 16px;\n right: 16px;\n width: auto;\n max-width: none;\n }\n\n .search-popup-content {\n flex-wrap: wrap;\n }\n\n .search-entity-dropdown {\n width: 100%;\n }\n\n .search-input {\n flex: 1;\n min-width: 0;\n }\n}\n\n/* ========================================\n PIN PROGRESS OVERLAY\n ======================================== */\n.pin-progress-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: color-mix(in srgb, var(--mj-text-primary) 40%, transparent);\n z-index: 100000;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: pinBackdropIn 0.2s ease;\n}\n\n@keyframes pinBackdropIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.pin-progress-modal {\n background: var(--mj-bg-surface);\n border-radius: var(--mj-radius-xl);\n padding: var(--mj-space-7) var(--mj-space-10);\n display: flex;\n align-items: center;\n gap: var(--mj-space-4);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--mj-text-primary) 30%, transparent);\n animation: pinModalIn 0.25s ease;\n}\n\n@keyframes pinModalIn {\n from { opacity: 0; transform: scale(0.9) translateY(8px); }\n to { opacity: 1; transform: scale(1) translateY(0); }\n}\n\n.pin-progress-text {\n font-size: var(--mj-text-sm);\n font-weight: var(--mj-font-medium);\n color: var(--mj-text-secondary);\n white-space: nowrap;\n}\n/* ============================================================\n Global Activity indicator (P3)\n ============================================================ */\n.activity-wrap { position: relative; display: inline-flex; }\n.activity-btn { position: relative; }\n.activity-btn .activity-pulse { color: var(--mj-brand-primary); }\n.activity-badge {\n position: absolute;\n top: 2px;\n right: 2px;\n min-width: 16px;\n height: 16px;\n padding: 0 var(--mj-space-1);\n border-radius: var(--mj-radius-md);\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 10px;\n font-weight: var(--mj-font-bold);\n line-height: 16px;\n text-align: center;\n}\n.activity-drawer {\n position: absolute;\n top: calc(100% + 8px);\n right: 0;\n width: 320px;\n max-height: 420px;\n display: flex;\n flex-direction: column;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: 0 8px 28px color-mix(in srgb, var(--mj-text-primary) 18%, transparent);\n z-index: 1000;\n overflow: hidden;\n}\n.activity-drawer-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--mj-space-2-5) var(--mj-space-3);\n font-weight: var(--mj-font-bold);\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n border-bottom: 1px solid var(--mj-border-default);\n}\n.activity-clear {\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 0.8rem;\n padding: var(--mj-space-1) var(--mj-space-1-5);\n border-radius: var(--mj-radius-md);\n}\n.activity-clear:hover { color: var(--mj-text-primary); background: var(--mj-bg-surface-hover); }\n.activity-list { overflow-y: auto; padding: var(--mj-space-1-5); }\n.activity-row {\n display: flex;\n gap: var(--mj-space-2-5);\n padding: var(--mj-space-2);\n border-radius: var(--mj-radius-md);\n}\n.activity-row:hover { background: var(--mj-bg-surface-hover); }\n.activity-row-icon { margin-top: var(--mj-space-0-5); color: var(--mj-brand-primary); font-size: 0.85rem; }\n.activity-row.is-error .activity-row-icon { color: var(--mj-status-error); }\n.activity-row-body { flex: 1; min-width: 0; }\n.activity-row-label { font-size: 0.82rem; font-weight: var(--mj-font-semibold); color: var(--mj-text-primary); }\n.activity-row-detail { font-size: 0.72rem; color: var(--mj-text-muted); margin-top: var(--mj-space-px); }\n.activity-bar {\n margin-top: var(--mj-space-1-5);\n height: 4px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 2px;\n overflow: hidden;\n}\n.activity-bar-fill { height: 100%; background: var(--mj-brand-primary); transition: width var(--mj-transition-slow); }\n\n"] }]
|
|
4180
|
+
}], () => [{ type: i1.ApplicationManager }, { type: i1.WorkspaceStateManager }, { type: i1.GoldenLayoutManager }, { type: i1.TabService }, { type: i2.NavigationService }, { type: i3.ActivatedRoute }, { type: i3.Router }, { type: i4.MJAuthBase }, { type: i0.ChangeDetectorRef }, { type: i5.UserAvatarService }, { type: i6.UserSharingCenterDialogService }, { type: i7.AboutDialogService }, { type: i8.ProfileDialogService }, { type: i0.ViewContainerRef }, { type: i2.TitleService }, { type: i2.DeveloperModeService }, { type: i9.StartupValidationService }, { type: i10.CommandPaletteService }, { type: i2.ThemeService }, { type: i2.HomeAppPinService }, { type: i11.FileOpenService }, { type: i12.FeedbackDialogService }, { type: i12.FeedbackService }, { type: i2.ActivityService }], { SearchInput: [{
|
|
3764
4181
|
type: ViewChild,
|
|
3765
4182
|
args: ['searchInput']
|
|
3766
|
-
}],
|
|
4183
|
+
}], MobileNavDrawer: [{
|
|
3767
4184
|
type: ViewChild,
|
|
3768
4185
|
args: ['mobileNavDrawer']
|
|
3769
|
-
}],
|
|
4186
|
+
}], MobileNavToggle: [{
|
|
3770
4187
|
type: ViewChild,
|
|
3771
4188
|
args: ['mobileNavToggle']
|
|
3772
|
-
}],
|
|
4189
|
+
}], OmnibarPalette: [{
|
|
3773
4190
|
type: ViewChild,
|
|
3774
4191
|
args: ['omnibarPalette']
|
|
3775
|
-
}],
|
|
4192
|
+
}], ShellSearchComposite: [{
|
|
3776
4193
|
type: ViewChild,
|
|
3777
4194
|
args: ['shellSearchComposite']
|
|
3778
|
-
}],
|
|
4195
|
+
}], TabContainerRef: [{
|
|
3779
4196
|
type: ViewChild,
|
|
3780
4197
|
args: [TabContainerComponent]
|
|
3781
|
-
}],
|
|
4198
|
+
}], AppAccessDialog: [{
|
|
3782
4199
|
type: ViewChild,
|
|
3783
4200
|
args: ['appAccessDialog']
|
|
3784
|
-
}], handleGlobalKeyboardShortcuts: [{
|
|
3785
|
-
type: HostListener,
|
|
3786
|
-
args: ['document:keydown', ['$event']]
|
|
3787
4201
|
}], OnGlobalKeydown: [{
|
|
3788
4202
|
type: HostListener,
|
|
3789
4203
|
args: ['document:keydown', ['$event']]
|
|
@@ -3791,5 +4205,5 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3791
4205
|
type: HostListener,
|
|
3792
4206
|
args: ['document:click']
|
|
3793
4207
|
}] }); })();
|
|
3794
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ShellComponent, { className: "ShellComponent", filePath: "src/lib/shell/shell.component.ts", lineNumber:
|
|
4208
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ShellComponent, { className: "ShellComponent", filePath: "src/lib/shell/shell.component.ts", lineNumber: 59 }); })();
|
|
3795
4209
|
//# sourceMappingURL=shell.component.js.map
|