@memberjunction/ng-explorer-core 6.2.0-edge.3 → 6.2.0-edge.5
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/README.md +10 -0
- package/dist/app-routing.module.d.ts.map +1 -1
- package/dist/app-routing.module.js +13 -6
- 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 +4 -2
- package/dist/generated/lazy-feature-config.js.map +1 -1
- package/dist/lib/guards/moved-nav-item-redirect.d.ts +15 -0
- package/dist/lib/guards/moved-nav-item-redirect.d.ts.map +1 -0
- package/dist/lib/guards/moved-nav-item-redirect.js +37 -0
- package/dist/lib/guards/moved-nav-item-redirect.js.map +1 -0
- package/dist/lib/oauth/oauth-callback.component.d.ts +2 -0
- package/dist/lib/oauth/oauth-callback.component.d.ts.map +1 -1
- package/dist/lib/oauth/oauth-callback.component.js +10 -10
- package/dist/lib/oauth/oauth-callback.component.js.map +1 -1
- package/dist/lib/omnibar/omnibar-dashboard-matches.d.ts +34 -0
- package/dist/lib/omnibar/omnibar-dashboard-matches.d.ts.map +1 -0
- package/dist/lib/omnibar/omnibar-dashboard-matches.js +70 -0
- package/dist/lib/omnibar/omnibar-dashboard-matches.js.map +1 -0
- package/dist/lib/omnibar/omnibar-palette.component.d.ts +27 -2
- package/dist/lib/omnibar/omnibar-palette.component.d.ts.map +1 -1
- package/dist/lib/omnibar/omnibar-palette.component.js +126 -65
- package/dist/lib/omnibar/omnibar-palette.component.js.map +1 -1
- package/dist/lib/omnibar/omnibar-provider.d.ts +4 -0
- package/dist/lib/omnibar/omnibar-provider.d.ts.map +1 -1
- package/dist/lib/omnibar/omnibar-provider.js.map +1 -1
- package/dist/lib/omnibar/providers/omnibar-record.provider.d.ts +2 -2
- package/dist/lib/omnibar/providers/omnibar-record.provider.d.ts.map +1 -1
- package/dist/lib/omnibar/providers/omnibar-record.provider.js +3 -7
- package/dist/lib/omnibar/providers/omnibar-record.provider.js.map +1 -1
- package/dist/lib/omnibar/providers/omnibar-search.provider.d.ts +15 -2
- package/dist/lib/omnibar/providers/omnibar-search.provider.d.ts.map +1 -1
- package/dist/lib/omnibar/providers/omnibar-search.provider.js +59 -7
- package/dist/lib/omnibar/providers/omnibar-search.provider.js.map +1 -1
- package/dist/lib/profile/forking-setting.d.ts +8 -0
- package/dist/lib/profile/forking-setting.d.ts.map +1 -0
- package/dist/lib/profile/forking-setting.js +18 -0
- package/dist/lib/profile/forking-setting.js.map +1 -0
- package/dist/lib/profile/profile-dialog.component.d.ts +8 -0
- package/dist/lib/profile/profile-dialog.component.d.ts.map +1 -1
- package/dist/lib/profile/profile-dialog.component.js +182 -92
- package/dist/lib/profile/profile-dialog.component.js.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +16 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.js +27 -4
- package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +24 -21
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +231 -54
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-search-routing.d.ts +10 -3
- package/dist/lib/resource-wrappers/chat-search-routing.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-search-routing.js +11 -4
- package/dist/lib/resource-wrappers/chat-search-routing.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts +12 -3
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.js +18 -3
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.d.ts +29 -0
- package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.js +141 -0
- package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-part-config.d.ts +28 -0
- package/dist/lib/resource-wrappers/dashboard-part-config.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-part-config.js +150 -0
- package/dist/lib/resource-wrappers/dashboard-part-config.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts +361 -56
- package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-resource.component.js +1312 -361
- package/dist/lib/resource-wrappers/dashboard-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/dashboard-source-search.d.ts +86 -0
- package/dist/lib/resource-wrappers/dashboard-source-search.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-source-search.js +136 -0
- package/dist/lib/resource-wrappers/dashboard-source-search.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-studio-pane.d.ts +30 -0
- package/dist/lib/resource-wrappers/dashboard-studio-pane.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-studio-pane.js +55 -0
- package/dist/lib/resource-wrappers/dashboard-studio-pane.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-studio-tools.d.ts +135 -0
- package/dist/lib/resource-wrappers/dashboard-studio-tools.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-studio-tools.js +856 -0
- package/dist/lib/resource-wrappers/dashboard-studio-tools.js.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-tab-agent.d.ts +87 -0
- package/dist/lib/resource-wrappers/dashboard-tab-agent.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/dashboard-tab-agent.js +125 -0
- package/dist/lib/resource-wrappers/dashboard-tab-agent.js.map +1 -0
- package/dist/lib/resource-wrappers/livekit-room-resource.component.d.ts +28 -7
- package/dist/lib/resource-wrappers/livekit-room-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/livekit-room-resource.component.js +174 -112
- package/dist/lib/resource-wrappers/livekit-room-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/search-results-resource.component.d.ts +3 -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 +10 -14
- package/dist/lib/resource-wrappers/search-results-resource.component.js.map +1 -1
- package/dist/lib/server-connectivity/server-connectivity-banner.component.d.ts +16 -3
- package/dist/lib/server-connectivity/server-connectivity-banner.component.d.ts.map +1 -1
- package/dist/lib/server-connectivity/server-connectivity-banner.component.js +17 -3
- package/dist/lib/server-connectivity/server-connectivity-banner.component.js.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +41 -0
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.js +170 -39
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/shell/dashboard-tab-for-url.d.ts +35 -0
- package/dist/lib/shell/dashboard-tab-for-url.d.ts.map +1 -0
- package/dist/lib/shell/dashboard-tab-for-url.js +39 -0
- package/dist/lib/shell/dashboard-tab-for-url.js.map +1 -0
- package/dist/lib/shell/deep-link-workspace.d.ts +25 -0
- package/dist/lib/shell/deep-link-workspace.d.ts.map +1 -0
- package/dist/lib/shell/deep-link-workspace.js +49 -0
- package/dist/lib/shell/deep-link-workspace.js.map +1 -0
- package/dist/lib/shell/shell.component.d.ts +43 -2
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +174 -42
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/shell/shell.module.d.ts +2 -1
- package/dist/lib/shell/shell.module.d.ts.map +1 -1
- package/dist/lib/shell/shell.module.js +6 -2
- package/dist/lib/shell/shell.module.js.map +1 -1
- package/dist/lib/single-dashboard/single-dashboard.component.js +1 -1
- package/dist/lib/single-dashboard/single-dashboard.component.js.map +1 -1
- package/dist/lib/single-record/single-record.component.d.ts +12 -0
- package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
- package/dist/lib/single-record/single-record.component.js +49 -8
- package/dist/lib/single-record/single-record.component.js.map +1 -1
- package/dist/lib/template-user/template-user-banner.component.d.ts +20 -0
- package/dist/lib/template-user/template-user-banner.component.d.ts.map +1 -0
- package/dist/lib/template-user/template-user-banner.component.js +67 -0
- package/dist/lib/template-user/template-user-banner.component.js.map +1 -0
- package/dist/module.d.ts +61 -59
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +7 -0
- package/dist/module.js.map +1 -1
- package/dist/public-api.d.ts +1 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +1 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +58 -53
|
@@ -5,7 +5,7 @@ import { combineLatest, Subject } from 'rxjs';
|
|
|
5
5
|
import { filter, takeUntil } from 'rxjs/operators';
|
|
6
6
|
import { LogStatus, LogError, StartupManager, CompositeKey, EncodeNewRecordValuesForURL, IsNewEntityRecordUrlId, NEW_ENTITY_RECORD_URL_ID, NEW_RECORD_VALUES_QUERY_PARAM, RecordUrlMatchesTab, ResourceUrlsEquivalent } from '@memberjunction/core';
|
|
7
7
|
import { MJEventType, MJGlobal, uuidv4, UUIDsEqual } from '@memberjunction/global';
|
|
8
|
-
import { EventCodes, SharedService, SYSTEM_APP_ID, SetRecordOpenStyle, IsRecordsRegionTab, IsRecordsTabConfiguration } from '@memberjunction/ng-shared';
|
|
8
|
+
import { EventCodes, SharedService, SYSTEM_APP_ID, SetRecordOpenStyle, IsRecordsRegionTab, IsRecordsTabConfiguration, NormalizeRecordIdForMatch } from '@memberjunction/ng-shared';
|
|
9
9
|
import { MJNotificationService } from '@memberjunction/ng-notifications';
|
|
10
10
|
import { IsOmnibarAvailable, IsOmnibarEnabledForUser, OMNIBAR_PROMO_DISMISSED_KEY, OMNIBAR_USER_SETTING_KEY } from '../omnibar/omnibar-user-setting';
|
|
11
11
|
import { GetOmnibarShortcutLabel } from '../omnibar/omnibar-shortcut';
|
|
@@ -17,6 +17,8 @@ import { PACKAGE_VERSION } from '@memberjunction/graphql-dataprovider';
|
|
|
17
17
|
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
|
|
18
18
|
import { ApplyShellChromePolicy, BaseShellChromePolicy } from './shell-chrome-policy';
|
|
19
19
|
import { SetReadinessBeacon } from './readiness-beacon';
|
|
20
|
+
import { FindDashboardTabForUrl, OpenDashboardForUrl, ParseAppDashboardUrl } from './dashboard-tab-for-url';
|
|
21
|
+
import { ApplyDeepLinkToRestoredWorkspace } from './deep-link-workspace';
|
|
20
22
|
import * as i0 from "@angular/core";
|
|
21
23
|
import * as i1 from "@memberjunction/ng-base-application";
|
|
22
24
|
import * as i2 from "@memberjunction/ng-shared";
|
|
@@ -35,12 +37,13 @@ import * as i14 from "@memberjunction/ng-ui-components";
|
|
|
35
37
|
import * as i15 from "@memberjunction/ng-shared-generic";
|
|
36
38
|
import * as i16 from "@memberjunction/ng-search";
|
|
37
39
|
import * as i17 from "./components/record-open/records-hub-pill.component";
|
|
38
|
-
import * as i18 from "
|
|
39
|
-
import * as i19 from "./components/header/app-
|
|
40
|
-
import * as i20 from "./components/
|
|
41
|
-
import * as i21 from "./components/
|
|
42
|
-
import * as i22 from "
|
|
43
|
-
import * as i23 from "../
|
|
40
|
+
import * as i18 from "@memberjunction/ng-realtime-media";
|
|
41
|
+
import * as i19 from "./components/header/app-switcher.component";
|
|
42
|
+
import * as i20 from "./components/header/app-nav.component";
|
|
43
|
+
import * as i21 from "./components/tabs/tab-container.component";
|
|
44
|
+
import * as i22 from "./components/dialogs/app-access-dialog.component";
|
|
45
|
+
import * as i23 from "../command-palette/command-palette.component";
|
|
46
|
+
import * as i24 from "../omnibar/omnibar-palette.component";
|
|
44
47
|
const _c0 = ["searchInput"];
|
|
45
48
|
const _c1 = ["mobileNavDrawer"];
|
|
46
49
|
const _c2 = ["mobileNavToggle"];
|
|
@@ -944,6 +947,26 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
944
947
|
}
|
|
945
948
|
/** The record-open style resolved at startup ('records' until resolved) */
|
|
946
949
|
resolvedRecordOpenStyle = 'records';
|
|
950
|
+
/**
|
|
951
|
+
* When the current url became current.
|
|
952
|
+
*
|
|
953
|
+
* It exists so a url sync can tell whether it is reacting to something OLDER than the active tab.
|
|
954
|
+
* Without it the only available answer was Date.now(), which is newer than everything and so can
|
|
955
|
+
* never lose -- the bug this was added for.
|
|
956
|
+
*
|
|
957
|
+
* SEEDED AT CONSTRUCTION, AND UPDATED ON EVERY NavigationEnd THIS COMPONENT SEES — which is not
|
|
958
|
+
* quite every NavigationEnd, and the difference is worth stating because an earlier version of this
|
|
959
|
+
* comment claimed otherwise. On a deep link `ngOnInit` starts `InitializeShell` FROM the first
|
|
960
|
+
* NavigationEnd, and the subscriber that maintains this field is created later, inside that method.
|
|
961
|
+
* So the first navigation is never stamped, and the startup sync at the end of `InitializeShell`
|
|
962
|
+
* compares against construction time instead.
|
|
963
|
+
*
|
|
964
|
+
* That is harmless where it has been looked at: by the time the startup sync runs, the tab the url
|
|
965
|
+
* matches is the active tab, so the guard is not reached. It is recorded rather than fixed because
|
|
966
|
+
* the construction seed and the first navigation are milliseconds apart, and because a reader who
|
|
967
|
+
* trusts the old wording would mis-reason about the startup path.
|
|
968
|
+
*/
|
|
969
|
+
lastNavigationAt = Date.now();
|
|
947
970
|
/**
|
|
948
971
|
* Resolve `Shell.RecordOpen.Style` from instance config and push it to the
|
|
949
972
|
* two collaborators that partition tabs by it: the ng-shared style module
|
|
@@ -1287,8 +1310,17 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1287
1310
|
// Check the current URL to determine if we're loading from a URL-based navigation
|
|
1288
1311
|
const currentUrl = this.router.url;
|
|
1289
1312
|
this.urlBasedNavigation = currentUrl.includes('/app/') || currentUrl.includes('/resource/');
|
|
1290
|
-
// Wait for workspace initialization to complete before allowing any tab operations
|
|
1291
|
-
|
|
1313
|
+
// Wait for workspace initialization to complete before allowing any tab operations.
|
|
1314
|
+
// On a deep link the URL's query params are written into the matching restored tab
|
|
1315
|
+
// BEFORE the configuration is published, so the URL — not the saved workspace — is what
|
|
1316
|
+
// that tab's component first sees (see ApplyDeepLinkToRestoredWorkspace).
|
|
1317
|
+
const startupUrl = currentUrl;
|
|
1318
|
+
const startupPath = startupUrl.split('?')[0];
|
|
1319
|
+
const startupIsDeepLink = startupPath.length > 1 && startupPath !== '/';
|
|
1320
|
+
await this.workspaceManager.Initialize(user.ID, startupIsDeepLink ? { PrepareConfiguration: config => this.applyStartupDeepLink(config, startupUrl) } : undefined);
|
|
1321
|
+
// A saved tab of a nav item that moved to another app becomes a tab of that app's nav item: once now, before the
|
|
1322
|
+
// shell syncs the URL or the active app from the saved tabs, and again each time the user's app list changes.
|
|
1323
|
+
this.subscriptions.push(await this.navigationService.WatchMovedNavItemTabs());
|
|
1292
1324
|
// Subscribe to tab bar visibility changes
|
|
1293
1325
|
this.subscriptions.push(this.workspaceManager.TabBarVisible.subscribe(visible => {
|
|
1294
1326
|
this.TabBarVisible = visible;
|
|
@@ -1343,6 +1375,13 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1343
1375
|
// Nav-app list is now populated/changed — refresh the precomputed arrays
|
|
1344
1376
|
// bound in the header template.
|
|
1345
1377
|
this.recomputeNavBarApps();
|
|
1378
|
+
// An app-locked session (magic link) whose user cannot read, or has no, User Application rows
|
|
1379
|
+
// still gets its scoped app: adopt it before deciding there are no apps. Adopting re-emits
|
|
1380
|
+
// Applications, which re-enters this handler with the app present.
|
|
1381
|
+
const sessionLockedId = this.authBase.GetSessionScope()?.restrictedToApplicationId;
|
|
1382
|
+
if (sessionLockedId && this.appManager.AdoptSessionApplication(sessionLockedId)) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1346
1385
|
// Handle the case where user has no apps at all (only after loading is complete)
|
|
1347
1386
|
if (apps.length === 0) {
|
|
1348
1387
|
await this.handleNoAppsAvailable();
|
|
@@ -1449,8 +1488,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1449
1488
|
}));
|
|
1450
1489
|
// Subscribe to router navigation events (for browser back/forward)
|
|
1451
1490
|
this.subscriptions.push(this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(event => {
|
|
1491
|
+
// Record WHEN the url changed, whether or not the shell is ready to act on it: the stamp is
|
|
1492
|
+
// what later tells a sync that it is older than an activation it would otherwise override.
|
|
1493
|
+
this.lastNavigationAt = Date.now();
|
|
1452
1494
|
if (this.Initialized) {
|
|
1453
|
-
this.syncWorkspaceWithUrl(event.urlAfterRedirects || event.url);
|
|
1495
|
+
this.syncWorkspaceWithUrl(event.urlAfterRedirects || event.url, this.lastNavigationAt);
|
|
1454
1496
|
}
|
|
1455
1497
|
}));
|
|
1456
1498
|
// Check for deep link parameters on initialization
|
|
@@ -1465,6 +1507,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1465
1507
|
const currentUserInfo = md.CurrentUser;
|
|
1466
1508
|
const userEntity = await md.GetEntityObject('MJ: Users');
|
|
1467
1509
|
await userEntity.Load(currentUserInfo.ID);
|
|
1510
|
+
// The user menu reads its own context entity (GetUserDisplayInfo), so refresh it too.
|
|
1511
|
+
this.userEntity = userEntity;
|
|
1512
|
+
this.userMenu?.UpdateContext({ userEntity });
|
|
1468
1513
|
this.applyUserAvatar(userEntity);
|
|
1469
1514
|
}
|
|
1470
1515
|
}));
|
|
@@ -1521,6 +1566,24 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1521
1566
|
// does this on construction (#4802). Non-blocking; Preload() logs and retries on demand.
|
|
1522
1567
|
this.reactAdapter.Preload();
|
|
1523
1568
|
}
|
|
1569
|
+
/**
|
|
1570
|
+
* PrepareConfiguration hook for the startup deep link: when the URL carries query params and
|
|
1571
|
+
* resolves to a tab already in the restored workspace, that tab takes the URL's params and
|
|
1572
|
+
* becomes active. Runs before any tab component mounts.
|
|
1573
|
+
*/
|
|
1574
|
+
async applyStartupDeepLink(config, url) {
|
|
1575
|
+
const urlParams = this.extractQueryParamsFromUrl(url);
|
|
1576
|
+
if (Object.keys(urlParams).length === 0 || !config.tabs?.length) {
|
|
1577
|
+
return config;
|
|
1578
|
+
}
|
|
1579
|
+
// findTabForUrl resolves /app/... URLs through the user's app list, which ApplicationManager
|
|
1580
|
+
// is still loading when workspace initialization calls this hook; resolving now would match no
|
|
1581
|
+
// app and drop the deep link. Waiting is cheap (the app list loads in parallel with the
|
|
1582
|
+
// workspace) and resolves even when loading fails.
|
|
1583
|
+
await this.appManager.WhenReady();
|
|
1584
|
+
const tab = await this.findTabForUrl(url, config.tabs);
|
|
1585
|
+
return tab ? ApplyDeepLinkToRestoredWorkspace(config, tab.id, urlParams, new Date().toISOString()) : config;
|
|
1586
|
+
}
|
|
1524
1587
|
/** @deprecated Use {@link InitializeShell}. */
|
|
1525
1588
|
async initializeShell() {
|
|
1526
1589
|
return this.InitializeShell();
|
|
@@ -1666,22 +1729,100 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1666
1729
|
// Suppress ResourceResolver for this navigation - we're just syncing the URL
|
|
1667
1730
|
// to reflect the current active tab, not requesting a new tab to be opened
|
|
1668
1731
|
this.tabService.SuppressNextResolve();
|
|
1669
|
-
|
|
1732
|
+
// Leaving a record that was deleted: replace its history entry so
|
|
1733
|
+
// browser Back can't reopen it as a blank form.
|
|
1734
|
+
const leavingRecord = this.parseRecordUrl(currentUrl);
|
|
1735
|
+
const replaceUrl = !!leavingRecord &&
|
|
1736
|
+
this.tabService.ConsumeRecordUrlReplacement(leavingRecord.EntityName, leavingRecord.RecordId);
|
|
1737
|
+
this.router.navigateByUrl(resourceUrl, { replaceUrl });
|
|
1670
1738
|
}
|
|
1671
1739
|
}
|
|
1672
1740
|
}
|
|
1673
1741
|
/**
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1742
|
+
* The record a URL shows, from either record URL shape
|
|
1743
|
+
* (`/app/:app/record/:entity/:id` or legacy `/resource/record/:entity/:id`),
|
|
1744
|
+
* or null when it is not a record URL. Query string and fragment ignored.
|
|
1745
|
+
*/
|
|
1746
|
+
parseRecordUrl(url) {
|
|
1747
|
+
const urlPath = url.split(/[?#]/)[0];
|
|
1748
|
+
const appRecordMatch = urlPath.match(/^\/app\/[^\/]+\/record\/([^\/]+)\/(.+)$/);
|
|
1749
|
+
const legacyRecordMatch = appRecordMatch ? null : urlPath.match(/^\/resource\/record\/([^\/]+)\/(.+)$/);
|
|
1750
|
+
const match = appRecordMatch ?? legacyRecordMatch;
|
|
1751
|
+
if (!match) {
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
try {
|
|
1755
|
+
return { EntityName: decodeURIComponent(match[1]), RecordId: NormalizeRecordIdForMatch(decodeURIComponent(match[2])) };
|
|
1756
|
+
}
|
|
1757
|
+
catch {
|
|
1758
|
+
return null;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Sync workspace state with the current URL: find the tab that matches it and activate that tab.
|
|
1763
|
+
*
|
|
1764
|
+
* TWO CALLERS, not one. Browser back/forward reaches it through the NavigationEnd subscriber, which
|
|
1765
|
+
* passes the time that navigation landed. The end of `InitializeShell` also calls it once, to settle
|
|
1766
|
+
* a deep-linked url against the restored workspace, and passes nothing.
|
|
1767
|
+
*
|
|
1768
|
+
* @param url the url to match a tab against.
|
|
1769
|
+
* @param navigatedAt when that url became current, as a `Date.now()` reading. It decides whether this
|
|
1770
|
+
* sync is older than the active tab's own activation; see the ordering guard below, which yields
|
|
1771
|
+
* when it is. Defaults to {@link lastNavigationAt}, which for the startup call is construction time
|
|
1772
|
+
* rather than the first navigation — that field's comment says why.
|
|
1676
1773
|
*/
|
|
1677
|
-
async syncWorkspaceWithUrl(url) {
|
|
1774
|
+
async syncWorkspaceWithUrl(url, navigatedAt = this.lastNavigationAt) {
|
|
1678
1775
|
const config = this.workspaceManager.GetConfiguration();
|
|
1679
|
-
if (!config?.tabs
|
|
1776
|
+
if (!config?.tabs) {
|
|
1680
1777
|
return;
|
|
1681
1778
|
}
|
|
1682
|
-
// Find the tab that matches this URL
|
|
1683
|
-
|
|
1779
|
+
// Find the tab that matches this URL (null when workspace has no tabs yet,
|
|
1780
|
+
// which falls through to handleMissingTabForUrl below so deep links still open).
|
|
1781
|
+
const matchingTab = config.tabs.length > 0 ? await this.findTabForUrl(url, config.tabs) : null;
|
|
1684
1782
|
if (matchingTab && matchingTab.id !== config.activeTabId) {
|
|
1783
|
+
/**
|
|
1784
|
+
* ORDERING. A url sync must not override an activation that happened AFTER this url was
|
|
1785
|
+
* current, because then the url is the stale fact, not the active tab.
|
|
1786
|
+
*
|
|
1787
|
+
* The case that motivated this: opening a new record activates its tab immediately, and the
|
|
1788
|
+
* record's own url is written a few milliseconds later. A NavigationEnd for the PREVIOUS url
|
|
1789
|
+
* is still in flight; when it resolves, findTabForUrl matches the nav tab, and switching back
|
|
1790
|
+
* to it hides the records region -- with a fully rendered form inside it -- permanently,
|
|
1791
|
+
* because the records region cannot re-activate itself once it stops being shown.
|
|
1792
|
+
*
|
|
1793
|
+
* Measured before this guard: the switch happened on every new-deal open, and whether the
|
|
1794
|
+
* user saw a form at all depended on whether it had finished rendering first. Suppressing
|
|
1795
|
+
* exactly this call took a usable form from 12/18 opens to 18/18 (Fisher p = 0.0095).
|
|
1796
|
+
*
|
|
1797
|
+
* This deliberately compares TIMES rather than tab kinds. Back/forward navigation must still
|
|
1798
|
+
* move the active tab, and it arrives here too -- but there the navigation is newer than the
|
|
1799
|
+
* activation, so it wins, which is correct. A "don't leave a record tab" guard would break it.
|
|
1800
|
+
*/
|
|
1801
|
+
const latest = this.workspaceManager.GetConfiguration();
|
|
1802
|
+
const activeTab = latest?.tabs.find(t => t.id === latest.activeTabId);
|
|
1803
|
+
const activatedAt = activeTab?.lastAccessedAt ? Date.parse(activeTab.lastAccessedAt) : 0;
|
|
1804
|
+
/**
|
|
1805
|
+
* A STAMP FROM THE FUTURE IS NOT EVIDENCE, because it did not come from this clock.
|
|
1806
|
+
*
|
|
1807
|
+
* The workspace configuration — `lastAccessedAt` included — is saved to `MJ: Workspaces` and
|
|
1808
|
+
* restored unchanged on the next load, on any device, and nothing on restore restamps the active
|
|
1809
|
+
* tab. So a tab stamped on a machine whose clock runs ahead keeps an `activatedAt` that beats
|
|
1810
|
+
* every `navigatedAt` here, and the guard would then suppress EVERY url sync until something
|
|
1811
|
+
* called `SetActiveTab` locally: back, forward and the startup deep link would all silently do
|
|
1812
|
+
* nothing until the user clicked a tab.
|
|
1813
|
+
*
|
|
1814
|
+
* Measured in review with the active tab stamped five minutes ahead: no `SetActiveTab` at all,
|
|
1815
|
+
* where `next` switched. Clamping to "not later than now" restores both cases and costs one
|
|
1816
|
+
* condition. An activation cannot legitimately be in this clock's future, so nothing real is
|
|
1817
|
+
* excluded.
|
|
1818
|
+
*/
|
|
1819
|
+
// `Number.isFinite` is belt-and-braces, not load-bearing: an unparseable `lastAccessedAt` gives
|
|
1820
|
+
// NaN, and `NaN > navigatedAt` is already false, so the guard declines to yield either way. It
|
|
1821
|
+
// stays because it says what the comparison relies on, but removing it changes no behaviour and
|
|
1822
|
+
// breaks no test — do not read it as the thing that handles NaN.
|
|
1823
|
+
if (Number.isFinite(activatedAt) && activatedAt > navigatedAt && activatedAt <= Date.now()) {
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1685
1826
|
// Activate the matching tab
|
|
1686
1827
|
this.workspaceManager.SetActiveTab(matchingTab.id);
|
|
1687
1828
|
}
|
|
@@ -1767,11 +1908,13 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1767
1908
|
this.navigationService.OpenDynamicView(entityName, extraFilter);
|
|
1768
1909
|
return;
|
|
1769
1910
|
}
|
|
1770
|
-
// Check for app-scoped dashboard URL: /app/:appName/dashboard/:dashboardId
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
this.navigationService.OpenDashboard(dashboardId,
|
|
1911
|
+
// Check for app-scoped dashboard URL: /app/:appName/dashboard/:dashboardId. The tab goes to
|
|
1912
|
+
// the URL's application, where the ResourceResolver also opens it.
|
|
1913
|
+
const openedDashboard = OpenDashboardForUrl(urlPath, {
|
|
1914
|
+
FindApp: (appPath) => this.appManager.GetAppByPath(appPath) || this.appManager.GetAppByName(appPath),
|
|
1915
|
+
OpenDashboard: (dashboardId, dashboardName, options) => this.navigationService.OpenDashboard(dashboardId, dashboardName, options),
|
|
1916
|
+
});
|
|
1917
|
+
if (openedDashboard) {
|
|
1775
1918
|
return;
|
|
1776
1919
|
}
|
|
1777
1920
|
// Check for app-scoped artifact URL: /app/:appName/artifact/:artifactId
|
|
@@ -1951,15 +2094,10 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1951
2094
|
// Check for app-scoped resource URLs (new pattern)
|
|
1952
2095
|
// Pattern: /app/:appName/:resourceType/:param1/:param2?
|
|
1953
2096
|
// Dashboard: /app/:appName/dashboard/:dashboardId
|
|
1954
|
-
const
|
|
1955
|
-
if (
|
|
1956
|
-
const
|
|
1957
|
-
return tabs.
|
|
1958
|
-
const tabConfig = tab.configuration || {};
|
|
1959
|
-
const resourceType = tabConfig['resourceType']?.toLowerCase();
|
|
1960
|
-
const tabDashboardId = (tabConfig['dashboardId'] || tabConfig['recordId'] || tab.resourceRecordId);
|
|
1961
|
-
return resourceType === 'dashboards' && tabDashboardId === dashboardId;
|
|
1962
|
-
}) || null;
|
|
2097
|
+
const appDashboardUrl = ParseAppDashboardUrl(urlPath);
|
|
2098
|
+
if (appDashboardUrl) {
|
|
2099
|
+
const app = this.appManager.GetAppByPath(appDashboardUrl.AppPath) || this.appManager.GetAppByName(appDashboardUrl.AppPath);
|
|
2100
|
+
return FindDashboardTabForUrl(tabs, appDashboardUrl.DashboardId, app?.ID);
|
|
1963
2101
|
}
|
|
1964
2102
|
// Record: /app/:appName/record/:entityName/:recordId
|
|
1965
2103
|
const appRecordMatch = urlPath.match(/^\/app\/([^\/]+)\/record\/([^\/]+)\/(.+)$/);
|
|
@@ -2051,13 +2189,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2051
2189
|
// Check for dashboard URL: /resource/dashboard/:dashboardId
|
|
2052
2190
|
const dashboardMatch = urlPath.match(/^\/resource\/dashboard\/(.+)$/);
|
|
2053
2191
|
if (dashboardMatch) {
|
|
2054
|
-
|
|
2055
|
-
return tabs.find(tab => {
|
|
2056
|
-
const tabConfig = tab.configuration || {};
|
|
2057
|
-
const resourceType = tabConfig['resourceType']?.toLowerCase();
|
|
2058
|
-
const tabDashboardId = (tabConfig['dashboardId'] || tabConfig['recordId'] || tab.resourceRecordId);
|
|
2059
|
-
return resourceType === 'dashboards' && tabDashboardId === dashboardId;
|
|
2060
|
-
}) || null;
|
|
2192
|
+
return FindDashboardTabForUrl(tabs, dashboardMatch[1]);
|
|
2061
2193
|
}
|
|
2062
2194
|
// Check for artifact URL: /resource/artifact/:artifactId
|
|
2063
2195
|
const artifactMatch = urlPath.match(/^\/resource\/artifact\/(.+)$/);
|
|
@@ -4056,7 +4188,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
4056
4188
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.AppAccessDialog = _t.first);
|
|
4057
4189
|
} }, hostBindings: function ShellComponent_HostBindings(rf, ctx) { if (rf & 1) {
|
|
4058
4190
|
i0.ɵɵlistener("keydown", function ShellComponent_keydown_HostBindingHandler($event) { return ctx.OnGlobalKeydown($event); }, i0.ɵɵresolveDocument)("click", function ShellComponent_click_HostBindingHandler() { return ctx.onDocumentClickCloseActivity(); }, i0.ɵɵresolveDocument);
|
|
4059
|
-
} }, 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) {
|
|
4191
|
+
} }, 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", "mjSharePanel", "Main content", "mjSharePanelIcon", "fa-solid fa-table-cells-large", 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) {
|
|
4060
4192
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
4061
4193
|
i0.ɵɵelementStart(0, "div", 6)(1, "a", 7);
|
|
4062
4194
|
i0.ɵɵlistener("click", function ShellComponent_Template_a_click_1_listener($event) { return ctx.skipToMainContent($event); });
|
|
@@ -4182,11 +4314,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
4182
4314
|
i0.ɵɵconditional(ctx.ShowSearchBar && !ctx.UseOmnibar ? 53 : -1);
|
|
4183
4315
|
i0.ɵɵadvance();
|
|
4184
4316
|
i0.ɵɵconditional(ctx.PinProgressVisible ? 54 : -1);
|
|
4185
|
-
} }, dependencies: [i13.NgControlStatus, i13.NgModel, i14.MJDropdownComponent, i15.LoadingComponent, i16.SearchOverlayComponent, i16.SearchCompositeComponent, i17.RecordsHubPillComponent, i18.AppSwitcherComponent, i19.AppNavComponent, i20.TabContainerComponent, i21.AppAccessDialogComponent, i22.CommandPaletteComponent, i23.OmnibarPaletteComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n\n\n\n\n.mj-logo[_ngcontent-%COMP%] {\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\n\n\n.mj-skip-link[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:focus, \n.mj-skip-link[_ngcontent-%COMP%]: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\n\nmj-tab-container[_ngcontent-%COMP%]:focus {\n outline: none;\n}\n\n.shell-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n\n\n.shell-container.hidden[_ngcontent-%COMP%] {\n visibility: hidden;\n position: absolute;\n pointer-events: none;\n}\n\nmj-tab-container[_ngcontent-%COMP%] {\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\n\n\n\nmj-tab-container.hide-tab-bar[_ngcontent-%COMP%] .main-region .lm_header {\n display: none;\n}\n\n\nmj-tab-container[_ngcontent-%COMP%]:not(.hide-tab-bar) .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\nmj-tab-container.hide-tab-bar[_ngcontent-%COMP%] .main-region .lm_content {\n height: 100% !important;\n}\n\n\nmj-tab-container[_ngcontent-%COMP%] .lm_stack {\n transition: all 0.2s ease-in-out;\n}\n\n.shell-header[_ngcontent-%COMP%] {\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 \n\n position: relative;\n z-index: 500;\n}\n\n\n.nav-bar-apps[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-1);\n}\n\n.nav-bar-apps.left-of-switcher[_ngcontent-%COMP%] {\n \n \n}\n\n.nav-bar-apps.left-of-user-menu[_ngcontent-%COMP%] {\n margin-right: var(--mj-space-2);\n}\n\n.nav-bar-app-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n\n\n\n.nav-bar-app-btn.active[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n transition: transform var(--mj-transition-fast);\n margin-top: var(--mj-space-0-5); \n}\n\n.nav-bar-app-btn[_ngcontent-%COMP%]:hover i[_ngcontent-%COMP%] {\n transform: scale(1.1);\n}\n\n.spacer[_ngcontent-%COMP%] {\n flex: 1;\n}\n\n\n\n\n\n\n\n@media (min-width: 769px) {\n mj-app-nav.desktop-nav[_ngcontent-%COMP%] {\n flex: 1 1 auto;\n min-width: 0;\n }\n mj-app-nav.desktop-nav[_ngcontent-%COMP%] + .spacer[_ngcontent-%COMP%] {\n flex: 0 0 0px;\n }\n}\n\n.header-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n}\n\n.icon-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n\n\n.nav-bar-app-btn[_ngcontent-%COMP%]:focus-visible, \n.icon-btn[_ngcontent-%COMP%]:focus-visible, \n.hamburger-btn[_ngcontent-%COMP%]:focus-visible, \n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n\n\n\n\n\n.search-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]: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[_ngcontent-%COMP%]: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\n\n.search-btn-mobile[_ngcontent-%COMP%] {\n display: none;\n}\n\n\n\n.shell-search-bar[_ngcontent-%COMP%] {\n position: relative;\n width: 260px;\n z-index: 99999;\n}\n\n\n\n\n.shell-search-bar[_ngcontent-%COMP%] .suggest-dropdown {\n left: auto;\n right: 0;\n width: min(560px, calc(100vw - var(--mj-space-6)));\n}\n\n.search-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-sm);\n flex-shrink: 0;\n}\n\n.search-btn[_ngcontent-%COMP%] .search-btn-text[_ngcontent-%COMP%] {\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\n\n\n.search-btn[_ngcontent-%COMP%] .search-btn-kbd[_ngcontent-%COMP%] {\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\n.notification-btn[_ngcontent-%COMP%] {\n position: relative;\n}\n\n.notification-badge[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\n position: relative;\n}\n\n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%]:hover {\n transform: scale(1.05);\n}\n\n\n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%] .icon-fallback[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%]:hover .icon-fallback[_ngcontent-%COMP%] {\n border-color: var(--mj-brand-primary);\n}\n\n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%] .icon-fallback[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: var(--mj-text-base);\n}\n\n\n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%] .avatar-img[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%]:hover .avatar-img[_ngcontent-%COMP%] {\n box-shadow: var(--mj-shadow-md);\n}\n\n\n.user-menu-header[_ngcontent-%COMP%] {\n padding: var(--mj-space-3) var(--mj-space-4);\n background: var(--mj-bg-surface-sunken);\n}\n\n.user-menu-header[_ngcontent-%COMP%] .user-info[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-0-5);\n}\n\n.user-menu-header[_ngcontent-%COMP%] .user-name[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .user-email[_ngcontent-%COMP%] {\n font-size: var(--mj-text-xs);\n color: var(--mj-text-muted);\n}\n\n.user-menu-header[_ngcontent-%COMP%] .user-subtitle[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n\n.user-context-menu[_ngcontent-%COMP%] {\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 \n\n\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.user-menu-item.danger[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n.user-menu-item.danger[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n.user-menu-item.danger[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface-elevated));\n}\n\n.user-menu-divider[_ngcontent-%COMP%] {\n height: 1px;\n background: var(--mj-border-default);\n margin: var(--mj-space-1) 0;\n}\n\n\n.user-menu-item.disabled[_ngcontent-%COMP%] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.user-menu-item.disabled[_ngcontent-%COMP%]:hover {\n background: transparent;\n}\n\n\n.theme-toggle-item[_ngcontent-%COMP%] {\n cursor: pointer;\n}\n\n.theme-toggle-track[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\n background: var(--mj-brand-primary);\n}\n\n.theme-toggle-thumb[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .theme-toggle-thumb[_ngcontent-%COMP%] {\n transform: translateX(18px);\n}\n\n.theme-toggle-icon[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-muted);\n}\n\n.theme-toggle-track.dark[_ngcontent-%COMP%] .theme-toggle-icon[_ngcontent-%COMP%] {\n color: var(--mj-text-inverse);\n}\n\n\n.user-menu-item[_ngcontent-%COMP%] .menu-shortcut[_ngcontent-%COMP%] {\n margin-left: auto;\n font-size: 11px;\n color: var(--mj-text-disabled);\n font-family: monospace;\n}\n\n\n.user-context-menu.menu-fade[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_menuFadeIn 0.15s ease-out;\n}\n\n.user-context-menu.menu-slide[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_menuSlideIn 0.2s ease-out;\n}\n\n@keyframes _ngcontent-%COMP%_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 _ngcontent-%COMP%_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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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\n.loading-reset-panel[_ngcontent-%COMP%] {\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: _ngcontent-%COMP%_resetPanelFadeIn 0.4s ease-out;\n}\n\n.loading-reset-message[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-lg);\n color: var(--mj-status-warning);\n}\n\n.loading-reset-hint[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]: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[_ngcontent-%COMP%]: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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-sm);\n}\n\n@keyframes _ngcontent-%COMP%_resetPanelFadeIn {\n from {\n opacity: 0;\n transform: translateY(12px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n\n.hamburger-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n\n.mobile-nav-overlay[_ngcontent-%COMP%] {\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\n\n\n\n\n\n\n.mobile-nav-drawer[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\n transform: translateX(0);\n visibility: visible;\n transition: transform var(--mj-transition-slow), visibility 0s linear 0s;\n}\n\n\n@media (prefers-reduced-motion: reduce) {\n .mobile-nav-drawer[_ngcontent-%COMP%], \n .mobile-nav-drawer.open[_ngcontent-%COMP%] {\n transition: none;\n }\n}\n\n.mobile-nav-header[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .close-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .close-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-active);\n}\n\n.mobile-nav-content[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n padding: var(--mj-space-4) 0;\n}\n\n.mobile-nav-section-title[_ngcontent-%COMP%] {\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\n\n\n.mobile-nav-records[_ngcontent-%COMP%] {\n padding: var(--mj-space-2) var(--mj-space-3);\n border-top: 1px solid var(--mj-border-default);\n}\n.mobile-nav-records[_ngcontent-%COMP%] .records-pill[_ngcontent-%COMP%] {\n width: 100%;\n min-height: 48px; \n justify-content: flex-start;\n}\n\n.mobile-nav-footer[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n.mobile-nav-action[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-base);\n width: 20px;\n text-align: center;\n}\n\n\n@media (max-width: 768px) {\n .hamburger-btn[_ngcontent-%COMP%] {\n display: flex;\n }\n\n .desktop-nav[_ngcontent-%COMP%] {\n display: none !important;\n }\n\n .desktop-only[_ngcontent-%COMP%] {\n display: none !important;\n }\n\n .mobile-nav-overlay[_ngcontent-%COMP%] {\n display: block;\n }\n\n .mobile-nav-drawer[_ngcontent-%COMP%] {\n display: flex;\n }\n\n .shell-header[_ngcontent-%COMP%] {\n padding: 0 var(--mj-space-3);\n gap: var(--mj-space-2);\n }\n\n \n\n\n .header-actions[_ngcontent-%COMP%] {\n flex-direction: row;\n flex-wrap: nowrap;\n }\n .header-actions[_ngcontent-%COMP%] .user-menu[_ngcontent-%COMP%], \n .header-actions[_ngcontent-%COMP%] .nav-bar-apps[_ngcontent-%COMP%], \n .header-actions[_ngcontent-%COMP%] .search-btn[_ngcontent-%COMP%] {\n flex-shrink: 0;\n }\n\n \n\n\n\n .search-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-lg);\n }\n .search-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n border: none;\n }\n}\n\n\n .settings-fullscreen-window {\n \n box-shadow: none !important;\n border: none !important;\n}\n\n .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 .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 .settings-fullscreen-window .k-window-titlebar-actions {\n gap: var(--mj-space-1);\n}\n\n .settings-fullscreen-window .k-window-titlebar-action {\n width: 32px;\n height: 32px;\n border-radius: var(--mj-radius-md);\n}\n\n .settings-fullscreen-window .k-window-titlebar-action:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n .settings-fullscreen-window .k-window-content {\n padding: 0;\n overflow: auto;\n background: var(--mj-bg-page);\n}\n\n\n .settings-fullscreen-window .k-window-titlebar-action[title=\"Minimize\"], \n .settings-fullscreen-window .k-window-titlebar-action[title=\"Maximize\"], \n .settings-fullscreen-window .k-window-titlebar-action .k-i-window-minimize, \n .settings-fullscreen-window .k-window-titlebar-action .k-i-window-maximize, \n .settings-fullscreen-window .k-window-titlebar-action .k-i-window {\n display: none !important;\n}\n\n\n\n\n\n\n\n\n\n.search-popup-overlay[_ngcontent-%COMP%] {\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: _ngcontent-%COMP%_fadeIn 0.15s ease;\n}\n\n@keyframes _ngcontent-%COMP%_fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.search-popup[_ngcontent-%COMP%] {\n position: absolute;\n top: 60px; \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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%], \n .search-popup.open[_ngcontent-%COMP%] {\n transition: none;\n }\n}\n\n.search-popup-content[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\n min-width: 140px;\n max-width: 180px;\n flex-shrink: 0;\n}\n\n.search-input[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]: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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary-hover);\n}\n\n.search-submit-btn[_ngcontent-%COMP%]:active {\n background: var(--mj-brand-primary-hover);\n}\n\n\n.notification-badge-mobile[_ngcontent-%COMP%] {\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\n@media (max-width: 768px) {\n .search-popup[_ngcontent-%COMP%] {\n top: 60px;\n left: 16px;\n right: 16px;\n width: auto;\n max-width: none;\n }\n\n .search-popup-content[_ngcontent-%COMP%] {\n flex-wrap: wrap;\n }\n\n .search-entity-dropdown[_ngcontent-%COMP%] {\n width: 100%;\n }\n\n .search-input[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n }\n}\n\n\n\n\n.pin-progress-backdrop[_ngcontent-%COMP%] {\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: _ngcontent-%COMP%_pinBackdropIn 0.2s ease;\n}\n\n@keyframes _ngcontent-%COMP%_pinBackdropIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.pin-progress-modal[_ngcontent-%COMP%] {\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: _ngcontent-%COMP%_pinModalIn 0.25s ease;\n}\n\n@keyframes _ngcontent-%COMP%_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[_ngcontent-%COMP%] {\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\n\n.activity-wrap[_ngcontent-%COMP%] { position: relative; display: inline-flex; }\n.activity-btn[_ngcontent-%COMP%] { position: relative; }\n.activity-btn[_ngcontent-%COMP%] .activity-pulse[_ngcontent-%COMP%] { color: var(--mj-brand-primary); }\n.activity-badge[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover { color: var(--mj-text-primary); background: var(--mj-bg-surface-hover); }\n.activity-list[_ngcontent-%COMP%] { overflow-y: auto; padding: var(--mj-space-1-5); }\n.activity-row[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover { background: var(--mj-bg-surface-hover); }\n.activity-row-icon[_ngcontent-%COMP%] { margin-top: var(--mj-space-0-5); color: var(--mj-brand-primary); font-size: 0.85rem; }\n.activity-row.is-error[_ngcontent-%COMP%] .activity-row-icon[_ngcontent-%COMP%] { color: var(--mj-status-error); }\n.activity-row-body[_ngcontent-%COMP%] { flex: 1; min-width: 0; }\n.activity-row-label[_ngcontent-%COMP%] { font-size: 0.82rem; font-weight: var(--mj-font-semibold); color: var(--mj-text-primary); }\n.activity-row-detail[_ngcontent-%COMP%] { font-size: 0.72rem; color: var(--mj-text-muted); margin-top: var(--mj-space-px); }\n.activity-bar[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] { height: 100%; background: var(--mj-brand-primary); transition: width var(--mj-transition-slow); }"] });
|
|
4317
|
+
} }, dependencies: [i13.NgControlStatus, i13.NgModel, i14.MJDropdownComponent, i15.LoadingComponent, i16.SearchOverlayComponent, i16.SearchCompositeComponent, i17.RecordsHubPillComponent, i18.SharePanelDirective, i19.AppSwitcherComponent, i20.AppNavComponent, i21.TabContainerComponent, i22.AppAccessDialogComponent, i23.CommandPaletteComponent, i24.OmnibarPaletteComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n width: 100%;\n overflow: hidden;\n}\n\n\n\n\n\n.mj-logo[_ngcontent-%COMP%] {\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\n\n\n.mj-skip-link[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:focus, \n.mj-skip-link[_ngcontent-%COMP%]: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\n\nmj-tab-container[_ngcontent-%COMP%]:focus {\n outline: none;\n}\n\n.shell-container[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n\n\n.shell-container.hidden[_ngcontent-%COMP%] {\n visibility: hidden;\n position: absolute;\n pointer-events: none;\n}\n\nmj-tab-container[_ngcontent-%COMP%] {\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\n\n\n\nmj-tab-container.hide-tab-bar[_ngcontent-%COMP%] .main-region .lm_header {\n display: none;\n}\n\n\nmj-tab-container[_ngcontent-%COMP%]:not(.hide-tab-bar) .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\nmj-tab-container.hide-tab-bar[_ngcontent-%COMP%] .main-region .lm_content {\n height: 100% !important;\n}\n\n\n\n\nmj-tab-container[_ngcontent-%COMP%] :is(.gl-container, .records-gl-container) .lm_stack:not(.lm_content .lm_stack) {\n transition: all 0.2s ease-in-out;\n}\n\n.shell-header[_ngcontent-%COMP%] {\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 \n\n position: relative;\n z-index: 500;\n}\n\n\n.nav-bar-apps[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-1);\n}\n\n.nav-bar-apps.left-of-switcher[_ngcontent-%COMP%] {\n \n \n}\n\n.nav-bar-apps.left-of-user-menu[_ngcontent-%COMP%] {\n margin-right: var(--mj-space-2);\n}\n\n.nav-bar-app-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n\n\n\n.nav-bar-app-btn.active[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n transition: transform var(--mj-transition-fast);\n margin-top: var(--mj-space-0-5); \n}\n\n.nav-bar-app-btn[_ngcontent-%COMP%]:hover i[_ngcontent-%COMP%] {\n transform: scale(1.1);\n}\n\n.spacer[_ngcontent-%COMP%] {\n flex: 1;\n}\n\n\n\n\n\n\n\n@media (min-width: 769px) {\n mj-app-nav.desktop-nav[_ngcontent-%COMP%] {\n flex: 1 1 auto;\n min-width: 0;\n }\n mj-app-nav.desktop-nav[_ngcontent-%COMP%] + .spacer[_ngcontent-%COMP%] {\n flex: 0 0 0px;\n }\n}\n\n.header-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n}\n\n.icon-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n\n\n.nav-bar-app-btn[_ngcontent-%COMP%]:focus-visible, \n.icon-btn[_ngcontent-%COMP%]:focus-visible, \n.hamburger-btn[_ngcontent-%COMP%]:focus-visible, \n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n\n\n\n\n\n.search-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]: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[_ngcontent-%COMP%]: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\n\n.search-btn-mobile[_ngcontent-%COMP%] {\n display: none;\n}\n\n\n\n.shell-search-bar[_ngcontent-%COMP%] {\n position: relative;\n width: 260px;\n z-index: 99999;\n}\n\n\n\n\n.shell-search-bar[_ngcontent-%COMP%] .suggest-dropdown {\n left: auto;\n right: 0;\n width: min(560px, calc(100vw - var(--mj-space-6)));\n}\n\n.search-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-sm);\n flex-shrink: 0;\n}\n\n.search-btn[_ngcontent-%COMP%] .search-btn-text[_ngcontent-%COMP%] {\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\n\n\n.search-btn[_ngcontent-%COMP%] .search-btn-kbd[_ngcontent-%COMP%] {\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\n.notification-btn[_ngcontent-%COMP%] {\n position: relative;\n}\n\n.notification-badge[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\n position: relative;\n}\n\n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%]:hover {\n transform: scale(1.05);\n}\n\n\n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%] .icon-fallback[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%]:hover .icon-fallback[_ngcontent-%COMP%] {\n border-color: var(--mj-brand-primary);\n}\n\n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%] .icon-fallback[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: var(--mj-text-base);\n}\n\n\n.user-menu[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%] .avatar-img[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .avatar-btn[_ngcontent-%COMP%]:hover .avatar-img[_ngcontent-%COMP%] {\n box-shadow: var(--mj-shadow-md);\n}\n\n\n.user-menu-header[_ngcontent-%COMP%] {\n padding: var(--mj-space-3) var(--mj-space-4);\n background: var(--mj-bg-surface-sunken);\n}\n\n.user-menu-header[_ngcontent-%COMP%] .user-info[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: var(--mj-space-0-5);\n}\n\n.user-menu-header[_ngcontent-%COMP%] .user-name[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .user-email[_ngcontent-%COMP%] {\n font-size: var(--mj-text-xs);\n color: var(--mj-text-muted);\n}\n\n.user-menu-header[_ngcontent-%COMP%] .user-subtitle[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n\n.user-context-menu[_ngcontent-%COMP%] {\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 \n\n\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.user-menu-item.danger[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n.user-menu-item.danger[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n.user-menu-item.danger[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface-elevated));\n}\n\n.user-menu-divider[_ngcontent-%COMP%] {\n height: 1px;\n background: var(--mj-border-default);\n margin: var(--mj-space-1) 0;\n}\n\n\n.user-menu-item.disabled[_ngcontent-%COMP%] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.user-menu-item.disabled[_ngcontent-%COMP%]:hover {\n background: transparent;\n}\n\n\n.theme-toggle-item[_ngcontent-%COMP%] {\n cursor: pointer;\n}\n\n.theme-toggle-track[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\n background: var(--mj-brand-primary);\n}\n\n.theme-toggle-thumb[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .theme-toggle-thumb[_ngcontent-%COMP%] {\n transform: translateX(18px);\n}\n\n.theme-toggle-icon[_ngcontent-%COMP%] {\n font-size: 10px;\n color: var(--mj-text-muted);\n}\n\n.theme-toggle-track.dark[_ngcontent-%COMP%] .theme-toggle-icon[_ngcontent-%COMP%] {\n color: var(--mj-text-inverse);\n}\n\n\n.user-menu-item[_ngcontent-%COMP%] .menu-shortcut[_ngcontent-%COMP%] {\n margin-left: auto;\n font-size: 11px;\n color: var(--mj-text-disabled);\n font-family: monospace;\n}\n\n\n.user-context-menu.menu-fade[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_menuFadeIn 0.15s ease-out;\n}\n\n.user-context-menu.menu-slide[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_menuSlideIn 0.2s ease-out;\n}\n\n@keyframes _ngcontent-%COMP%_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 _ngcontent-%COMP%_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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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\n.loading-reset-panel[_ngcontent-%COMP%] {\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: _ngcontent-%COMP%_resetPanelFadeIn 0.4s ease-out;\n}\n\n.loading-reset-message[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-lg);\n color: var(--mj-status-warning);\n}\n\n.loading-reset-hint[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]: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[_ngcontent-%COMP%]: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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-sm);\n}\n\n@keyframes _ngcontent-%COMP%_resetPanelFadeIn {\n from {\n opacity: 0;\n transform: translateY(12px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n\n.hamburger-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n\n.mobile-nav-overlay[_ngcontent-%COMP%] {\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\n\n\n\n\n\n\n.mobile-nav-drawer[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\n transform: translateX(0);\n visibility: visible;\n transition: transform var(--mj-transition-slow), visibility 0s linear 0s;\n}\n\n\n@media (prefers-reduced-motion: reduce) {\n .mobile-nav-drawer[_ngcontent-%COMP%], \n .mobile-nav-drawer.open[_ngcontent-%COMP%] {\n transition: none;\n }\n}\n\n.mobile-nav-header[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .close-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] .close-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-active);\n}\n\n.mobile-nav-content[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n padding: var(--mj-space-4) 0;\n}\n\n.mobile-nav-section-title[_ngcontent-%COMP%] {\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\n\n\n.mobile-nav-records[_ngcontent-%COMP%] {\n padding: var(--mj-space-2) var(--mj-space-3);\n border-top: 1px solid var(--mj-border-default);\n}\n.mobile-nav-records[_ngcontent-%COMP%] .records-pill[_ngcontent-%COMP%] {\n width: 100%;\n min-height: 48px; \n justify-content: flex-start;\n}\n\n.mobile-nav-footer[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n.mobile-nav-action[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-base);\n width: 20px;\n text-align: center;\n}\n\n\n@media (max-width: 768px) {\n .hamburger-btn[_ngcontent-%COMP%] {\n display: flex;\n }\n\n .desktop-nav[_ngcontent-%COMP%] {\n display: none !important;\n }\n\n .desktop-only[_ngcontent-%COMP%] {\n display: none !important;\n }\n\n .mobile-nav-overlay[_ngcontent-%COMP%] {\n display: block;\n }\n\n .mobile-nav-drawer[_ngcontent-%COMP%] {\n display: flex;\n }\n\n .shell-header[_ngcontent-%COMP%] {\n padding: 0 var(--mj-space-3);\n gap: var(--mj-space-2);\n }\n\n \n\n\n .header-actions[_ngcontent-%COMP%] {\n flex-direction: row;\n flex-wrap: nowrap;\n }\n .header-actions[_ngcontent-%COMP%] .user-menu[_ngcontent-%COMP%], \n .header-actions[_ngcontent-%COMP%] .nav-bar-apps[_ngcontent-%COMP%], \n .header-actions[_ngcontent-%COMP%] .search-btn[_ngcontent-%COMP%] {\n flex-shrink: 0;\n }\n\n \n\n\n\n .search-btn[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-lg);\n }\n .search-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n border: none;\n }\n}\n\n\n .settings-fullscreen-window {\n \n box-shadow: none !important;\n border: none !important;\n}\n\n .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 .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 .settings-fullscreen-window .k-window-titlebar-actions {\n gap: var(--mj-space-1);\n}\n\n .settings-fullscreen-window .k-window-titlebar-action {\n width: 32px;\n height: 32px;\n border-radius: var(--mj-radius-md);\n}\n\n .settings-fullscreen-window .k-window-titlebar-action:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n .settings-fullscreen-window .k-window-content {\n padding: 0;\n overflow: auto;\n background: var(--mj-bg-page);\n}\n\n\n .settings-fullscreen-window .k-window-titlebar-action[title=\"Minimize\"], \n .settings-fullscreen-window .k-window-titlebar-action[title=\"Maximize\"], \n .settings-fullscreen-window .k-window-titlebar-action .k-i-window-minimize, \n .settings-fullscreen-window .k-window-titlebar-action .k-i-window-maximize, \n .settings-fullscreen-window .k-window-titlebar-action .k-i-window {\n display: none !important;\n}\n\n\n\n\n\n\n\n\n\n.search-popup-overlay[_ngcontent-%COMP%] {\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: _ngcontent-%COMP%_fadeIn 0.15s ease;\n}\n\n@keyframes _ngcontent-%COMP%_fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.search-popup[_ngcontent-%COMP%] {\n position: absolute;\n top: 60px; \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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%], \n .search-popup.open[_ngcontent-%COMP%] {\n transition: none;\n }\n}\n\n.search-popup-content[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\n min-width: 140px;\n max-width: 180px;\n flex-shrink: 0;\n}\n\n.search-input[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]: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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary-hover);\n}\n\n.search-submit-btn[_ngcontent-%COMP%]:active {\n background: var(--mj-brand-primary-hover);\n}\n\n\n.notification-badge-mobile[_ngcontent-%COMP%] {\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\n@media (max-width: 768px) {\n .search-popup[_ngcontent-%COMP%] {\n top: 60px;\n left: 16px;\n right: 16px;\n width: auto;\n max-width: none;\n }\n\n .search-popup-content[_ngcontent-%COMP%] {\n flex-wrap: wrap;\n }\n\n .search-entity-dropdown[_ngcontent-%COMP%] {\n width: 100%;\n }\n\n .search-input[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n }\n}\n\n\n\n\n.pin-progress-backdrop[_ngcontent-%COMP%] {\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: _ngcontent-%COMP%_pinBackdropIn 0.2s ease;\n}\n\n@keyframes _ngcontent-%COMP%_pinBackdropIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.pin-progress-modal[_ngcontent-%COMP%] {\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: _ngcontent-%COMP%_pinModalIn 0.25s ease;\n}\n\n@keyframes _ngcontent-%COMP%_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[_ngcontent-%COMP%] {\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\n\n.activity-wrap[_ngcontent-%COMP%] { position: relative; display: inline-flex; }\n.activity-btn[_ngcontent-%COMP%] { position: relative; }\n.activity-btn[_ngcontent-%COMP%] .activity-pulse[_ngcontent-%COMP%] { color: var(--mj-brand-primary); }\n.activity-badge[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover { color: var(--mj-text-primary); background: var(--mj-bg-surface-hover); }\n.activity-list[_ngcontent-%COMP%] { overflow-y: auto; padding: var(--mj-space-1-5); }\n.activity-row[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%]:hover { background: var(--mj-bg-surface-hover); }\n.activity-row-icon[_ngcontent-%COMP%] { margin-top: var(--mj-space-0-5); color: var(--mj-brand-primary); font-size: 0.85rem; }\n.activity-row.is-error[_ngcontent-%COMP%] .activity-row-icon[_ngcontent-%COMP%] { color: var(--mj-status-error); }\n.activity-row-body[_ngcontent-%COMP%] { flex: 1; min-width: 0; }\n.activity-row-label[_ngcontent-%COMP%] { font-size: 0.82rem; font-weight: var(--mj-font-semibold); color: var(--mj-text-primary); }\n.activity-row-detail[_ngcontent-%COMP%] { font-size: 0.72rem; color: var(--mj-text-muted); margin-top: var(--mj-space-px); }\n.activity-bar[_ngcontent-%COMP%] {\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[_ngcontent-%COMP%] { height: 100%; background: var(--mj-brand-primary); transition: width var(--mj-transition-slow); }"] });
|
|
4186
4318
|
}
|
|
4187
4319
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ShellComponent, [{
|
|
4188
4320
|
type: Component,
|
|
4189
|
-
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"] }]
|
|
4321
|
+
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. A call's Share menu offers it as \"Main content\" under\n \"This panel\", except to a call inside it. -->\n <mj-tab-container\n id=\"mj-main-content\"\n role=\"main\"\n tabindex=\"-1\"\n mjSharePanel=\"Main content\"\n mjSharePanelIcon=\"fa-solid fa-table-cells-large\"\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/* Animate the stacks of the shell's own layouts (main and records regions) only.\n A Golden Layout inside a tab's content (the dashboard viewer) sizes its own\n stacks: with this transition they grow from zero height on every rebuild. */\nmj-tab-container ::ng-deep :is(.gl-container, .records-gl-container) .lm_stack:not(.lm_content .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"] }]
|
|
4190
4322
|
}], () => [{ 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: [{
|
|
4191
4323
|
type: ViewChild,
|
|
4192
4324
|
args: ['searchInput']
|
|
@@ -4215,5 +4347,5 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
4215
4347
|
type: HostListener,
|
|
4216
4348
|
args: ['document:click']
|
|
4217
4349
|
}] }); })();
|
|
4218
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ShellComponent, { className: "ShellComponent", filePath: "src/lib/shell/shell.component.ts", lineNumber:
|
|
4350
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ShellComponent, { className: "ShellComponent", filePath: "src/lib/shell/shell.component.ts", lineNumber: 62 }); })();
|
|
4219
4351
|
//# sourceMappingURL=shell.component.js.map
|