@memberjunction/ng-explorer-core 6.2.0-edge.0 → 6.2.0-edge.2
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 +50 -14
- 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 +12 -1
- 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 +199 -25
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +534 -182
- 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/compose-email-fallback.d.ts +46 -0
- package/dist/lib/resource-wrappers/compose-email-fallback.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/compose-email-fallback.js +62 -0
- package/dist/lib/resource-wrappers/compose-email-fallback.js.map +1 -0
- 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 +74 -11
- package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/view-resource.component.js +214 -68
- package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
- package/dist/lib/services/server-connectivity.service.d.ts +9 -4
- package/dist/lib/services/server-connectivity.service.d.ts.map +1 -1
- package/dist/lib/services/server-connectivity.service.js +56 -12
- package/dist/lib/services/server-connectivity.service.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.js +1 -1
- 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 +50 -6
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.js +95 -22
- package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +91 -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 +257 -101
- 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 +2 -0
- package/dist/lib/shell/readiness-beacon.d.ts.map +1 -1
- package/dist/lib/shell/readiness-beacon.js +5 -1
- package/dist/lib/shell/readiness-beacon.js.map +1 -1
- 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 +264 -39
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +697 -294
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Component, ViewChild, HostListener } from '@angular/core';
|
|
1
|
+
import { Component, ViewChild, HostListener, inject } from '@angular/core';
|
|
2
|
+
import { AngularAdapterService } from '@memberjunction/ng-react';
|
|
2
3
|
import { NavigationEnd } from '@angular/router';
|
|
3
4
|
import { combineLatest, Subject } from 'rxjs';
|
|
4
5
|
import { filter, takeUntil } from 'rxjs/operators';
|
|
@@ -15,7 +16,7 @@ import { InstanceConfigEngine, UserInfoEngine } from '@memberjunction/core-entit
|
|
|
15
16
|
import { PACKAGE_VERSION } from '@memberjunction/graphql-dataprovider';
|
|
16
17
|
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
|
|
17
18
|
import { ApplyShellChromePolicy, BaseShellChromePolicy } from './shell-chrome-policy';
|
|
18
|
-
import {
|
|
19
|
+
import { SetReadinessBeacon } from './readiness-beacon';
|
|
19
20
|
import * as i0 from "@angular/core";
|
|
20
21
|
import * as i1 from "@memberjunction/ng-base-application";
|
|
21
22
|
import * as i2 from "@memberjunction/ng-shared";
|
|
@@ -568,7 +569,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
568
569
|
profileDialogService;
|
|
569
570
|
viewContainerRef;
|
|
570
571
|
titleService;
|
|
571
|
-
|
|
572
|
+
DeveloperModeService;
|
|
572
573
|
startupValidationService;
|
|
573
574
|
commandPaletteService;
|
|
574
575
|
themeService;
|
|
@@ -582,7 +583,15 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
582
583
|
/** Newest workspace configuration emission — see the staleness guard in the Configuration subscription */
|
|
583
584
|
latestSyncedConfig = null;
|
|
584
585
|
initialNavigationComplete = false; // Track if initial navigation has completed
|
|
585
|
-
|
|
586
|
+
ActiveApp = null;
|
|
587
|
+
/** @deprecated Use {@link ActiveApp}. */
|
|
588
|
+
get activeApp() {
|
|
589
|
+
return this.ActiveApp;
|
|
590
|
+
}
|
|
591
|
+
/** @deprecated Use {@link ActiveApp}. */
|
|
592
|
+
set activeApp(value) {
|
|
593
|
+
this.ActiveApp = value;
|
|
594
|
+
}
|
|
586
595
|
_loading = true;
|
|
587
596
|
/**
|
|
588
597
|
* Shell loading-screen flag, and the source of the Computer Use readiness
|
|
@@ -594,16 +603,40 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
594
603
|
* places assign `loading` — routing, resource load, app switch, error paths.
|
|
595
604
|
* Publishing here means every one of them stays correct for free.
|
|
596
605
|
*/
|
|
597
|
-
get
|
|
606
|
+
get Loading() {
|
|
598
607
|
return this._loading;
|
|
599
608
|
}
|
|
600
|
-
set
|
|
609
|
+
set Loading(value) {
|
|
601
610
|
this._loading = value;
|
|
602
|
-
|
|
611
|
+
SetReadinessBeacon(!value);
|
|
612
|
+
}
|
|
613
|
+
/** @deprecated Use {@link Loading}. */
|
|
614
|
+
get loading() {
|
|
615
|
+
return this.Loading;
|
|
616
|
+
}
|
|
617
|
+
/** @deprecated Use {@link Loading}. */
|
|
618
|
+
set loading(value) {
|
|
619
|
+
this.Loading = value;
|
|
620
|
+
}
|
|
621
|
+
Initialized = false;
|
|
622
|
+
/** @deprecated Use {@link Initialized}. */
|
|
623
|
+
get initialized() {
|
|
624
|
+
return this.Initialized;
|
|
625
|
+
}
|
|
626
|
+
/** @deprecated Use {@link Initialized}. */
|
|
627
|
+
set initialized(value) {
|
|
628
|
+
this.Initialized = value;
|
|
603
629
|
}
|
|
604
|
-
initialized = false;
|
|
605
630
|
waitingForFirstResource = false;
|
|
606
|
-
|
|
631
|
+
TabBarVisible = true;
|
|
632
|
+
/** @deprecated Use {@link TabBarVisible}. */
|
|
633
|
+
get tabBarVisible() {
|
|
634
|
+
return this.TabBarVisible;
|
|
635
|
+
}
|
|
636
|
+
/** @deprecated Use {@link TabBarVisible}. */
|
|
637
|
+
set tabBarVisible(value) {
|
|
638
|
+
this.TabBarVisible = value;
|
|
639
|
+
} // Controlled by workspace manager
|
|
607
640
|
/**
|
|
608
641
|
* True when the deployment uses the records-as-tabs record-open style
|
|
609
642
|
* (`Shell.RecordOpen.Style` != 'classic'): records open as native Golden
|
|
@@ -614,15 +647,79 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
614
647
|
get RecordTabsStyle() {
|
|
615
648
|
return this.chromeFlags.recordOpenStyle === 'records';
|
|
616
649
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
650
|
+
UserMenuVisible = false;
|
|
651
|
+
/** @deprecated Use {@link UserMenuVisible}. */
|
|
652
|
+
get userMenuVisible() {
|
|
653
|
+
return this.UserMenuVisible;
|
|
654
|
+
}
|
|
655
|
+
/** @deprecated Use {@link UserMenuVisible}. */
|
|
656
|
+
set userMenuVisible(value) {
|
|
657
|
+
this.UserMenuVisible = value;
|
|
658
|
+
} // User avatar context menu
|
|
659
|
+
MobileNavOpen = false;
|
|
660
|
+
/** @deprecated Use {@link MobileNavOpen}. */
|
|
661
|
+
get mobileNavOpen() {
|
|
662
|
+
return this.MobileNavOpen;
|
|
663
|
+
}
|
|
664
|
+
/** @deprecated Use {@link MobileNavOpen}. */
|
|
665
|
+
set mobileNavOpen(value) {
|
|
666
|
+
this.MobileNavOpen = value;
|
|
667
|
+
} // Mobile navigation drawer
|
|
668
|
+
UnreadNotificationCount = 0;
|
|
669
|
+
/** @deprecated Use {@link UnreadNotificationCount}. */
|
|
670
|
+
get unreadNotificationCount() {
|
|
671
|
+
return this.UnreadNotificationCount;
|
|
672
|
+
}
|
|
673
|
+
/** @deprecated Use {@link UnreadNotificationCount}. */
|
|
674
|
+
set unreadNotificationCount(value) {
|
|
675
|
+
this.UnreadNotificationCount = value;
|
|
676
|
+
} // Notification badge count
|
|
620
677
|
// Global Activity indicator (P3)
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
678
|
+
ActivityItems = [];
|
|
679
|
+
/** @deprecated Use {@link ActivityItems}. */
|
|
680
|
+
get activityItems() {
|
|
681
|
+
return this.ActivityItems;
|
|
682
|
+
}
|
|
683
|
+
/** @deprecated Use {@link ActivityItems}. */
|
|
684
|
+
set activityItems(value) {
|
|
685
|
+
this.ActivityItems = value;
|
|
686
|
+
}
|
|
687
|
+
ActivityRunningCount = 0;
|
|
688
|
+
/** @deprecated Use {@link ActivityRunningCount}. */
|
|
689
|
+
get activityRunningCount() {
|
|
690
|
+
return this.ActivityRunningCount;
|
|
691
|
+
}
|
|
692
|
+
/** @deprecated Use {@link ActivityRunningCount}. */
|
|
693
|
+
set activityRunningCount(value) {
|
|
694
|
+
this.ActivityRunningCount = value;
|
|
695
|
+
}
|
|
696
|
+
ActivityOpen = false;
|
|
697
|
+
/** @deprecated Use {@link ActivityOpen}. */
|
|
698
|
+
get activityOpen() {
|
|
699
|
+
return this.ActivityOpen;
|
|
700
|
+
}
|
|
701
|
+
/** @deprecated Use {@link ActivityOpen}. */
|
|
702
|
+
set activityOpen(value) {
|
|
703
|
+
this.ActivityOpen = value;
|
|
704
|
+
}
|
|
705
|
+
IsViewingSystemTab = false;
|
|
706
|
+
/** @deprecated Use {@link IsViewingSystemTab}. */
|
|
707
|
+
get isViewingSystemTab() {
|
|
708
|
+
return this.IsViewingSystemTab;
|
|
709
|
+
}
|
|
710
|
+
/** @deprecated Use {@link IsViewingSystemTab}. */
|
|
711
|
+
set isViewingSystemTab(value) {
|
|
712
|
+
this.IsViewingSystemTab = value;
|
|
713
|
+
} // True when viewing a resource tab (not associated with a registered app)
|
|
714
|
+
LoadingAppId = null;
|
|
715
|
+
/** @deprecated Use {@link LoadingAppId}. */
|
|
716
|
+
get loadingAppId() {
|
|
717
|
+
return this.LoadingAppId;
|
|
718
|
+
}
|
|
719
|
+
/** @deprecated Use {@link LoadingAppId}. */
|
|
720
|
+
set loadingAppId(value) {
|
|
721
|
+
this.LoadingAppId = value;
|
|
722
|
+
} // ID of app currently being loaded (for app switcher loading indicator)
|
|
626
723
|
// Loading animation state
|
|
627
724
|
loadingMessageInterval = null;
|
|
628
725
|
loadingMessageIndex = 0;
|
|
@@ -644,32 +741,152 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
644
741
|
MJVersion = PACKAGE_VERSION;
|
|
645
742
|
loadingResetTimeout = null;
|
|
646
743
|
loadingResetDelayMs = 20_000; // 20 seconds before showing reset option
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
744
|
+
CurrentLoadingText;
|
|
745
|
+
/** @deprecated Use {@link CurrentLoadingText}. */
|
|
746
|
+
get currentLoadingText() {
|
|
747
|
+
return this.CurrentLoadingText;
|
|
748
|
+
}
|
|
749
|
+
/** @deprecated Use {@link CurrentLoadingText}. */
|
|
750
|
+
set currentLoadingText(value) {
|
|
751
|
+
this.CurrentLoadingText = value;
|
|
752
|
+
}
|
|
753
|
+
CurrentLoadingColor;
|
|
754
|
+
/** @deprecated Use {@link CurrentLoadingColor}. */
|
|
755
|
+
get currentLoadingColor() {
|
|
756
|
+
return this.CurrentLoadingColor;
|
|
757
|
+
}
|
|
758
|
+
/** @deprecated Use {@link CurrentLoadingColor}. */
|
|
759
|
+
set currentLoadingColor(value) {
|
|
760
|
+
this.CurrentLoadingColor = value;
|
|
761
|
+
}
|
|
762
|
+
CurrentLoadingTextColor;
|
|
763
|
+
/** @deprecated Use {@link CurrentLoadingTextColor}. */
|
|
764
|
+
get currentLoadingTextColor() {
|
|
765
|
+
return this.CurrentLoadingTextColor;
|
|
766
|
+
}
|
|
767
|
+
/** @deprecated Use {@link CurrentLoadingTextColor}. */
|
|
768
|
+
set currentLoadingTextColor(value) {
|
|
769
|
+
this.CurrentLoadingTextColor = value;
|
|
770
|
+
}
|
|
771
|
+
CurrentLoadingGradient;
|
|
772
|
+
/** @deprecated Use {@link CurrentLoadingGradient}. */
|
|
773
|
+
get currentLoadingGradient() {
|
|
774
|
+
return this.CurrentLoadingGradient;
|
|
775
|
+
}
|
|
776
|
+
/** @deprecated Use {@link CurrentLoadingGradient}. */
|
|
777
|
+
set currentLoadingGradient(value) {
|
|
778
|
+
this.CurrentLoadingGradient = value;
|
|
779
|
+
}
|
|
780
|
+
CurrentLoadingAnimation = 'pulse';
|
|
781
|
+
/** @deprecated Use {@link CurrentLoadingAnimation}. */
|
|
782
|
+
get currentLoadingAnimation() {
|
|
783
|
+
return this.CurrentLoadingAnimation;
|
|
784
|
+
}
|
|
785
|
+
/** @deprecated Use {@link CurrentLoadingAnimation}. */
|
|
786
|
+
set currentLoadingAnimation(value) {
|
|
787
|
+
this.CurrentLoadingAnimation = value;
|
|
788
|
+
}
|
|
652
789
|
// User avatar state
|
|
653
|
-
|
|
654
|
-
|
|
790
|
+
UserImageURL = '';
|
|
791
|
+
/** @deprecated Use {@link UserImageURL}. */
|
|
792
|
+
get userImageURL() {
|
|
793
|
+
return this.UserImageURL;
|
|
794
|
+
}
|
|
795
|
+
/** @deprecated Use {@link UserImageURL}. */
|
|
796
|
+
set userImageURL(value) {
|
|
797
|
+
this.UserImageURL = value;
|
|
798
|
+
}
|
|
799
|
+
UserIconClass = null;
|
|
800
|
+
/** @deprecated Use {@link UserIconClass}. */
|
|
801
|
+
get userIconClass() {
|
|
802
|
+
return this.UserIconClass;
|
|
803
|
+
}
|
|
804
|
+
/** @deprecated Use {@link UserIconClass}. */
|
|
805
|
+
set userIconClass(value) {
|
|
806
|
+
this.UserIconClass = value;
|
|
807
|
+
}
|
|
655
808
|
userName = '';
|
|
656
|
-
|
|
809
|
+
UserEmail = '';
|
|
810
|
+
/** @deprecated Use {@link UserEmail}. */
|
|
811
|
+
get userEmail() {
|
|
812
|
+
return this.UserEmail;
|
|
813
|
+
}
|
|
814
|
+
/** @deprecated Use {@link UserEmail}. */
|
|
815
|
+
set userEmail(value) {
|
|
816
|
+
this.UserEmail = value;
|
|
817
|
+
}
|
|
657
818
|
userEntity = null;
|
|
658
819
|
// User menu plugin system
|
|
659
820
|
userMenu = null;
|
|
660
|
-
|
|
821
|
+
UserMenuElements = [];
|
|
822
|
+
/** @deprecated Use {@link UserMenuElements}. */
|
|
823
|
+
get userMenuElements() {
|
|
824
|
+
return this.UserMenuElements;
|
|
825
|
+
}
|
|
826
|
+
/** @deprecated Use {@link UserMenuElements}. */
|
|
827
|
+
set userMenuElements(value) {
|
|
828
|
+
this.UserMenuElements = value;
|
|
829
|
+
}
|
|
661
830
|
destroy$ = new Subject();
|
|
662
831
|
// Pin progress overlay
|
|
663
832
|
PinProgressVisible = false;
|
|
664
833
|
PinProgressText = '';
|
|
665
834
|
// Search state
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
835
|
+
IsSearchOpen = false;
|
|
836
|
+
/** @deprecated Use {@link IsSearchOpen}. */
|
|
837
|
+
get isSearchOpen() {
|
|
838
|
+
return this.IsSearchOpen;
|
|
839
|
+
}
|
|
840
|
+
/** @deprecated Use {@link IsSearchOpen}. */
|
|
841
|
+
set isSearchOpen(value) {
|
|
842
|
+
this.IsSearchOpen = value;
|
|
843
|
+
}
|
|
844
|
+
SearchableEntities = [];
|
|
845
|
+
/** @deprecated Use {@link SearchableEntities}. */
|
|
846
|
+
get searchableEntities() {
|
|
847
|
+
return this.SearchableEntities;
|
|
848
|
+
}
|
|
849
|
+
/** @deprecated Use {@link SearchableEntities}. */
|
|
850
|
+
set searchableEntities(value) {
|
|
851
|
+
this.SearchableEntities = value;
|
|
852
|
+
}
|
|
853
|
+
SelectedEntity = null;
|
|
854
|
+
/** @deprecated Use {@link SelectedEntity}. */
|
|
855
|
+
get selectedEntity() {
|
|
856
|
+
return this.SelectedEntity;
|
|
857
|
+
}
|
|
858
|
+
/** @deprecated Use {@link SelectedEntity}. */
|
|
859
|
+
set selectedEntity(value) {
|
|
860
|
+
this.SelectedEntity = value;
|
|
861
|
+
}
|
|
862
|
+
SearchInput;
|
|
863
|
+
/** @deprecated Use {@link SearchInput}. */
|
|
864
|
+
get searchInput() {
|
|
865
|
+
return this.SearchInput;
|
|
866
|
+
}
|
|
867
|
+
/** @deprecated Use {@link SearchInput}. */
|
|
868
|
+
set searchInput(value) {
|
|
869
|
+
this.SearchInput = value;
|
|
870
|
+
}
|
|
670
871
|
/** Mobile drawer + its toggle — used to return focus to the toggle when the drawer closes. */
|
|
671
|
-
|
|
672
|
-
|
|
872
|
+
MobileNavDrawer;
|
|
873
|
+
/** @deprecated Use {@link MobileNavDrawer}. */
|
|
874
|
+
get mobileNavDrawer() {
|
|
875
|
+
return this.MobileNavDrawer;
|
|
876
|
+
}
|
|
877
|
+
/** @deprecated Use {@link MobileNavDrawer}. */
|
|
878
|
+
set mobileNavDrawer(value) {
|
|
879
|
+
this.MobileNavDrawer = value;
|
|
880
|
+
}
|
|
881
|
+
MobileNavToggle;
|
|
882
|
+
/** @deprecated Use {@link MobileNavToggle}. */
|
|
883
|
+
get mobileNavToggle() {
|
|
884
|
+
return this.MobileNavToggle;
|
|
885
|
+
}
|
|
886
|
+
/** @deprecated Use {@link MobileNavToggle}. */
|
|
887
|
+
set mobileNavToggle(value) {
|
|
888
|
+
this.MobileNavToggle = value;
|
|
889
|
+
}
|
|
673
890
|
// Legacy universal search overlay (omnibar-off MOBILE path) — opened by the
|
|
674
891
|
// mobile search icon or Ctrl/Cmd+K when the inline composite isn't visible.
|
|
675
892
|
// Desktop omnibar-off uses the inline header composite instead.
|
|
@@ -765,7 +982,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
765
982
|
// is the same guarantee read off state we already have, instead of a new
|
|
766
983
|
// dirty-tracking pipeline.)
|
|
767
984
|
this.workspaceManager.RecordsRegionTabFilter = this.resolvedRecordOpenStyle === 'records'
|
|
768
|
-
? (tab) => IsRecordsRegionTab(tab.configuration) && !this.
|
|
985
|
+
? (tab) => IsRecordsRegionTab(tab.configuration) && !this.TabContainerRef?.IsRecordTabEditing(tab.id)
|
|
769
986
|
: null;
|
|
770
987
|
}
|
|
771
988
|
get ShowSearchBar() {
|
|
@@ -851,25 +1068,57 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
851
1068
|
/** Palette footer gear → My Profile (where the Command Palette section lives). */
|
|
852
1069
|
OnPaletteSettingsRequested() {
|
|
853
1070
|
this.profileDialogService.open(this.viewContainerRef, {
|
|
854
|
-
avatarUrl: this.
|
|
855
|
-
avatarIconClass: this.
|
|
1071
|
+
avatarUrl: this.UserImageURL || null,
|
|
1072
|
+
avatarIconClass: this.UserIconClass || null
|
|
856
1073
|
});
|
|
857
1074
|
}
|
|
858
|
-
|
|
1075
|
+
OmnibarPalette;
|
|
1076
|
+
/** @deprecated Use {@link OmnibarPalette}. */
|
|
1077
|
+
get omnibarPalette() {
|
|
1078
|
+
return this.OmnibarPalette;
|
|
1079
|
+
}
|
|
1080
|
+
/** @deprecated Use {@link OmnibarPalette}. */
|
|
1081
|
+
set omnibarPalette(value) {
|
|
1082
|
+
this.OmnibarPalette = value;
|
|
1083
|
+
}
|
|
859
1084
|
/** Legacy inline search composite (omnibar-off desktop). Structural typing keeps
|
|
860
1085
|
the shell decoupled from the ng-search component class. */
|
|
861
|
-
|
|
1086
|
+
ShellSearchComposite;
|
|
1087
|
+
/** @deprecated Use {@link ShellSearchComposite}. */
|
|
1088
|
+
get shellSearchComposite() {
|
|
1089
|
+
return this.ShellSearchComposite;
|
|
1090
|
+
}
|
|
1091
|
+
/** @deprecated Use {@link ShellSearchComposite}. */
|
|
1092
|
+
set shellSearchComposite(value) {
|
|
1093
|
+
this.ShellSearchComposite = value;
|
|
1094
|
+
}
|
|
862
1095
|
/** Header affordance click → open the palette. */
|
|
863
1096
|
OpenOmnibar(initialQuery = '') {
|
|
864
1097
|
if (!this.ShowSearchBar) {
|
|
865
1098
|
return;
|
|
866
1099
|
}
|
|
867
|
-
this.
|
|
1100
|
+
this.OmnibarPalette?.Open(initialQuery);
|
|
868
1101
|
}
|
|
869
1102
|
// Tab container reference for thumbnail capture
|
|
870
|
-
|
|
1103
|
+
TabContainerRef;
|
|
1104
|
+
/** @deprecated Use {@link TabContainerRef}. */
|
|
1105
|
+
get tabContainerRef() {
|
|
1106
|
+
return this.TabContainerRef;
|
|
1107
|
+
}
|
|
1108
|
+
/** @deprecated Use {@link TabContainerRef}. */
|
|
1109
|
+
set tabContainerRef(value) {
|
|
1110
|
+
this.TabContainerRef = value;
|
|
1111
|
+
}
|
|
871
1112
|
// App access dialog
|
|
872
|
-
|
|
1113
|
+
AppAccessDialog;
|
|
1114
|
+
/** @deprecated Use {@link AppAccessDialog}. */
|
|
1115
|
+
get appAccessDialog() {
|
|
1116
|
+
return this.AppAccessDialog;
|
|
1117
|
+
}
|
|
1118
|
+
/** @deprecated Use {@link AppAccessDialog}. */
|
|
1119
|
+
set appAccessDialog(value) {
|
|
1120
|
+
this.AppAccessDialog = value;
|
|
1121
|
+
}
|
|
873
1122
|
pendingAppPath = null; // Store the app path we tried to access
|
|
874
1123
|
/**
|
|
875
1124
|
* Nav Bar apps positioned to the left of the app switcher.
|
|
@@ -878,26 +1127,44 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
878
1127
|
* this is bound directly in the shell nav template's @for/@if. Filters out apps
|
|
879
1128
|
* that have HideNavBarIconWhenActive=true and are currently active.
|
|
880
1129
|
*/
|
|
881
|
-
|
|
1130
|
+
LeftOfSwitcherApps = [];
|
|
1131
|
+
/** @deprecated Use {@link LeftOfSwitcherApps}. */
|
|
1132
|
+
get leftOfSwitcherApps() {
|
|
1133
|
+
return this.LeftOfSwitcherApps;
|
|
1134
|
+
}
|
|
1135
|
+
/** @deprecated Use {@link LeftOfSwitcherApps}. */
|
|
1136
|
+
set leftOfSwitcherApps(value) {
|
|
1137
|
+
this.LeftOfSwitcherApps = value;
|
|
1138
|
+
}
|
|
882
1139
|
/**
|
|
883
1140
|
* Nav Bar apps positioned to the left of the user menu.
|
|
884
1141
|
* Backing field recomputed only when the active app or nav-app list changes
|
|
885
1142
|
* (see {@link recomputeNavBarApps}). Filters out apps that have
|
|
886
1143
|
* HideNavBarIconWhenActive=true and are currently active.
|
|
887
1144
|
*/
|
|
888
|
-
|
|
1145
|
+
LeftOfUserMenuApps = [];
|
|
1146
|
+
/** @deprecated Use {@link LeftOfUserMenuApps}. */
|
|
1147
|
+
get leftOfUserMenuApps() {
|
|
1148
|
+
return this.LeftOfUserMenuApps;
|
|
1149
|
+
}
|
|
1150
|
+
/** @deprecated Use {@link LeftOfUserMenuApps}. */
|
|
1151
|
+
set leftOfUserMenuApps(value) {
|
|
1152
|
+
this.LeftOfUserMenuApps = value;
|
|
1153
|
+
}
|
|
889
1154
|
/**
|
|
890
1155
|
* Recompute the precomputed nav-bar app arrays. Called only when the active app
|
|
891
1156
|
* changes or the underlying nav-app list (re)loads — keeping the filtered arrays
|
|
892
1157
|
* (and their per-app UUIDsEqual checks) out of the per-CD-cycle hot path.
|
|
893
1158
|
*/
|
|
894
1159
|
recomputeNavBarApps() {
|
|
895
|
-
this.
|
|
896
|
-
.filter(app => !(app.HideNavBarIconWhenActive && UUIDsEqual(app.ID, this.
|
|
897
|
-
this.
|
|
898
|
-
.filter(app => !(app.HideNavBarIconWhenActive && UUIDsEqual(app.ID, this.
|
|
1160
|
+
this.LeftOfSwitcherApps = this.appManager.GetNavBarApps('Left of App Switcher')
|
|
1161
|
+
.filter(app => !(app.HideNavBarIconWhenActive && UUIDsEqual(app.ID, this.ActiveApp?.ID)));
|
|
1162
|
+
this.LeftOfUserMenuApps = this.appManager.GetNavBarApps('Left of User Menu')
|
|
1163
|
+
.filter(app => !(app.HideNavBarIconWhenActive && UUIDsEqual(app.ID, this.ActiveApp?.ID)));
|
|
899
1164
|
}
|
|
900
|
-
|
|
1165
|
+
/** Warms the React runtime (CDN scripts) after login; see InitializeShell(). Field-injected so the constructor signature is unchanged. */
|
|
1166
|
+
reactAdapter = inject(AngularAdapterService);
|
|
1167
|
+
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) {
|
|
901
1168
|
super();
|
|
902
1169
|
this.appManager = appManager;
|
|
903
1170
|
this.workspaceManager = workspaceManager;
|
|
@@ -914,7 +1181,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
914
1181
|
this.profileDialogService = profileDialogService;
|
|
915
1182
|
this.viewContainerRef = viewContainerRef;
|
|
916
1183
|
this.titleService = titleService;
|
|
917
|
-
this.
|
|
1184
|
+
this.DeveloperModeService = DeveloperModeService;
|
|
918
1185
|
this.startupValidationService = startupValidationService;
|
|
919
1186
|
this.commandPaletteService = commandPaletteService;
|
|
920
1187
|
this.themeService = themeService;
|
|
@@ -930,7 +1197,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
930
1197
|
const providerForServices = this.ProviderToUse;
|
|
931
1198
|
this.appManager.Provider = providerForServices;
|
|
932
1199
|
this.workspaceManager.Provider = providerForServices;
|
|
933
|
-
this.
|
|
1200
|
+
this.DeveloperModeService.Provider = providerForServices;
|
|
934
1201
|
this.startupValidationService.Provider = providerForServices;
|
|
935
1202
|
if (SharedService.Instance)
|
|
936
1203
|
SharedService.Instance.Provider = providerForServices;
|
|
@@ -939,49 +1206,57 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
939
1206
|
// Initialize animation based on theme configuration
|
|
940
1207
|
this.initializeAnimationFromTheme();
|
|
941
1208
|
// Set first message
|
|
942
|
-
this.
|
|
943
|
-
if (this.activeTheme.
|
|
1209
|
+
this.CurrentLoadingText = this.activeTheme.Messages[0];
|
|
1210
|
+
if (this.activeTheme.StaticColors) {
|
|
944
1211
|
// Standard theme: keep MJ blue, no gradient
|
|
945
|
-
this.
|
|
946
|
-
this.
|
|
947
|
-
this.
|
|
1212
|
+
this.CurrentLoadingColor = this.activeTheme.Colors[0];
|
|
1213
|
+
this.CurrentLoadingTextColor = '#757575'; // Default gray text
|
|
1214
|
+
this.CurrentLoadingGradient = null;
|
|
948
1215
|
}
|
|
949
1216
|
else {
|
|
950
1217
|
// Themed period: use theme colors and first gradient from the start
|
|
951
|
-
this.
|
|
952
|
-
this.
|
|
1218
|
+
this.CurrentLoadingColor = this.activeTheme.Colors[0];
|
|
1219
|
+
this.CurrentLoadingTextColor = this.activeTheme.Colors[0];
|
|
953
1220
|
// Set initial gradient if theme has gradients
|
|
954
|
-
if (this.activeTheme.
|
|
955
|
-
this.
|
|
1221
|
+
if (this.activeTheme.Gradients && this.activeTheme.Gradients.length > 0) {
|
|
1222
|
+
this.CurrentLoadingGradient = this.activeTheme.Gradients[0];
|
|
956
1223
|
}
|
|
957
1224
|
else {
|
|
958
|
-
this.
|
|
1225
|
+
this.CurrentLoadingGradient = null;
|
|
959
1226
|
}
|
|
960
1227
|
}
|
|
961
1228
|
}
|
|
1229
|
+
/** @deprecated Use {@link DeveloperModeService}. */
|
|
1230
|
+
get developerModeService() {
|
|
1231
|
+
return this.DeveloperModeService;
|
|
1232
|
+
}
|
|
1233
|
+
/** @deprecated Use {@link DeveloperModeService}. */
|
|
1234
|
+
set developerModeService(value) {
|
|
1235
|
+
this.DeveloperModeService = value;
|
|
1236
|
+
}
|
|
962
1237
|
async ngOnInit() {
|
|
963
1238
|
try {
|
|
964
1239
|
MJGlobal.Instance.GetEventListener(true).subscribe(async (loginEvent) => {
|
|
965
1240
|
if (loginEvent.event === MJEventType.LoggedIn) {
|
|
966
1241
|
if (this.authBase.initialPath === "/") {
|
|
967
1242
|
// Base route - no need to wait for NavigationEnd
|
|
968
|
-
await this.
|
|
1243
|
+
await this.InitializeShell();
|
|
969
1244
|
}
|
|
970
1245
|
else {
|
|
971
1246
|
// Deep link route - wait for NavigationEnd to ensure router URL is correct
|
|
972
1247
|
this.router.events.pipe(filter((event) => event instanceof NavigationEnd), filter(() => !this.initialNavigationComplete)).subscribe(async () => {
|
|
973
1248
|
this.initialNavigationComplete = true;
|
|
974
|
-
await this.
|
|
1249
|
+
await this.InitializeShell();
|
|
975
1250
|
});
|
|
976
1251
|
}
|
|
977
1252
|
}
|
|
978
1253
|
});
|
|
979
1254
|
}
|
|
980
1255
|
catch (error) {
|
|
981
|
-
this.
|
|
1256
|
+
this.Loading = false;
|
|
982
1257
|
}
|
|
983
1258
|
}
|
|
984
|
-
async
|
|
1259
|
+
async InitializeShell() {
|
|
985
1260
|
// Start the loading animation with cycling messages
|
|
986
1261
|
this.startLoadingAnimation();
|
|
987
1262
|
// Initialize application manager (subscribes to LoggedIn event)
|
|
@@ -1013,26 +1288,26 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1013
1288
|
await this.workspaceManager.Initialize(user.ID);
|
|
1014
1289
|
// Subscribe to tab bar visibility changes
|
|
1015
1290
|
this.subscriptions.push(this.workspaceManager.TabBarVisible.subscribe(visible => {
|
|
1016
|
-
this.
|
|
1291
|
+
this.TabBarVisible = visible;
|
|
1017
1292
|
}));
|
|
1018
1293
|
// The host's chrome policy says its answer may have changed (org or plan switch)
|
|
1019
1294
|
this.subscriptions.push(this.chromePolicy.Changed.subscribe(() => this.onChromePolicyChanged()));
|
|
1020
1295
|
// Subscribe to the global Activity tracker (Run Pipeline, Sync, Cluster, …)
|
|
1021
1296
|
this.subscriptions.push(this.activityService.Activities$.subscribe(items => {
|
|
1022
|
-
this.
|
|
1023
|
-
this.
|
|
1297
|
+
this.ActivityItems = items;
|
|
1298
|
+
this.ActivityRunningCount = items.filter(i => i.Status === 'running').length;
|
|
1024
1299
|
if (items.length === 0)
|
|
1025
|
-
this.
|
|
1300
|
+
this.ActivityOpen = false;
|
|
1026
1301
|
this.cdr.detectChanges();
|
|
1027
1302
|
}));
|
|
1028
1303
|
// Subscribe to unread notification count changes
|
|
1029
1304
|
this.subscriptions.push(MJNotificationService.UnreadCount$.subscribe(count => {
|
|
1030
|
-
this.
|
|
1305
|
+
this.UnreadNotificationCount = count;
|
|
1031
1306
|
this.cdr.detectChanges();
|
|
1032
1307
|
}));
|
|
1033
1308
|
// Subscribe to active app changes
|
|
1034
1309
|
this.subscriptions.push(this.appManager.ActiveApp.subscribe(async (app) => {
|
|
1035
|
-
this.
|
|
1310
|
+
this.ActiveApp = app;
|
|
1036
1311
|
this.recomputeNavBarApps();
|
|
1037
1312
|
this.cdr.detectChanges();
|
|
1038
1313
|
// Create default tab when app is activated ONLY if:
|
|
@@ -1149,7 +1424,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1149
1424
|
}
|
|
1150
1425
|
// Subscribe to workspace configuration changes to sync URL and active app
|
|
1151
1426
|
this.subscriptions.push(this.workspaceManager.Configuration.subscribe(async (config) => {
|
|
1152
|
-
if (config && this.
|
|
1427
|
+
if (config && this.Initialized) {
|
|
1153
1428
|
this.latestSyncedConfig = config;
|
|
1154
1429
|
// Sync active app with active tab's application
|
|
1155
1430
|
await this.syncActiveAppWithTab(config);
|
|
@@ -1171,7 +1446,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1171
1446
|
}));
|
|
1172
1447
|
// Subscribe to router navigation events (for browser back/forward)
|
|
1173
1448
|
this.subscriptions.push(this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(event => {
|
|
1174
|
-
if (this.
|
|
1449
|
+
if (this.Initialized) {
|
|
1175
1450
|
this.syncWorkspaceWithUrl(event.urlAfterRedirects || event.url);
|
|
1176
1451
|
}
|
|
1177
1452
|
}));
|
|
@@ -1197,16 +1472,16 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1197
1472
|
this.subscriptions.push(MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
|
|
1198
1473
|
if (event.event === MJEventType.TenantChanged) {
|
|
1199
1474
|
if (event.eventCode === 'TenantChanging') {
|
|
1200
|
-
this.
|
|
1201
|
-
this.
|
|
1475
|
+
this.CurrentLoadingText = 'Switching organization...';
|
|
1476
|
+
this.Loading = true;
|
|
1202
1477
|
this.cdr.detectChanges();
|
|
1203
1478
|
}
|
|
1204
|
-
else if (event.eventCode === 'TenantChanged' && this.
|
|
1479
|
+
else if (event.eventCode === 'TenantChanged' && this.TabContainerRef) {
|
|
1205
1480
|
try {
|
|
1206
|
-
await this.
|
|
1481
|
+
await this.TabContainerRef.ReloadAllTabs();
|
|
1207
1482
|
}
|
|
1208
1483
|
finally {
|
|
1209
|
-
this.
|
|
1484
|
+
this.Loading = false;
|
|
1210
1485
|
this.cdr.detectChanges();
|
|
1211
1486
|
}
|
|
1212
1487
|
}
|
|
@@ -1214,7 +1489,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1214
1489
|
}));
|
|
1215
1490
|
// Load searchable entities for search functionality
|
|
1216
1491
|
await this.loadSearchableEntities();
|
|
1217
|
-
this.
|
|
1492
|
+
this.Initialized = true;
|
|
1218
1493
|
this.waitingForFirstResource = true;
|
|
1219
1494
|
// Decide whether to restore workspace state or honor the current URL.
|
|
1220
1495
|
// If the user navigated to a specific path (deep link, bookmark, typed URL),
|
|
@@ -1238,6 +1513,14 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1238
1513
|
// state changes (apps loaded, searchableEntities populated, etc.) to prevent
|
|
1239
1514
|
// NG0100 ExpressionChangedAfterItHasBeenCheckedError in dev mode
|
|
1240
1515
|
this.cdr.detectChanges();
|
|
1516
|
+
// Explorer is a host where users routinely open interactive component artifacts, so warm the
|
|
1517
|
+
// React runtime in the background now that the user is signed in. ArtifactsModule no longer
|
|
1518
|
+
// does this on construction (#4802). Non-blocking; Preload() logs and retries on demand.
|
|
1519
|
+
this.reactAdapter.Preload();
|
|
1520
|
+
}
|
|
1521
|
+
/** @deprecated Use {@link InitializeShell}. */
|
|
1522
|
+
async initializeShell() {
|
|
1523
|
+
return this.InitializeShell();
|
|
1241
1524
|
}
|
|
1242
1525
|
/**
|
|
1243
1526
|
* Handle deep links from URL
|
|
@@ -1274,7 +1557,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1274
1557
|
// 1. We're initialized (past the startup phase)
|
|
1275
1558
|
// 2. App is different from current
|
|
1276
1559
|
// 3. Either NOT in URL-based navigation mode, OR this IS a URL-based tab
|
|
1277
|
-
const shouldSetActiveApp = this.
|
|
1560
|
+
const shouldSetActiveApp = this.Initialized &&
|
|
1278
1561
|
app &&
|
|
1279
1562
|
!UUIDsEqual(currentActiveApp?.ID, request.ApplicationId) &&
|
|
1280
1563
|
(!this.urlBasedNavigation || isUrlBasedTab);
|
|
@@ -1315,7 +1598,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1315
1598
|
}
|
|
1316
1599
|
// Check if this is a system tab (not associated with a registered app)
|
|
1317
1600
|
if (tabAppId === SYSTEM_APP_ID) {
|
|
1318
|
-
this.
|
|
1601
|
+
this.IsViewingSystemTab = true;
|
|
1319
1602
|
this.cdr.detectChanges();
|
|
1320
1603
|
// Don't try to set active app - SYSTEM_APP_ID has no registered app
|
|
1321
1604
|
// Update browser title with just the tab title (no app context)
|
|
@@ -1323,7 +1606,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1323
1606
|
return;
|
|
1324
1607
|
}
|
|
1325
1608
|
// Not a system tab - clear the flag
|
|
1326
|
-
this.
|
|
1609
|
+
this.IsViewingSystemTab = false;
|
|
1327
1610
|
this.cdr.detectChanges();
|
|
1328
1611
|
// Records style: records-REGION tabs are a GLOBAL
|
|
1329
1612
|
// surface — viewing one must never flip the user's app context. Without
|
|
@@ -1333,7 +1616,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1333
1616
|
// stays wherever the user is. Records DOCKED to the workspace fall
|
|
1334
1617
|
// through: they are ordinary main-layout tabs and DO flip app context.
|
|
1335
1618
|
if (this.RecordTabsStyle && IsRecordsRegionTab(activeTab.configuration)) {
|
|
1336
|
-
this.titleService.setContext(this.
|
|
1619
|
+
this.titleService.setContext(this.ActiveApp?.Name || null, activeTab.title || null);
|
|
1337
1620
|
return;
|
|
1338
1621
|
}
|
|
1339
1622
|
// Check if active app needs to be updated
|
|
@@ -2055,15 +2338,23 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2055
2338
|
* user stays within their scoped app. Data access is still enforced
|
|
2056
2339
|
* server-side by the user's role; this is the UI-confinement layer.
|
|
2057
2340
|
*/
|
|
2058
|
-
get
|
|
2341
|
+
get AppSwitchingLocked() {
|
|
2059
2342
|
return !!this.authBase.GetSessionScope()?.restrictedToApplicationId;
|
|
2060
2343
|
}
|
|
2061
|
-
|
|
2344
|
+
/** @deprecated Use {@link AppSwitchingLocked}. */
|
|
2345
|
+
get appSwitchingLocked() {
|
|
2346
|
+
return this.AppSwitchingLocked;
|
|
2347
|
+
}
|
|
2348
|
+
OnFirstResourceLoadComplete() {
|
|
2062
2349
|
this.waitingForFirstResource = false;
|
|
2063
|
-
this.
|
|
2350
|
+
this.Loading = false;
|
|
2064
2351
|
this.stopLoadingAnimation();
|
|
2065
2352
|
this.cdr.detectChanges();
|
|
2066
2353
|
}
|
|
2354
|
+
/** @deprecated Use {@link OnFirstResourceLoadComplete}. */
|
|
2355
|
+
onFirstResourceLoadComplete() {
|
|
2356
|
+
return this.OnFirstResourceLoadComplete();
|
|
2357
|
+
}
|
|
2067
2358
|
/**
|
|
2068
2359
|
* Start the loading message cycling animation.
|
|
2069
2360
|
* Messages cycle every 2.5 seconds without repeating until all are used.
|
|
@@ -2120,7 +2411,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2120
2411
|
startLoadingResetTimer() {
|
|
2121
2412
|
this.cancelLoadingResetTimer();
|
|
2122
2413
|
this.loadingResetTimeout = setTimeout(() => {
|
|
2123
|
-
if (this.
|
|
2414
|
+
if (this.Loading) {
|
|
2124
2415
|
this.ShowResetOption = true;
|
|
2125
2416
|
this.cdr.detectChanges();
|
|
2126
2417
|
}
|
|
@@ -2186,24 +2477,24 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2186
2477
|
*/
|
|
2187
2478
|
initializeLoadingDisplay() {
|
|
2188
2479
|
// Set first message
|
|
2189
|
-
this.
|
|
2190
|
-
if (this.activeTheme.
|
|
2480
|
+
this.CurrentLoadingText = this.activeTheme.Messages[0];
|
|
2481
|
+
if (this.activeTheme.StaticColors) {
|
|
2191
2482
|
// Standard theme: keep MJ blue, no gradient
|
|
2192
|
-
this.
|
|
2193
|
-
this.
|
|
2194
|
-
this.
|
|
2483
|
+
this.CurrentLoadingColor = this.activeTheme.Colors[0];
|
|
2484
|
+
this.CurrentLoadingTextColor = '#757575'; // Default gray text
|
|
2485
|
+
this.CurrentLoadingGradient = null;
|
|
2195
2486
|
}
|
|
2196
2487
|
else {
|
|
2197
2488
|
// Themed period: use theme colors and first gradient from the start
|
|
2198
|
-
this.
|
|
2199
|
-
this.
|
|
2489
|
+
this.CurrentLoadingColor = this.activeTheme.Colors[0];
|
|
2490
|
+
this.CurrentLoadingTextColor = this.activeTheme.Colors[0];
|
|
2200
2491
|
// Set initial gradient if theme has gradients
|
|
2201
|
-
if (this.activeTheme.
|
|
2202
|
-
this.
|
|
2492
|
+
if (this.activeTheme.Gradients && this.activeTheme.Gradients.length > 0) {
|
|
2493
|
+
this.CurrentLoadingGradient = this.activeTheme.Gradients[0];
|
|
2203
2494
|
this.usedGradientIndices = [0];
|
|
2204
2495
|
}
|
|
2205
2496
|
else {
|
|
2206
|
-
this.
|
|
2497
|
+
this.CurrentLoadingGradient = null;
|
|
2207
2498
|
}
|
|
2208
2499
|
}
|
|
2209
2500
|
}
|
|
@@ -2213,7 +2504,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2213
2504
|
* For standard theme, colors remain static.
|
|
2214
2505
|
*/
|
|
2215
2506
|
cycleToNextMessage() {
|
|
2216
|
-
const messages = this.activeTheme.
|
|
2507
|
+
const messages = this.activeTheme.Messages;
|
|
2217
2508
|
this.messageCycleCount++;
|
|
2218
2509
|
// If we've used all messages, reset the used list (but exclude current to avoid immediate repeat)
|
|
2219
2510
|
if (this.usedMessageIndices.length >= messages.length) {
|
|
@@ -2228,10 +2519,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2228
2519
|
this.usedMessageIndices.push(randomIndex);
|
|
2229
2520
|
this.loadingMessageIndex = randomIndex;
|
|
2230
2521
|
// Update the message
|
|
2231
|
-
this.
|
|
2522
|
+
this.CurrentLoadingText = this.activeTheme.Messages[randomIndex];
|
|
2232
2523
|
// Check if it's time to change colors (every 2nd message = every 5 seconds)
|
|
2233
2524
|
// But only for non-static themes
|
|
2234
|
-
if (!this.activeTheme.
|
|
2525
|
+
if (!this.activeTheme.StaticColors && this.messageCycleCount % this.colorChangeEveryNMessages === 0) {
|
|
2235
2526
|
this.cycleToNextColor();
|
|
2236
2527
|
}
|
|
2237
2528
|
}
|
|
@@ -2242,8 +2533,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2242
2533
|
*/
|
|
2243
2534
|
cycleToNextColor() {
|
|
2244
2535
|
// Cycle to next gradient if available
|
|
2245
|
-
if (this.activeTheme.
|
|
2246
|
-
const gradients = this.activeTheme.
|
|
2536
|
+
if (this.activeTheme.Gradients && this.activeTheme.Gradients.length > 0) {
|
|
2537
|
+
const gradients = this.activeTheme.Gradients;
|
|
2247
2538
|
// If we've used all gradients, reset (but exclude current to avoid immediate repeat)
|
|
2248
2539
|
if (this.usedGradientIndices.length >= gradients.length) {
|
|
2249
2540
|
const lastUsed = this.usedGradientIndices[this.usedGradientIndices.length - 1];
|
|
@@ -2253,15 +2544,15 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2253
2544
|
const currentIndex = this.usedGradientIndices[this.usedGradientIndices.length - 1] ?? -1;
|
|
2254
2545
|
const nextIndex = (currentIndex + 1) % gradients.length;
|
|
2255
2546
|
this.usedGradientIndices.push(nextIndex);
|
|
2256
|
-
this.
|
|
2547
|
+
this.CurrentLoadingGradient = gradients[nextIndex];
|
|
2257
2548
|
}
|
|
2258
2549
|
// Also cycle text color through theme colors
|
|
2259
|
-
const colors = this.activeTheme.
|
|
2550
|
+
const colors = this.activeTheme.Colors;
|
|
2260
2551
|
if (colors.length > 1) {
|
|
2261
2552
|
// Get a random color from the theme for text
|
|
2262
2553
|
const randomIndex = Math.floor(Math.random() * colors.length);
|
|
2263
|
-
this.
|
|
2264
|
-
this.
|
|
2554
|
+
this.CurrentLoadingColor = colors[randomIndex];
|
|
2555
|
+
this.CurrentLoadingTextColor = colors[randomIndex];
|
|
2265
2556
|
}
|
|
2266
2557
|
}
|
|
2267
2558
|
/**
|
|
@@ -2270,22 +2561,22 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2270
2561
|
*/
|
|
2271
2562
|
initializeAnimationFromTheme() {
|
|
2272
2563
|
this.currentAnimationIndex = 0;
|
|
2273
|
-
const themeAnimations = this.activeTheme.
|
|
2564
|
+
const themeAnimations = this.activeTheme.Animations;
|
|
2274
2565
|
if (!themeAnimations) {
|
|
2275
2566
|
// No animation config - use random selection for non-standard themes
|
|
2276
2567
|
if (this.activeTheme.id === 'standard') {
|
|
2277
2568
|
// Standard theme defaults to pulse only
|
|
2278
|
-
this.animationSequence = [{
|
|
2569
|
+
this.animationSequence = [{ Type: 'pulse' }];
|
|
2279
2570
|
}
|
|
2280
2571
|
else {
|
|
2281
2572
|
// Random selection for themed holidays without explicit config
|
|
2282
2573
|
const randomType = this.allAnimationTypes[Math.floor(Math.random() * this.allAnimationTypes.length)];
|
|
2283
|
-
this.animationSequence = [{
|
|
2574
|
+
this.animationSequence = [{ Type: randomType }];
|
|
2284
2575
|
}
|
|
2285
2576
|
}
|
|
2286
2577
|
else if (typeof themeAnimations === 'string') {
|
|
2287
2578
|
// Single animation type specified
|
|
2288
|
-
this.animationSequence = [{
|
|
2579
|
+
this.animationSequence = [{ Type: themeAnimations }];
|
|
2289
2580
|
}
|
|
2290
2581
|
else {
|
|
2291
2582
|
// Array of animation steps
|
|
@@ -2293,7 +2584,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2293
2584
|
}
|
|
2294
2585
|
// Set initial animation
|
|
2295
2586
|
if (this.animationSequence.length > 0) {
|
|
2296
|
-
this.
|
|
2587
|
+
this.CurrentLoadingAnimation = this.animationSequence[0].Type;
|
|
2297
2588
|
}
|
|
2298
2589
|
}
|
|
2299
2590
|
/**
|
|
@@ -2317,7 +2608,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2317
2608
|
return;
|
|
2318
2609
|
}
|
|
2319
2610
|
const currentStep = this.animationSequence[this.currentAnimationIndex];
|
|
2320
|
-
const durationMs = currentStep.
|
|
2611
|
+
const durationMs = currentStep.DurationMs;
|
|
2321
2612
|
// If no duration specified (or 0), this step runs indefinitely
|
|
2322
2613
|
if (!durationMs || durationMs <= 0) {
|
|
2323
2614
|
return;
|
|
@@ -2337,7 +2628,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2337
2628
|
return;
|
|
2338
2629
|
}
|
|
2339
2630
|
const nextStep = this.animationSequence[this.currentAnimationIndex];
|
|
2340
|
-
this.
|
|
2631
|
+
this.CurrentLoadingAnimation = nextStep.Type;
|
|
2341
2632
|
this.cdr.detectChanges();
|
|
2342
2633
|
// Schedule the next transition if this step has a duration
|
|
2343
2634
|
this.scheduleNextAnimationStep();
|
|
@@ -2354,11 +2645,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2354
2645
|
/**
|
|
2355
2646
|
* Handle app switch from app switcher
|
|
2356
2647
|
*/
|
|
2357
|
-
async
|
|
2648
|
+
async OnAppSwitch(appId) {
|
|
2358
2649
|
// Clear the system tab flag since we're switching to a real app
|
|
2359
|
-
this.
|
|
2650
|
+
this.IsViewingSystemTab = false;
|
|
2360
2651
|
// Show loading indicator in app switcher
|
|
2361
|
-
this.
|
|
2652
|
+
this.LoadingAppId = appId;
|
|
2362
2653
|
this.cdr.detectChanges();
|
|
2363
2654
|
try {
|
|
2364
2655
|
const app = this.appManager.GetAppById(appId);
|
|
@@ -2367,11 +2658,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2367
2658
|
const systemApp = this.appManager.GetAllSystemApps().find(a => UUIDsEqual(a.ID, appId));
|
|
2368
2659
|
const appName = systemApp?.Name || 'this application';
|
|
2369
2660
|
// Clear loading indicator before showing dialog
|
|
2370
|
-
this.
|
|
2661
|
+
this.LoadingAppId = null;
|
|
2371
2662
|
this.cdr.detectChanges();
|
|
2372
2663
|
// Show "Add Application?" dialog
|
|
2373
|
-
if (this.
|
|
2374
|
-
this.
|
|
2664
|
+
if (this.AppAccessDialog) {
|
|
2665
|
+
this.AppAccessDialog.show({
|
|
2375
2666
|
type: 'not_installed',
|
|
2376
2667
|
appName: appName,
|
|
2377
2668
|
appId: appId
|
|
@@ -2423,10 +2714,14 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2423
2714
|
}
|
|
2424
2715
|
finally {
|
|
2425
2716
|
// Clear loading indicator
|
|
2426
|
-
this.
|
|
2717
|
+
this.LoadingAppId = null;
|
|
2427
2718
|
this.cdr.detectChanges();
|
|
2428
2719
|
}
|
|
2429
2720
|
}
|
|
2721
|
+
/** @deprecated Use {@link OnAppSwitch}. */
|
|
2722
|
+
async onAppSwitch(appId) {
|
|
2723
|
+
return this.OnAppSwitch(appId);
|
|
2724
|
+
}
|
|
2430
2725
|
/**
|
|
2431
2726
|
* Find an existing tab that matches the default nav item
|
|
2432
2727
|
*/
|
|
@@ -2454,22 +2749,30 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2454
2749
|
/**
|
|
2455
2750
|
* Handle Nav Bar app icon click (single click switches to app)
|
|
2456
2751
|
*/
|
|
2457
|
-
|
|
2752
|
+
OnNavBarAppClick(app, event) {
|
|
2458
2753
|
// If shift key is held, force new tab
|
|
2459
2754
|
if (event.shiftKey) {
|
|
2460
2755
|
this.openNavBarAppInNewTab(app);
|
|
2461
2756
|
}
|
|
2462
2757
|
else {
|
|
2463
|
-
this.
|
|
2758
|
+
this.OnAppSwitch(app.ID);
|
|
2464
2759
|
}
|
|
2465
2760
|
}
|
|
2761
|
+
/** @deprecated Use {@link OnNavBarAppClick}. */
|
|
2762
|
+
onNavBarAppClick(app, event) {
|
|
2763
|
+
return this.OnNavBarAppClick(app, event);
|
|
2764
|
+
}
|
|
2466
2765
|
/**
|
|
2467
2766
|
* Handle Nav Bar app icon double-click (opens in new tab)
|
|
2468
2767
|
*/
|
|
2469
|
-
|
|
2768
|
+
OnNavBarAppDblClick(app, event) {
|
|
2470
2769
|
event.preventDefault();
|
|
2471
2770
|
this.openNavBarAppInNewTab(app);
|
|
2472
2771
|
}
|
|
2772
|
+
/** @deprecated Use {@link OnNavBarAppDblClick}. */
|
|
2773
|
+
onNavBarAppDblClick(app, event) {
|
|
2774
|
+
return this.OnNavBarAppDblClick(app, event);
|
|
2775
|
+
}
|
|
2473
2776
|
/**
|
|
2474
2777
|
* Open a nav bar app's default content in a new tab
|
|
2475
2778
|
*/
|
|
@@ -2478,7 +2781,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2478
2781
|
const tabRequest = await app.CreateDefaultTab();
|
|
2479
2782
|
if (tabRequest) {
|
|
2480
2783
|
// Set the app as active first if it isn't already
|
|
2481
|
-
if (!UUIDsEqual(this.
|
|
2784
|
+
if (!UUIDsEqual(this.ActiveApp?.ID, app.ID)) {
|
|
2482
2785
|
await this.appManager.SetActiveApp(app.ID);
|
|
2483
2786
|
}
|
|
2484
2787
|
// Open in new tab by adding to workspace
|
|
@@ -2488,49 +2791,65 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2488
2791
|
/**
|
|
2489
2792
|
* Handle navigation item click with shift-key detection
|
|
2490
2793
|
*/
|
|
2491
|
-
|
|
2492
|
-
if (!this.
|
|
2794
|
+
OnNavItemClick(event) {
|
|
2795
|
+
if (!this.ActiveApp) {
|
|
2493
2796
|
return;
|
|
2494
2797
|
}
|
|
2495
2798
|
const { item, shiftKey } = event;
|
|
2496
2799
|
// Close mobile nav if open. Routed through closeMobileNav() rather than setting the flag
|
|
2497
2800
|
// directly so focus does not get dropped to <body> when the drawer goes inert — tapping a
|
|
2498
2801
|
// nav item is the most common way the drawer closes on a phone.
|
|
2499
|
-
this.
|
|
2802
|
+
this.CloseMobileNav();
|
|
2500
2803
|
// Use NavigationService with forceNewTab option if shift was pressed
|
|
2501
|
-
this.navigationService.OpenNavItem(this.
|
|
2804
|
+
this.navigationService.OpenNavItem(this.ActiveApp.ID, item, this.ActiveApp.GetColor(), { forceNewTab: shiftKey });
|
|
2805
|
+
}
|
|
2806
|
+
/** @deprecated Use {@link OnNavItemClick}. */
|
|
2807
|
+
onNavItemClick(event) {
|
|
2808
|
+
return this.OnNavItemClick(event);
|
|
2502
2809
|
}
|
|
2503
2810
|
/**
|
|
2504
2811
|
* Handle dismiss of a dynamic nav item (remove from recent stack)
|
|
2505
2812
|
*/
|
|
2506
|
-
|
|
2507
|
-
if (!this.
|
|
2813
|
+
OnNavItemDismiss(item) {
|
|
2814
|
+
if (!this.ActiveApp) {
|
|
2508
2815
|
return;
|
|
2509
2816
|
}
|
|
2510
2817
|
// Delegate to HomeApplication's RemoveDynamicNavItem if available
|
|
2511
|
-
const appWithRemove = this.
|
|
2818
|
+
const appWithRemove = this.ActiveApp;
|
|
2512
2819
|
if (typeof appWithRemove.RemoveDynamicNavItem === 'function') {
|
|
2513
2820
|
appWithRemove.RemoveDynamicNavItem(item);
|
|
2514
2821
|
}
|
|
2515
2822
|
}
|
|
2823
|
+
/** @deprecated Use {@link OnNavItemDismiss}. */
|
|
2824
|
+
onNavItemDismiss(item) {
|
|
2825
|
+
return this.OnNavItemDismiss(item);
|
|
2826
|
+
}
|
|
2516
2827
|
/**
|
|
2517
2828
|
* Toggle mobile navigation drawer
|
|
2518
2829
|
*/
|
|
2519
|
-
|
|
2520
|
-
this.
|
|
2521
|
-
if (!this.
|
|
2830
|
+
ToggleMobileNav() {
|
|
2831
|
+
this.MobileNavOpen = !this.MobileNavOpen;
|
|
2832
|
+
if (!this.MobileNavOpen) {
|
|
2522
2833
|
this.returnFocusFromMobileNav();
|
|
2523
2834
|
}
|
|
2524
2835
|
}
|
|
2836
|
+
/** @deprecated Use {@link ToggleMobileNav}. */
|
|
2837
|
+
toggleMobileNav() {
|
|
2838
|
+
return this.ToggleMobileNav();
|
|
2839
|
+
}
|
|
2525
2840
|
/**
|
|
2526
2841
|
* Close mobile navigation drawer
|
|
2527
2842
|
*/
|
|
2528
|
-
|
|
2529
|
-
if (this.
|
|
2530
|
-
this.
|
|
2843
|
+
CloseMobileNav() {
|
|
2844
|
+
if (this.MobileNavOpen) {
|
|
2845
|
+
this.MobileNavOpen = false;
|
|
2531
2846
|
this.returnFocusFromMobileNav();
|
|
2532
2847
|
}
|
|
2533
2848
|
}
|
|
2849
|
+
/** @deprecated Use {@link CloseMobileNav}. */
|
|
2850
|
+
closeMobileNav() {
|
|
2851
|
+
return this.CloseMobileNav();
|
|
2852
|
+
}
|
|
2534
2853
|
/**
|
|
2535
2854
|
* When the drawer closes it becomes `inert`, which drops focus to <body> if focus was
|
|
2536
2855
|
* inside it — the user's place in the page is gone and the next Tab restarts at the top.
|
|
@@ -2538,12 +2857,12 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2538
2857
|
* (e.g. the drawer was closed from a keyboard shortcut while the user was in the content).
|
|
2539
2858
|
*/
|
|
2540
2859
|
returnFocusFromMobileNav() {
|
|
2541
|
-
const drawer = this.
|
|
2860
|
+
const drawer = this.MobileNavDrawer?.nativeElement;
|
|
2542
2861
|
const active = document.activeElement;
|
|
2543
2862
|
if (!drawer || !active || !drawer.contains(active)) {
|
|
2544
2863
|
return;
|
|
2545
2864
|
}
|
|
2546
|
-
this.
|
|
2865
|
+
this.MobileNavToggle?.nativeElement.focus();
|
|
2547
2866
|
}
|
|
2548
2867
|
/**
|
|
2549
2868
|
* Accessible name for the avatar button. The name must never depend on the avatar image
|
|
@@ -2551,15 +2870,19 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2551
2870
|
* name of its own — so it is set on the BUTTON and falls back to a generic label before
|
|
2552
2871
|
* the user record has loaded.
|
|
2553
2872
|
*/
|
|
2554
|
-
get
|
|
2873
|
+
get AvatarAriaLabel() {
|
|
2555
2874
|
return this.userName ? `Account: ${this.userName}` : 'Account';
|
|
2556
2875
|
}
|
|
2876
|
+
/** @deprecated Use {@link AvatarAriaLabel}. */
|
|
2877
|
+
get avatarAriaLabel() {
|
|
2878
|
+
return this.AvatarAriaLabel;
|
|
2879
|
+
}
|
|
2557
2880
|
/**
|
|
2558
2881
|
* Skip link target handoff (WCAG 2.4.1). Focus is moved programmatically rather than left
|
|
2559
2882
|
* to the browser's fragment navigation: the shell's main region is a Golden Layout host,
|
|
2560
2883
|
* and letting the URL hash change here would collide with the deep-link/tab restore path.
|
|
2561
2884
|
*/
|
|
2562
|
-
|
|
2885
|
+
SkipToMainContent(event) {
|
|
2563
2886
|
event.preventDefault();
|
|
2564
2887
|
const main = document.getElementById('mj-main-content');
|
|
2565
2888
|
if (!main) {
|
|
@@ -2568,18 +2891,22 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2568
2891
|
main.focus();
|
|
2569
2892
|
main.scrollIntoView({ block: 'start' });
|
|
2570
2893
|
}
|
|
2894
|
+
/** @deprecated Use {@link SkipToMainContent}. */
|
|
2895
|
+
skipToMainContent(event) {
|
|
2896
|
+
return this.SkipToMainContent(event);
|
|
2897
|
+
}
|
|
2571
2898
|
/**
|
|
2572
2899
|
* Toggle user menu visibility
|
|
2573
2900
|
*/
|
|
2574
|
-
|
|
2901
|
+
ToggleUserMenu(event) {
|
|
2575
2902
|
event.stopPropagation();
|
|
2576
|
-
this.
|
|
2577
|
-
if (this.
|
|
2903
|
+
this.UserMenuVisible = !this.UserMenuVisible;
|
|
2904
|
+
if (this.UserMenuVisible) {
|
|
2578
2905
|
// Close menu when clicking outside. CAPTURE phase so clicks whose
|
|
2579
2906
|
// bubbling something stopped (e.g. the origin crumb's GL-focus
|
|
2580
2907
|
// stoppers) still dismiss the menu.
|
|
2581
2908
|
const closeHandler = () => {
|
|
2582
|
-
this.
|
|
2909
|
+
this.UserMenuVisible = false;
|
|
2583
2910
|
document.removeEventListener('click', closeHandler, true);
|
|
2584
2911
|
};
|
|
2585
2912
|
setTimeout(() => {
|
|
@@ -2587,6 +2914,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2587
2914
|
}, 0);
|
|
2588
2915
|
}
|
|
2589
2916
|
}
|
|
2917
|
+
/** @deprecated Use {@link ToggleUserMenu}. */
|
|
2918
|
+
toggleUserMenu(event) {
|
|
2919
|
+
return this.ToggleUserMenu(event);
|
|
2920
|
+
}
|
|
2590
2921
|
/**
|
|
2591
2922
|
* Initialize the user menu plugin system
|
|
2592
2923
|
*/
|
|
@@ -2599,7 +2930,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2599
2930
|
}
|
|
2600
2931
|
// Initialize developer mode service
|
|
2601
2932
|
if (this.userEntity) {
|
|
2602
|
-
await this.
|
|
2933
|
+
await this.DeveloperModeService.Initialize(this.userEntity);
|
|
2603
2934
|
}
|
|
2604
2935
|
// Check org-level feedback kill switch (defaults to enabled on error)
|
|
2605
2936
|
const feedbackEnabled = await this.feedbackService.IsEnabled();
|
|
@@ -2609,9 +2940,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2609
2940
|
userEntity: this.userEntity,
|
|
2610
2941
|
shell: this,
|
|
2611
2942
|
viewContainerRef: this.viewContainerRef,
|
|
2612
|
-
isDeveloper: this.
|
|
2613
|
-
developerModeEnabled: this.
|
|
2614
|
-
currentApplication: this.
|
|
2943
|
+
isDeveloper: this.DeveloperModeService.IsDeveloper,
|
|
2944
|
+
developerModeEnabled: this.DeveloperModeService.IsEnabled,
|
|
2945
|
+
currentApplication: this.ActiveApp,
|
|
2615
2946
|
workspaceManager: this.workspaceManager,
|
|
2616
2947
|
authService: this.authBase,
|
|
2617
2948
|
pinService: this.homePinService,
|
|
@@ -2628,11 +2959,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2628
2959
|
// Get initial menu elements
|
|
2629
2960
|
this.refreshMenuElements();
|
|
2630
2961
|
// Subscribe to developer mode changes to refresh menu
|
|
2631
|
-
this.
|
|
2962
|
+
this.DeveloperModeService.IsEnabled$.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
2632
2963
|
// Update context and refresh menu
|
|
2633
2964
|
if (this.userMenu) {
|
|
2634
2965
|
this.userMenu.UpdateContext({
|
|
2635
|
-
developerModeEnabled: this.
|
|
2966
|
+
developerModeEnabled: this.DeveloperModeService.IsEnabled
|
|
2636
2967
|
});
|
|
2637
2968
|
}
|
|
2638
2969
|
this.refreshMenuElements();
|
|
@@ -2656,13 +2987,13 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2656
2987
|
*/
|
|
2657
2988
|
refreshMenuElements() {
|
|
2658
2989
|
if (this.userMenu) {
|
|
2659
|
-
this.
|
|
2990
|
+
this.UserMenuElements = this.userMenu.GetMenuElements();
|
|
2660
2991
|
}
|
|
2661
2992
|
}
|
|
2662
2993
|
/**
|
|
2663
2994
|
* Handle user menu item click
|
|
2664
2995
|
*/
|
|
2665
|
-
async
|
|
2996
|
+
async OnUserMenuItemClick(itemId) {
|
|
2666
2997
|
if (!this.userMenu)
|
|
2667
2998
|
return;
|
|
2668
2999
|
const result = await this.userMenu.HandleItemClick(itemId);
|
|
@@ -2683,12 +3014,12 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2683
3014
|
}
|
|
2684
3015
|
}
|
|
2685
3016
|
if (result.message === 'reset-layout') {
|
|
2686
|
-
await this.
|
|
3017
|
+
await this.OnResetLayout();
|
|
2687
3018
|
return;
|
|
2688
3019
|
}
|
|
2689
3020
|
if (result.message === 'pin-to-home') {
|
|
2690
3021
|
// Close menu and show overlay immediately before any async work
|
|
2691
|
-
this.
|
|
3022
|
+
this.UserMenuVisible = false;
|
|
2692
3023
|
this.refreshMenuElements();
|
|
2693
3024
|
this.showPinProgress('Pinning...');
|
|
2694
3025
|
// Let the UI render the overlay before starting the work
|
|
@@ -2707,38 +3038,42 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2707
3038
|
return;
|
|
2708
3039
|
}
|
|
2709
3040
|
if (result.message === 'sharing-center') {
|
|
2710
|
-
this.
|
|
3041
|
+
this.UserMenuVisible = false;
|
|
2711
3042
|
this.userSharingCenterDialogService.open(this.viewContainerRef);
|
|
2712
3043
|
return;
|
|
2713
3044
|
}
|
|
2714
3045
|
if (result.message === 'submit-feedback') {
|
|
2715
|
-
this.
|
|
3046
|
+
this.UserMenuVisible = false;
|
|
2716
3047
|
this.ShowFeedbackDialog();
|
|
2717
3048
|
return;
|
|
2718
3049
|
}
|
|
2719
3050
|
if (result.message === 'about') {
|
|
2720
|
-
this.
|
|
3051
|
+
this.UserMenuVisible = false;
|
|
2721
3052
|
this.aboutDialogService.open(this.viewContainerRef, {
|
|
2722
|
-
avatarUrl: this.
|
|
2723
|
-
avatarIconClass: this.
|
|
3053
|
+
avatarUrl: this.UserImageURL || null,
|
|
3054
|
+
avatarIconClass: this.UserIconClass || null
|
|
2724
3055
|
});
|
|
2725
3056
|
return;
|
|
2726
3057
|
}
|
|
2727
3058
|
if (result.message === 'profile') {
|
|
2728
|
-
this.
|
|
3059
|
+
this.UserMenuVisible = false;
|
|
2729
3060
|
this.profileDialogService.open(this.viewContainerRef, {
|
|
2730
|
-
avatarUrl: this.
|
|
2731
|
-
avatarIconClass: this.
|
|
3061
|
+
avatarUrl: this.UserImageURL || null,
|
|
3062
|
+
avatarIconClass: this.UserIconClass || null
|
|
2732
3063
|
});
|
|
2733
3064
|
return;
|
|
2734
3065
|
}
|
|
2735
3066
|
if (result.closeMenu) {
|
|
2736
|
-
this.
|
|
3067
|
+
this.UserMenuVisible = false;
|
|
2737
3068
|
}
|
|
2738
3069
|
// Refresh menu elements (some items may have changed state)
|
|
2739
3070
|
this.refreshMenuElements();
|
|
2740
3071
|
this.cdr.detectChanges();
|
|
2741
3072
|
}
|
|
3073
|
+
/** @deprecated Use {@link OnUserMenuItemClick}. */
|
|
3074
|
+
async onUserMenuItemClick(itemId) {
|
|
3075
|
+
return this.OnUserMenuItemClick(itemId);
|
|
3076
|
+
}
|
|
2742
3077
|
/**
|
|
2743
3078
|
* Whether the currently applied theme is dark (used by the toggle switch in the template)
|
|
2744
3079
|
*/
|
|
@@ -2749,28 +3084,44 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2749
3084
|
/**
|
|
2750
3085
|
* Get user menu options for template
|
|
2751
3086
|
*/
|
|
2752
|
-
|
|
3087
|
+
GetUserMenuOptions() {
|
|
2753
3088
|
return this.userMenu?.GetOptions();
|
|
2754
3089
|
}
|
|
3090
|
+
/** @deprecated Use {@link GetUserMenuOptions}. */
|
|
3091
|
+
getUserMenuOptions() {
|
|
3092
|
+
return this.GetUserMenuOptions();
|
|
3093
|
+
}
|
|
2755
3094
|
/**
|
|
2756
3095
|
* Get user display info for template
|
|
2757
3096
|
*/
|
|
2758
|
-
|
|
3097
|
+
GetUserDisplayInfo() {
|
|
2759
3098
|
return this.userMenu?.GetUserDisplayInfo();
|
|
2760
3099
|
}
|
|
3100
|
+
/** @deprecated Use {@link GetUserDisplayInfo}. */
|
|
3101
|
+
getUserDisplayInfo() {
|
|
3102
|
+
return this.GetUserDisplayInfo();
|
|
3103
|
+
}
|
|
2761
3104
|
/**
|
|
2762
3105
|
* Check if an element is a divider (for template)
|
|
2763
3106
|
*/
|
|
2764
|
-
|
|
3107
|
+
IsMenuDivider(element) {
|
|
2765
3108
|
return isUserMenuDivider(element);
|
|
2766
3109
|
}
|
|
3110
|
+
/** @deprecated Use {@link IsMenuDivider}. */
|
|
3111
|
+
isMenuDivider(element) {
|
|
3112
|
+
return this.IsMenuDivider(element);
|
|
3113
|
+
}
|
|
2767
3114
|
/**
|
|
2768
3115
|
* Cast element to UserMenuItem (for template type safety)
|
|
2769
3116
|
* Call this only after checking !isMenuDivider(element)
|
|
2770
3117
|
*/
|
|
2771
|
-
|
|
3118
|
+
AsMenuItem(element) {
|
|
2772
3119
|
return element;
|
|
2773
3120
|
}
|
|
3121
|
+
/** @deprecated Use {@link AsMenuItem}. */
|
|
3122
|
+
asMenuItem(element) {
|
|
3123
|
+
return this.AsMenuItem(element);
|
|
3124
|
+
}
|
|
2774
3125
|
/**
|
|
2775
3126
|
* Pin the currently active resource to the Home dashboard
|
|
2776
3127
|
*/
|
|
@@ -2797,10 +3148,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2797
3148
|
}
|
|
2798
3149
|
// Resolve nav item icon for Custom pins
|
|
2799
3150
|
let pinIcon;
|
|
2800
|
-
if (resourceType === 'Custom' && this.
|
|
3151
|
+
if (resourceType === 'Custom' && this.ActiveApp) {
|
|
2801
3152
|
const navItemName = activeTab.configuration?.['navItemName'];
|
|
2802
3153
|
if (navItemName) {
|
|
2803
|
-
const navItems = await this.
|
|
3154
|
+
const navItems = await this.ActiveApp.GetNavItems();
|
|
2804
3155
|
const navItem = navItems.find(ni => ni.Label === navItemName);
|
|
2805
3156
|
pinIcon = navItem?.Icon || undefined;
|
|
2806
3157
|
}
|
|
@@ -2809,9 +3160,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2809
3160
|
DisplayName: displayName,
|
|
2810
3161
|
ResourceType: resourceType,
|
|
2811
3162
|
ApplicationID: activeTab.applicationId,
|
|
2812
|
-
ApplicationName: this.
|
|
3163
|
+
ApplicationName: this.ActiveApp?.Name,
|
|
2813
3164
|
Icon: pinIcon,
|
|
2814
|
-
Color: this.
|
|
3165
|
+
Color: this.ActiveApp?.GetColor() || undefined,
|
|
2815
3166
|
Configuration: activeTab.configuration
|
|
2816
3167
|
});
|
|
2817
3168
|
if (added) {
|
|
@@ -2831,9 +3182,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2831
3182
|
*/
|
|
2832
3183
|
async captureAndAttachThumbnail(tab, resourceType) {
|
|
2833
3184
|
try {
|
|
2834
|
-
if (!this.
|
|
3185
|
+
if (!this.TabContainerRef)
|
|
2835
3186
|
return;
|
|
2836
|
-
const thumbnail = await this.
|
|
3187
|
+
const thumbnail = await this.TabContainerRef.CaptureActiveThumbnail();
|
|
2837
3188
|
if (thumbnail) {
|
|
2838
3189
|
const pin = this.homePinService.FindPin(resourceType, tab.configuration);
|
|
2839
3190
|
if (pin) {
|
|
@@ -2905,19 +3256,23 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2905
3256
|
/**
|
|
2906
3257
|
* Log current workspace configuration to console (debug)
|
|
2907
3258
|
*/
|
|
2908
|
-
|
|
3259
|
+
OnLogLayout() {
|
|
2909
3260
|
const config = this.workspaceManager.GetConfiguration();
|
|
2910
3261
|
console.log('📋 Workspace Configuration:', JSON.stringify(config, null, 2));
|
|
2911
3262
|
console.log('📋 Workspace Configuration (object):', config);
|
|
2912
|
-
this.
|
|
3263
|
+
this.UserMenuVisible = false;
|
|
3264
|
+
}
|
|
3265
|
+
/** @deprecated Use {@link OnLogLayout}. */
|
|
3266
|
+
onLogLayout() {
|
|
3267
|
+
return this.OnLogLayout();
|
|
2913
3268
|
}
|
|
2914
3269
|
/**
|
|
2915
3270
|
* Reset workspace layout - clears all tabs and switches to single-resource mode
|
|
2916
3271
|
*/
|
|
2917
|
-
async
|
|
2918
|
-
this.
|
|
3272
|
+
async OnResetLayout() {
|
|
3273
|
+
this.UserMenuVisible = false;
|
|
2919
3274
|
// Get current active app to create a fresh default tab
|
|
2920
|
-
const currentApp = this.
|
|
3275
|
+
const currentApp = this.ActiveApp;
|
|
2921
3276
|
if (!currentApp) {
|
|
2922
3277
|
console.warn('No active app to reset to');
|
|
2923
3278
|
return;
|
|
@@ -2961,42 +3316,22 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2961
3316
|
// Update workspace configuration
|
|
2962
3317
|
this.workspaceManager.UpdateConfiguration(freshConfig);
|
|
2963
3318
|
}
|
|
3319
|
+
/** @deprecated Use {@link OnResetLayout}. */
|
|
3320
|
+
async onResetLayout() {
|
|
3321
|
+
return this.OnResetLayout();
|
|
3322
|
+
}
|
|
2964
3323
|
/**
|
|
2965
3324
|
* Logout user and clear authentication data
|
|
2966
3325
|
*/
|
|
2967
|
-
async
|
|
2968
|
-
this.
|
|
3326
|
+
async OnLogout() {
|
|
3327
|
+
this.UserMenuVisible = false;
|
|
2969
3328
|
this.authBase.logout();
|
|
2970
3329
|
localStorage.removeItem('auth');
|
|
2971
3330
|
localStorage.removeItem('claims');
|
|
2972
3331
|
}
|
|
2973
|
-
/**
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
*/
|
|
2977
|
-
handleGlobalKeyboardShortcuts(event) {
|
|
2978
|
-
// Skip if user is typing in an input/textarea — EXCEPT when the omnibar is on:
|
|
2979
|
-
// a modal palette is summonable from anywhere (incl. the chat composer), like
|
|
2980
|
-
// Slack/Linear. The legacy path keeps the guard (its binding steals focus).
|
|
2981
|
-
const target = event.target;
|
|
2982
|
-
const inEditable = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
2983
|
-
if (inEditable && !this.UseOmnibar) {
|
|
2984
|
-
return;
|
|
2985
|
-
}
|
|
2986
|
-
// Platform detection
|
|
2987
|
-
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
2988
|
-
const isCtrlOrCmd = isMac ? event.metaKey : event.ctrlKey;
|
|
2989
|
-
// Cmd+/ or Ctrl+/ opens the palette (omnibar '/' mode when enabled, legacy otherwise)
|
|
2990
|
-
if (isCtrlOrCmd && event.key === '/') {
|
|
2991
|
-
event.preventDefault();
|
|
2992
|
-
event.stopPropagation();
|
|
2993
|
-
if (this.UseOmnibar) {
|
|
2994
|
-
this.OpenOmnibar('/');
|
|
2995
|
-
}
|
|
2996
|
-
else {
|
|
2997
|
-
this.commandPaletteService.Open();
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3332
|
+
/** @deprecated Use {@link OnLogout}. */
|
|
3333
|
+
async onLogout() {
|
|
3334
|
+
return this.OnLogout();
|
|
3000
3335
|
}
|
|
3001
3336
|
/**
|
|
3002
3337
|
* Load user avatar from database, auto-sync from auth provider if needed
|
|
@@ -3005,7 +3340,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3005
3340
|
try {
|
|
3006
3341
|
const md = this.ProviderToUse;
|
|
3007
3342
|
this.userName = currentUserInfo.FirstLast || currentUserInfo.Name || 'User';
|
|
3008
|
-
this.
|
|
3343
|
+
this.UserEmail = currentUserInfo.Email || '';
|
|
3009
3344
|
// Load the full MJUserEntity to access avatar fields
|
|
3010
3345
|
const currentUserEntity = await md.GetEntityObject('MJ: Users');
|
|
3011
3346
|
await currentUserEntity.Load(currentUserInfo.ID);
|
|
@@ -3025,8 +3360,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3025
3360
|
catch (error) {
|
|
3026
3361
|
console.warn('Could not load user avatar:', error);
|
|
3027
3362
|
// Use fallback
|
|
3028
|
-
this.
|
|
3029
|
-
this.
|
|
3363
|
+
this.UserImageURL = '';
|
|
3364
|
+
this.UserIconClass = null;
|
|
3030
3365
|
}
|
|
3031
3366
|
}
|
|
3032
3367
|
/**
|
|
@@ -3053,17 +3388,17 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3053
3388
|
*/
|
|
3054
3389
|
applyUserAvatar(user) {
|
|
3055
3390
|
if (user.UserImageURL) {
|
|
3056
|
-
this.
|
|
3057
|
-
this.
|
|
3391
|
+
this.UserImageURL = user.UserImageURL;
|
|
3392
|
+
this.UserIconClass = null;
|
|
3058
3393
|
}
|
|
3059
3394
|
else if (user.UserImageIconClass) {
|
|
3060
|
-
this.
|
|
3061
|
-
this.
|
|
3395
|
+
this.UserIconClass = user.UserImageIconClass;
|
|
3396
|
+
this.UserImageURL = '';
|
|
3062
3397
|
}
|
|
3063
3398
|
else {
|
|
3064
3399
|
// Default fallback - show icon
|
|
3065
|
-
this.
|
|
3066
|
-
this.
|
|
3400
|
+
this.UserImageURL = '';
|
|
3401
|
+
this.UserIconClass = null;
|
|
3067
3402
|
}
|
|
3068
3403
|
this.cdr.detectChanges();
|
|
3069
3404
|
}
|
|
@@ -3075,7 +3410,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3075
3410
|
// Find the active tab
|
|
3076
3411
|
const activeTab = config.tabs?.find(tab => tab.id === config.activeTabId);
|
|
3077
3412
|
// Get app name
|
|
3078
|
-
const appName = this.
|
|
3413
|
+
const appName = this.ActiveApp?.Name || null;
|
|
3079
3414
|
// Get resource name from active tab
|
|
3080
3415
|
let resourceName = null;
|
|
3081
3416
|
if (activeTab) {
|
|
@@ -3092,15 +3427,15 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3092
3427
|
*/
|
|
3093
3428
|
async loadSearchableEntities() {
|
|
3094
3429
|
const md = this.ProviderToUse;
|
|
3095
|
-
this.
|
|
3096
|
-
if (this.
|
|
3097
|
-
this.
|
|
3430
|
+
this.SearchableEntities = md.Entities.filter((e) => e.AllowUserSearchAPI).sort((a, b) => a.Name.localeCompare(b.Name));
|
|
3431
|
+
if (this.SearchableEntities.length > 0) {
|
|
3432
|
+
this.SelectedEntity = this.SearchableEntities[0];
|
|
3098
3433
|
}
|
|
3099
3434
|
}
|
|
3100
3435
|
/**
|
|
3101
3436
|
* Toggle search popup visibility
|
|
3102
3437
|
*/
|
|
3103
|
-
|
|
3438
|
+
ToggleSearch() {
|
|
3104
3439
|
if (!this.ShowSearchBar) {
|
|
3105
3440
|
return;
|
|
3106
3441
|
}
|
|
@@ -3108,36 +3443,44 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3108
3443
|
this.OpenOmnibar();
|
|
3109
3444
|
return;
|
|
3110
3445
|
}
|
|
3111
|
-
this.
|
|
3446
|
+
this.IsSearchOpen = !this.IsSearchOpen;
|
|
3112
3447
|
// Focus on search input when opened
|
|
3113
|
-
if (this.
|
|
3448
|
+
if (this.IsSearchOpen) {
|
|
3114
3449
|
setTimeout(() => {
|
|
3115
|
-
if (this.
|
|
3116
|
-
this.
|
|
3450
|
+
if (this.SearchInput && this.SearchInput.nativeElement) {
|
|
3451
|
+
this.SearchInput.nativeElement.focus();
|
|
3117
3452
|
}
|
|
3118
3453
|
}, 100);
|
|
3119
3454
|
}
|
|
3120
3455
|
}
|
|
3456
|
+
/** @deprecated Use {@link ToggleSearch}. */
|
|
3457
|
+
toggleSearch() {
|
|
3458
|
+
return this.ToggleSearch();
|
|
3459
|
+
}
|
|
3121
3460
|
/**
|
|
3122
3461
|
* Close search popup
|
|
3123
3462
|
*/
|
|
3463
|
+
CloseSearch() {
|
|
3464
|
+
this.IsSearchOpen = false;
|
|
3465
|
+
}
|
|
3466
|
+
/** @deprecated Use {@link CloseSearch}. */
|
|
3124
3467
|
closeSearch() {
|
|
3125
|
-
this.
|
|
3468
|
+
return this.CloseSearch();
|
|
3126
3469
|
}
|
|
3127
3470
|
/**
|
|
3128
3471
|
* Handle search submission
|
|
3129
3472
|
*/
|
|
3130
|
-
|
|
3131
|
-
if (!this.
|
|
3473
|
+
OnSearch(event) {
|
|
3474
|
+
if (!this.SearchInput) {
|
|
3132
3475
|
return;
|
|
3133
3476
|
}
|
|
3134
|
-
const inputValue = this.
|
|
3477
|
+
const inputValue = this.SearchInput.nativeElement.value;
|
|
3135
3478
|
if (inputValue && inputValue.length > 0 && inputValue.trim().length > 2) {
|
|
3136
|
-
this.
|
|
3137
|
-
this.
|
|
3479
|
+
this.SearchInput.nativeElement.value = ''; // Clear input
|
|
3480
|
+
this.IsSearchOpen = false; // Close search popup
|
|
3138
3481
|
// Navigate to search results
|
|
3139
|
-
if (this.
|
|
3140
|
-
this.router.navigate(['resource', 'search', inputValue], { queryParams: { Entity: this.
|
|
3482
|
+
if (this.SelectedEntity) {
|
|
3483
|
+
this.router.navigate(['resource', 'search', inputValue], { queryParams: { Entity: this.SelectedEntity.Name } });
|
|
3141
3484
|
}
|
|
3142
3485
|
}
|
|
3143
3486
|
else {
|
|
@@ -3154,23 +3497,60 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3154
3497
|
});
|
|
3155
3498
|
}
|
|
3156
3499
|
}
|
|
3500
|
+
/** @deprecated Use {@link OnSearch}. */
|
|
3501
|
+
onSearch(event) {
|
|
3502
|
+
return this.OnSearch(event);
|
|
3503
|
+
}
|
|
3157
3504
|
// ========================================
|
|
3158
3505
|
// UNIVERSAL SEARCH EVENT HANDLERS
|
|
3159
3506
|
// ========================================
|
|
3507
|
+
/**
|
|
3508
|
+
* The shell's ONLY `document:keydown` listener — handles every global chord.
|
|
3509
|
+
*
|
|
3510
|
+
* ⚠️ Do NOT add a second `@HostListener('document:keydown')` to this component.
|
|
3511
|
+
* Angular's compiler collects host listeners into an object keyed by event name,
|
|
3512
|
+
* so a second declaration for the same event SILENTLY REPLACES the first — no
|
|
3513
|
+
* build error, no warning, the earlier handler simply never registers. That is
|
|
3514
|
+
* exactly what happened here: a separate `handleGlobalKeyboardShortcuts` owned
|
|
3515
|
+
* Ctrl+/ and was overwritten by this method, so the command palette shortcut was
|
|
3516
|
+
* dead for its entire life. Every new global chord belongs in this one method.
|
|
3517
|
+
*
|
|
3518
|
+
* - **Ctrl/Cmd+K** summons search. Omnibar on: the modal palette opens from
|
|
3519
|
+
* anywhere, even while typing (explicit chord, Slack/Linear semantics). Omnibar
|
|
3520
|
+
* off: the chord FOCUSES the inline header composite on desktop (results attach
|
|
3521
|
+
* beneath it), so keep the don't-steal-focus-mid-typing guard; on mobile (no
|
|
3522
|
+
* composite rendered) it opens the Spotlight overlay instead.
|
|
3523
|
+
* - **Ctrl/Cmd+/** opens the command palette, even from an input — the chord can
|
|
3524
|
+
* never be mistaken for ordinary typing. It yields only when an element under
|
|
3525
|
+
* focus already handled it (`defaultPrevented`): CodeMirror binds Mod-/ to toggle
|
|
3526
|
+
* comment and calls `preventDefault()` without stopping propagation.
|
|
3527
|
+
*/
|
|
3160
3528
|
OnGlobalKeydown(event) {
|
|
3161
|
-
// Ctrl/Cmd+K summons search. Omnibar: the modal palette opens from anywhere,
|
|
3162
|
-
// even while typing (explicit chord, Slack/Linear semantics). Legacy: the
|
|
3163
|
-
// chord FOCUSES the inline header composite on desktop (results attach
|
|
3164
|
-
// beneath it), so keep the don't-steal-focus-mid-typing guard; on mobile
|
|
3165
|
-
// (no composite rendered) it opens the Spotlight overlay instead.
|
|
3166
|
-
const target = event.target;
|
|
3167
|
-
const inEditable = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
3168
|
-
if (inEditable && !this.UseOmnibar) {
|
|
3169
|
-
return;
|
|
3170
|
-
}
|
|
3171
3529
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
3172
3530
|
const isCtrlOrCmd = isMac ? event.metaKey : event.ctrlKey;
|
|
3173
|
-
if (isCtrlOrCmd
|
|
3531
|
+
if (!isCtrlOrCmd) {
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
if (event.key === '/') {
|
|
3535
|
+
if (event.defaultPrevented) {
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
event.preventDefault();
|
|
3539
|
+
event.stopPropagation();
|
|
3540
|
+
if (this.UseOmnibar) {
|
|
3541
|
+
this.OpenOmnibar('/');
|
|
3542
|
+
}
|
|
3543
|
+
else {
|
|
3544
|
+
this.commandPaletteService.Open();
|
|
3545
|
+
}
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
if (event.key === 'k') {
|
|
3549
|
+
const target = event.target;
|
|
3550
|
+
const inEditable = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
3551
|
+
if (inEditable && !this.UseOmnibar) {
|
|
3552
|
+
return;
|
|
3553
|
+
}
|
|
3174
3554
|
event.preventDefault();
|
|
3175
3555
|
event.stopPropagation();
|
|
3176
3556
|
this.OnHeaderSearchClick();
|
|
@@ -3190,10 +3570,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3190
3570
|
if (this.UseOmnibar) {
|
|
3191
3571
|
this.OpenOmnibar();
|
|
3192
3572
|
}
|
|
3193
|
-
else if (!isMobile && this.
|
|
3573
|
+
else if (!isMobile && this.ShellSearchComposite?.Focus) {
|
|
3194
3574
|
// Omnibar-off desktop: the inline composite is on screen — focus it and
|
|
3195
3575
|
// let its attached suggest dropdown do the work.
|
|
3196
|
-
this.
|
|
3576
|
+
this.ShellSearchComposite.Focus();
|
|
3197
3577
|
}
|
|
3198
3578
|
else {
|
|
3199
3579
|
// Omnibar-off mobile: no visible inline composite — open the Spotlight overlay.
|
|
@@ -3235,12 +3615,20 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3235
3615
|
this.LegacySearchOpen = false;
|
|
3236
3616
|
this.navigationService.OpenSearch(query);
|
|
3237
3617
|
}
|
|
3618
|
+
/** Command palette "Search everything" row → full Search Results workspace. Does
|
|
3619
|
+
nothing while the chrome hides search, like the other search entry points. */
|
|
3620
|
+
OnCommandPaletteSearch(query) {
|
|
3621
|
+
if (!this.ShowSearchBar || !query.trim()) {
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
this.navigationService.OpenSearch(query);
|
|
3625
|
+
}
|
|
3238
3626
|
/** Inline composite submit (Enter) → full Search Results workspace, carrying the
|
|
3239
3627
|
composite's relevance/scope selections. */
|
|
3240
3628
|
OnSearchSubmitted(query) {
|
|
3241
3629
|
if (query && query.trim().length >= 2) {
|
|
3242
|
-
const minRelevance = this.
|
|
3243
|
-
const scopeIDs = this.
|
|
3630
|
+
const minRelevance = this.ShellSearchComposite?.MinRelevancePercent;
|
|
3631
|
+
const scopeIDs = this.ShellSearchComposite?.SelectedScopeIDs;
|
|
3244
3632
|
const opts = {};
|
|
3245
3633
|
if (minRelevance)
|
|
3246
3634
|
opts.minRelevance = minRelevance;
|
|
@@ -3260,19 +3648,27 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3260
3648
|
* Show notifications page as a tab
|
|
3261
3649
|
*/
|
|
3262
3650
|
/** Toggle the global Activity drawer. */
|
|
3263
|
-
|
|
3651
|
+
ToggleActivity(event) {
|
|
3264
3652
|
event.stopPropagation();
|
|
3265
|
-
this.
|
|
3653
|
+
this.ActivityOpen = !this.ActivityOpen;
|
|
3266
3654
|
this.cdr.detectChanges();
|
|
3267
3655
|
}
|
|
3656
|
+
/** @deprecated Use {@link ToggleActivity}. */
|
|
3657
|
+
toggleActivity(event) {
|
|
3658
|
+
return this.ToggleActivity(event);
|
|
3659
|
+
}
|
|
3268
3660
|
/** Clear finished activities from the tracker. */
|
|
3269
|
-
|
|
3661
|
+
ClearFinishedActivity() {
|
|
3270
3662
|
this.activityService.ClearFinished();
|
|
3271
3663
|
}
|
|
3664
|
+
/** @deprecated Use {@link ClearFinishedActivity}. */
|
|
3665
|
+
clearFinishedActivity() {
|
|
3666
|
+
return this.ClearFinishedActivity();
|
|
3667
|
+
}
|
|
3272
3668
|
/** Close the Activity drawer when clicking anywhere outside it. */
|
|
3273
3669
|
onDocumentClickCloseActivity() {
|
|
3274
|
-
if (this.
|
|
3275
|
-
this.
|
|
3670
|
+
if (this.ActivityOpen) {
|
|
3671
|
+
this.ActivityOpen = false;
|
|
3276
3672
|
this.cdr.detectChanges();
|
|
3277
3673
|
}
|
|
3278
3674
|
}
|
|
@@ -3322,8 +3718,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3322
3718
|
async captureAndAttachFeedbackScreenshot() {
|
|
3323
3719
|
try {
|
|
3324
3720
|
let screenshot;
|
|
3325
|
-
if (this.
|
|
3326
|
-
screenshot = await this.
|
|
3721
|
+
if (this.TabContainerRef) {
|
|
3722
|
+
screenshot = await this.TabContainerRef.CaptureActiveThumbnail();
|
|
3327
3723
|
}
|
|
3328
3724
|
if (!screenshot) {
|
|
3329
3725
|
const contentEl = document.querySelector('.shell-content')
|
|
@@ -3369,8 +3765,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3369
3765
|
// Show the dialog on top of the loading screen
|
|
3370
3766
|
// Use setTimeout to ensure the dialog component is ready after view init
|
|
3371
3767
|
setTimeout(() => {
|
|
3372
|
-
if (this.
|
|
3373
|
-
this.
|
|
3768
|
+
if (this.AppAccessDialog) {
|
|
3769
|
+
this.AppAccessDialog.show(dialogConfig);
|
|
3374
3770
|
}
|
|
3375
3771
|
else {
|
|
3376
3772
|
// Fallback if dialog not available - redirect to a working app
|
|
@@ -3428,24 +3824,24 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3428
3824
|
LogStatus('User has no applications available');
|
|
3429
3825
|
// Stop loading animation and show the dialog
|
|
3430
3826
|
this.stopLoadingAnimation();
|
|
3431
|
-
this.
|
|
3827
|
+
this.Loading = false;
|
|
3432
3828
|
this.cdr.detectChanges();
|
|
3433
3829
|
setTimeout(() => {
|
|
3434
|
-
if (this.
|
|
3435
|
-
this.
|
|
3830
|
+
if (this.AppAccessDialog) {
|
|
3831
|
+
this.AppAccessDialog.show({ type: 'no_apps' });
|
|
3436
3832
|
}
|
|
3437
3833
|
}, 0);
|
|
3438
3834
|
}
|
|
3439
3835
|
/**
|
|
3440
3836
|
* Handle Golden Layout initialization failure
|
|
3441
3837
|
*/
|
|
3442
|
-
|
|
3838
|
+
HandleLayoutError() {
|
|
3443
3839
|
LogStatus('Golden Layout initialization failed');
|
|
3444
3840
|
const availableApps = this.appManager.GetAllApps();
|
|
3445
3841
|
if (availableApps.length > 0) {
|
|
3446
3842
|
setTimeout(() => {
|
|
3447
|
-
if (this.
|
|
3448
|
-
this.
|
|
3843
|
+
if (this.AppAccessDialog) {
|
|
3844
|
+
this.AppAccessDialog.show({ type: 'layout_error' });
|
|
3449
3845
|
}
|
|
3450
3846
|
else {
|
|
3451
3847
|
// Direct redirect if dialog not available
|
|
@@ -3454,10 +3850,14 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3454
3850
|
}, 0);
|
|
3455
3851
|
}
|
|
3456
3852
|
}
|
|
3853
|
+
/** @deprecated Use {@link HandleLayoutError}. */
|
|
3854
|
+
handleLayoutError() {
|
|
3855
|
+
return this.HandleLayoutError();
|
|
3856
|
+
}
|
|
3457
3857
|
/**
|
|
3458
3858
|
* Handle dialog result (install, enable, or redirect)
|
|
3459
3859
|
*/
|
|
3460
|
-
async
|
|
3860
|
+
async OnAppAccessDialogResult(result) {
|
|
3461
3861
|
const availableApps = this.appManager.GetAllApps();
|
|
3462
3862
|
switch (result.action) {
|
|
3463
3863
|
case 'install':
|
|
@@ -3477,6 +3877,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3477
3877
|
break;
|
|
3478
3878
|
}
|
|
3479
3879
|
}
|
|
3880
|
+
/** @deprecated Use {@link OnAppAccessDialogResult}. */
|
|
3881
|
+
async onAppAccessDialogResult(result) {
|
|
3882
|
+
return this.OnAppAccessDialogResult(result);
|
|
3883
|
+
}
|
|
3480
3884
|
/**
|
|
3481
3885
|
* Install an app for the user and navigate to it
|
|
3482
3886
|
*/
|
|
@@ -3493,13 +3897,13 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3493
3897
|
}
|
|
3494
3898
|
else {
|
|
3495
3899
|
console.error('[ShellComponent] Failed to add application');
|
|
3496
|
-
this.
|
|
3900
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3497
3901
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3498
3902
|
}
|
|
3499
3903
|
}
|
|
3500
3904
|
catch (error) {
|
|
3501
3905
|
console.error('Error adding app:', error);
|
|
3502
|
-
this.
|
|
3906
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3503
3907
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3504
3908
|
}
|
|
3505
3909
|
}
|
|
@@ -3514,13 +3918,13 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3514
3918
|
await this.waitForAppAndNavigate(appId);
|
|
3515
3919
|
}
|
|
3516
3920
|
else {
|
|
3517
|
-
this.
|
|
3921
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3518
3922
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3519
3923
|
}
|
|
3520
3924
|
}
|
|
3521
3925
|
catch (error) {
|
|
3522
3926
|
console.error('Error enabling app:', error);
|
|
3523
|
-
this.
|
|
3927
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3524
3928
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3525
3929
|
}
|
|
3526
3930
|
}
|
|
@@ -3536,7 +3940,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3536
3940
|
const app = this.appManager.GetAppById(appId);
|
|
3537
3941
|
if (app) {
|
|
3538
3942
|
await this.navigateToApp(app);
|
|
3539
|
-
this.
|
|
3943
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3540
3944
|
return;
|
|
3541
3945
|
}
|
|
3542
3946
|
// Wait a bit before checking again
|
|
@@ -3546,11 +3950,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3546
3950
|
const systemApp = this.appManager.GetAllSystemApps().find(a => UUIDsEqual(a.ID, appId));
|
|
3547
3951
|
if (systemApp) {
|
|
3548
3952
|
await this.navigateToApp(systemApp);
|
|
3549
|
-
this.
|
|
3953
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3550
3954
|
}
|
|
3551
3955
|
else {
|
|
3552
3956
|
console.warn(`[ShellComponent] App ${appId} not found after waiting, redirecting to fallback app`);
|
|
3553
|
-
this.
|
|
3957
|
+
this.AppAccessDialog?.completeProcessing();
|
|
3554
3958
|
this.redirectToFallbackApp(this.appManager.GetAllApps());
|
|
3555
3959
|
}
|
|
3556
3960
|
}
|
|
@@ -3571,7 +3975,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3571
3975
|
}
|
|
3572
3976
|
/** Case-insensitive UUID check whether an app is the currently active app. */
|
|
3573
3977
|
IsActiveApp(app) {
|
|
3574
|
-
return UUIDsEqual(app.ID, this.
|
|
3978
|
+
return UUIDsEqual(app.ID, this.ActiveApp?.ID);
|
|
3575
3979
|
}
|
|
3576
3980
|
/**
|
|
3577
3981
|
* Candidate order for landing and fallback navigation: the declared-default app
|
|
@@ -3632,7 +4036,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3632
4036
|
}
|
|
3633
4037
|
}
|
|
3634
4038
|
// No apps available - this shouldn't happen, but handle gracefully
|
|
3635
|
-
this.
|
|
4039
|
+
this.Loading = false;
|
|
3636
4040
|
this.cdr.detectChanges();
|
|
3637
4041
|
}
|
|
3638
4042
|
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)); };
|
|
@@ -3640,16 +4044,16 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3640
4044
|
i0.ɵɵviewQuery(_c0, 5)(_c1, 5)(_c2, 5)(_c3, 5)(_c4, 5)(TabContainerComponent, 5)(_c5, 5);
|
|
3641
4045
|
} if (rf & 2) {
|
|
3642
4046
|
let _t;
|
|
3643
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3644
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3645
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3646
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3647
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3648
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
3649
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
4047
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.SearchInput = _t.first);
|
|
4048
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.MobileNavDrawer = _t.first);
|
|
4049
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.MobileNavToggle = _t.first);
|
|
4050
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.OmnibarPalette = _t.first);
|
|
4051
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.ShellSearchComposite = _t.first);
|
|
4052
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.TabContainerRef = _t.first);
|
|
4053
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.AppAccessDialog = _t.first);
|
|
3650
4054
|
} }, hostBindings: function ShellComponent_HostBindings(rf, ctx) { if (rf & 1) {
|
|
3651
4055
|
i0.ɵɵlistener("keydown", function ShellComponent_keydown_HostBindingHandler($event) { return ctx.OnGlobalKeydown($event); }, i0.ɵɵresolveDocument)("click", function ShellComponent_click_HostBindingHandler() { return ctx.onDocumentClickCloseActivity(); }, i0.ɵɵresolveDocument);
|
|
3652
|
-
} }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 55, vars:
|
|
4056
|
+
} }, 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) {
|
|
3653
4057
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
3654
4058
|
i0.ɵɵelementStart(0, "div", 6)(1, "a", 7);
|
|
3655
4059
|
i0.ɵɵlistener("click", function ShellComponent_Template_a_click_1_listener($event) { return ctx.skipToMainContent($event); });
|
|
@@ -3715,7 +4119,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3715
4119
|
i0.ɵɵlistener("result", function ShellComponent_Template_mj_app_access_dialog_result_49_listener($event) { return ctx.onAppAccessDialogResult($event); });
|
|
3716
4120
|
i0.ɵɵelementEnd();
|
|
3717
4121
|
i0.ɵɵelementStart(51, "mj-command-palette", 46);
|
|
3718
|
-
i0.ɵɵlistener("AppSelected", function ShellComponent_Template_mj_command_palette_AppSelected_51_listener($event) { return ctx.onAppSwitch($event); });
|
|
4122
|
+
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); });
|
|
3719
4123
|
i0.ɵɵelementEnd();
|
|
3720
4124
|
i0.ɵɵconditionalCreate(52, ShellComponent_Conditional_52_Template, 2, 0, "mj-omnibar-palette");
|
|
3721
4125
|
i0.ɵɵconditionalCreate(53, ShellComponent_Conditional_53_Template, 1, 4, "mj-search-overlay", 47);
|
|
@@ -3767,7 +4171,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3767
4171
|
i0.ɵɵclassProp("hide-tab-bar", !ctx.tabBarVisible);
|
|
3768
4172
|
i0.ɵɵadvance();
|
|
3769
4173
|
i0.ɵɵconditional(ctx.loading ? 48 : -1);
|
|
3770
|
-
i0.ɵɵadvance(
|
|
4174
|
+
i0.ɵɵadvance(3);
|
|
4175
|
+
i0.ɵɵproperty("ShowSearch", ctx.ShowSearchBar);
|
|
4176
|
+
i0.ɵɵadvance();
|
|
3771
4177
|
i0.ɵɵconditional(ctx.ShowSearchBar ? 52 : -1);
|
|
3772
4178
|
i0.ɵɵadvance();
|
|
3773
4179
|
i0.ɵɵconditional(ctx.ShowSearchBar && !ctx.UseOmnibar ? 53 : -1);
|
|
@@ -3777,31 +4183,28 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3777
4183
|
}
|
|
3778
4184
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ShellComponent, [{
|
|
3779
4185
|
type: Component,
|
|
3780
|
-
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"] }]
|
|
3781
|
-
}], () => [{ 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 }], {
|
|
4186
|
+
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"] }]
|
|
4187
|
+
}], () => [{ 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: [{
|
|
3782
4188
|
type: ViewChild,
|
|
3783
4189
|
args: ['searchInput']
|
|
3784
|
-
}],
|
|
4190
|
+
}], MobileNavDrawer: [{
|
|
3785
4191
|
type: ViewChild,
|
|
3786
4192
|
args: ['mobileNavDrawer']
|
|
3787
|
-
}],
|
|
4193
|
+
}], MobileNavToggle: [{
|
|
3788
4194
|
type: ViewChild,
|
|
3789
4195
|
args: ['mobileNavToggle']
|
|
3790
|
-
}],
|
|
4196
|
+
}], OmnibarPalette: [{
|
|
3791
4197
|
type: ViewChild,
|
|
3792
4198
|
args: ['omnibarPalette']
|
|
3793
|
-
}],
|
|
4199
|
+
}], ShellSearchComposite: [{
|
|
3794
4200
|
type: ViewChild,
|
|
3795
4201
|
args: ['shellSearchComposite']
|
|
3796
|
-
}],
|
|
4202
|
+
}], TabContainerRef: [{
|
|
3797
4203
|
type: ViewChild,
|
|
3798
4204
|
args: [TabContainerComponent]
|
|
3799
|
-
}],
|
|
4205
|
+
}], AppAccessDialog: [{
|
|
3800
4206
|
type: ViewChild,
|
|
3801
4207
|
args: ['appAccessDialog']
|
|
3802
|
-
}], handleGlobalKeyboardShortcuts: [{
|
|
3803
|
-
type: HostListener,
|
|
3804
|
-
args: ['document:keydown', ['$event']]
|
|
3805
4208
|
}], OnGlobalKeydown: [{
|
|
3806
4209
|
type: HostListener,
|
|
3807
4210
|
args: ['document:keydown', ['$event']]
|
|
@@ -3809,5 +4212,5 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3809
4212
|
type: HostListener,
|
|
3810
4213
|
args: ['document:click']
|
|
3811
4214
|
}] }); })();
|
|
3812
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ShellComponent, { className: "ShellComponent", filePath: "src/lib/shell/shell.component.ts", lineNumber:
|
|
4215
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ShellComponent, { className: "ShellComponent", filePath: "src/lib/shell/shell.component.ts", lineNumber: 60 }); })();
|
|
3813
4216
|
//# sourceMappingURL=shell.component.js.map
|