@memberjunction/ng-explorer-core 5.49.0 → 5.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/omnibar/omnibar-palette.component.js +98 -96
- package/dist/lib/omnibar/omnibar-palette.component.js.map +1 -1
- package/dist/lib/resource-wrappers/view-resource.component.d.ts +19 -16
- package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/view-resource.component.js +127 -121
- package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.d.ts +84 -9
- package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.js +380 -41
- package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
- package/dist/lib/shell/components/header/app-switcher.component.d.ts +159 -29
- package/dist/lib/shell/components/header/app-switcher.component.d.ts.map +1 -1
- package/dist/lib/shell/components/header/app-switcher.component.js +822 -116
- package/dist/lib/shell/components/header/app-switcher.component.js.map +1 -1
- package/dist/lib/shell/services/sharing-center-dialog-host.component.d.ts +10 -1
- package/dist/lib/shell/services/sharing-center-dialog-host.component.d.ts.map +1 -1
- package/dist/lib/shell/services/sharing-center-dialog-host.component.js +30 -4
- package/dist/lib/shell/services/sharing-center-dialog-host.component.js.map +1 -1
- package/dist/lib/shell/shell.component.d.ts +3 -0
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +22 -12
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/shell/shell.module.d.ts +1 -1
- package/dist/lib/shell/shell.module.d.ts.map +1 -1
- package/dist/lib/shell/shell.module.js +3 -1
- package/dist/lib/shell/shell.module.js.map +1 -1
- package/package.json +51 -51
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
|
1
|
+
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewChild, HostListener } from '@angular/core';
|
|
2
2
|
import { Subject, takeUntil } from 'rxjs';
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
4
|
import * as i1 from "@memberjunction/ng-base-application";
|
|
5
5
|
import * as i2 from "@memberjunction/ng-shared";
|
|
6
6
|
import * as i3 from "@memberjunction/ng-ui-components";
|
|
7
|
-
|
|
7
|
+
const _c0 = ["navList"];
|
|
8
|
+
const _c1 = ["moreBtn"];
|
|
9
|
+
function AppNavComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
10
|
+
i0.ɵɵelementStart(0, "div", 3);
|
|
11
|
+
i0.ɵɵelement(1, "span", 7)(2, "span", 8)(3, "span", 9);
|
|
12
|
+
i0.ɵɵelementEnd();
|
|
13
|
+
} }
|
|
14
|
+
function AppNavComponent_For_4_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
8
15
|
i0.ɵɵelement(0, "i");
|
|
9
16
|
} if (rf & 2) {
|
|
10
17
|
const item_r2 = i0.ɵɵnextContext().$implicit;
|
|
11
18
|
i0.ɵɵclassMap(item_r2.Icon);
|
|
12
19
|
} }
|
|
13
|
-
function
|
|
14
|
-
i0.ɵɵelementStart(0, "span",
|
|
20
|
+
function AppNavComponent_For_4_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
21
|
+
i0.ɵɵelementStart(0, "span", 12);
|
|
15
22
|
i0.ɵɵtext(1);
|
|
16
23
|
i0.ɵɵelementEnd();
|
|
17
24
|
} if (rf & 2) {
|
|
@@ -19,23 +26,23 @@ function AppNavComponent_For_2_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
|
19
26
|
i0.ɵɵadvance();
|
|
20
27
|
i0.ɵɵtextInterpolate(item_r2.Badge);
|
|
21
28
|
} }
|
|
22
|
-
function
|
|
29
|
+
function AppNavComponent_For_4_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
23
30
|
const _r4 = i0.ɵɵgetCurrentView();
|
|
24
|
-
i0.ɵɵelementStart(0, "button",
|
|
25
|
-
i0.ɵɵlistener("click", function
|
|
26
|
-
i0.ɵɵelement(1, "i",
|
|
31
|
+
i0.ɵɵelementStart(0, "button", 14);
|
|
32
|
+
i0.ɵɵlistener("click", function AppNavComponent_For_4_Conditional_5_Template_button_click_0_listener($event) { i0.ɵɵrestoreView(_r4); const item_r2 = i0.ɵɵnextContext().$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onDismiss(item_r2, $event)); });
|
|
33
|
+
i0.ɵɵelement(1, "i", 15);
|
|
27
34
|
i0.ɵɵelementEnd();
|
|
28
35
|
} }
|
|
29
|
-
function
|
|
36
|
+
function AppNavComponent_For_4_Template(rf, ctx) { if (rf & 1) {
|
|
30
37
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
31
|
-
i0.ɵɵelementStart(0, "div",
|
|
32
|
-
i0.ɵɵlistener("click", function
|
|
33
|
-
i0.ɵɵconditionalCreate(1,
|
|
38
|
+
i0.ɵɵelementStart(0, "div", 10);
|
|
39
|
+
i0.ɵɵlistener("click", function AppNavComponent_For_4_Template_div_click_0_listener($event) { const item_r2 = i0.ɵɵrestoreView(_r1).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onNavClick(item_r2, $event)); });
|
|
40
|
+
i0.ɵɵconditionalCreate(1, AppNavComponent_For_4_Conditional_1_Template, 1, 2, "i", 11);
|
|
34
41
|
i0.ɵɵelementStart(2, "span");
|
|
35
42
|
i0.ɵɵtext(3);
|
|
36
43
|
i0.ɵɵelementEnd();
|
|
37
|
-
i0.ɵɵconditionalCreate(4,
|
|
38
|
-
i0.ɵɵconditionalCreate(5,
|
|
44
|
+
i0.ɵɵconditionalCreate(4, AppNavComponent_For_4_Conditional_4_Template, 2, 1, "span", 12);
|
|
45
|
+
i0.ɵɵconditionalCreate(5, AppNavComponent_For_4_Conditional_5_Template, 2, 0, "button", 13);
|
|
39
46
|
i0.ɵɵelementEnd();
|
|
40
47
|
} if (rf & 2) {
|
|
41
48
|
const item_r2 = ctx.$implicit;
|
|
@@ -52,18 +59,100 @@ function AppNavComponent_For_2_Template(rf, ctx) { if (rf & 1) {
|
|
|
52
59
|
i0.ɵɵadvance();
|
|
53
60
|
i0.ɵɵconditional(ctx_r2.isDynamic(item_r2) ? 5 : -1);
|
|
54
61
|
} }
|
|
62
|
+
function AppNavComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
63
|
+
const _r5 = i0.ɵɵgetCurrentView();
|
|
64
|
+
i0.ɵɵelementStart(0, "div", 16, 1);
|
|
65
|
+
i0.ɵɵlistener("click", function AppNavComponent_Conditional_5_Template_div_click_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.ToggleMore($event)); });
|
|
66
|
+
i0.ɵɵelement(2, "i", 17);
|
|
67
|
+
i0.ɵɵelementStart(3, "span");
|
|
68
|
+
i0.ɵɵtext(4, "More");
|
|
69
|
+
i0.ɵɵelementEnd();
|
|
70
|
+
i0.ɵɵelementStart(5, "span", 18);
|
|
71
|
+
i0.ɵɵtext(6);
|
|
72
|
+
i0.ɵɵelementEnd()();
|
|
73
|
+
} if (rf & 2) {
|
|
74
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
75
|
+
i0.ɵɵclassProp("active", ctx_r2.OverflowHasActive);
|
|
76
|
+
i0.ɵɵattribute("aria-haspopup", true)("aria-expanded", ctx_r2.MoreOpen);
|
|
77
|
+
i0.ɵɵadvance(6);
|
|
78
|
+
i0.ɵɵtextInterpolate(ctx_r2.OverflowItems.length);
|
|
79
|
+
} }
|
|
80
|
+
function AppNavComponent_Conditional_6_For_2_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
81
|
+
i0.ɵɵelement(0, "i");
|
|
82
|
+
} if (rf & 2) {
|
|
83
|
+
const item_r7 = i0.ɵɵnextContext().$implicit;
|
|
84
|
+
i0.ɵɵclassMap(item_r7.Icon);
|
|
85
|
+
} }
|
|
86
|
+
function AppNavComponent_Conditional_6_For_2_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
87
|
+
i0.ɵɵelementStart(0, "span", 12);
|
|
88
|
+
i0.ɵɵtext(1);
|
|
89
|
+
i0.ɵɵelementEnd();
|
|
90
|
+
} if (rf & 2) {
|
|
91
|
+
const item_r7 = i0.ɵɵnextContext().$implicit;
|
|
92
|
+
i0.ɵɵadvance();
|
|
93
|
+
i0.ɵɵtextInterpolate(item_r7.Badge);
|
|
94
|
+
} }
|
|
95
|
+
function AppNavComponent_Conditional_6_For_2_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
96
|
+
const _r8 = i0.ɵɵgetCurrentView();
|
|
97
|
+
i0.ɵɵelementStart(0, "button", 14);
|
|
98
|
+
i0.ɵɵlistener("click", function AppNavComponent_Conditional_6_For_2_Conditional_5_Template_button_click_0_listener($event) { i0.ɵɵrestoreView(_r8); const item_r7 = i0.ɵɵnextContext().$implicit; const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.onDismiss(item_r7, $event)); });
|
|
99
|
+
i0.ɵɵelement(1, "i", 15);
|
|
100
|
+
i0.ɵɵelementEnd();
|
|
101
|
+
} }
|
|
102
|
+
function AppNavComponent_Conditional_6_For_2_Template(rf, ctx) { if (rf & 1) {
|
|
103
|
+
const _r6 = i0.ɵɵgetCurrentView();
|
|
104
|
+
i0.ɵɵelementStart(0, "div", 21);
|
|
105
|
+
i0.ɵɵlistener("click", function AppNavComponent_Conditional_6_For_2_Template_div_click_0_listener($event) { const item_r7 = i0.ɵɵrestoreView(_r6).$implicit; const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.onNavClick(item_r7, $event)); });
|
|
106
|
+
i0.ɵɵconditionalCreate(1, AppNavComponent_Conditional_6_For_2_Conditional_1_Template, 1, 2, "i", 11);
|
|
107
|
+
i0.ɵɵelementStart(2, "span", 22);
|
|
108
|
+
i0.ɵɵtext(3);
|
|
109
|
+
i0.ɵɵelementEnd();
|
|
110
|
+
i0.ɵɵconditionalCreate(4, AppNavComponent_Conditional_6_For_2_Conditional_4_Template, 2, 1, "span", 12);
|
|
111
|
+
i0.ɵɵconditionalCreate(5, AppNavComponent_Conditional_6_For_2_Conditional_5_Template, 2, 0, "button", 13);
|
|
112
|
+
i0.ɵɵelementEnd();
|
|
113
|
+
} if (rf & 2) {
|
|
114
|
+
const item_r7 = ctx.$implicit;
|
|
115
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
116
|
+
i0.ɵɵclassProp("active", ctx_r2.isActive(item_r7))("dynamic", ctx_r2.isDynamic(item_r7));
|
|
117
|
+
i0.ɵɵproperty("mjClickable", item_r7.Label)("testId", "nav-more-item-" + item_r7.Label);
|
|
118
|
+
i0.ɵɵattribute("aria-current", ctx_r2.isActive(item_r7) ? "page" : null);
|
|
119
|
+
i0.ɵɵadvance();
|
|
120
|
+
i0.ɵɵconditional(item_r7.Icon ? 1 : -1);
|
|
121
|
+
i0.ɵɵadvance(2);
|
|
122
|
+
i0.ɵɵtextInterpolate(item_r7.Label);
|
|
123
|
+
i0.ɵɵadvance();
|
|
124
|
+
i0.ɵɵconditional(item_r7.Badge ? 4 : -1);
|
|
125
|
+
i0.ɵɵadvance();
|
|
126
|
+
i0.ɵɵconditional(ctx_r2.isDynamic(item_r7) ? 5 : -1);
|
|
127
|
+
} }
|
|
128
|
+
function AppNavComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
129
|
+
i0.ɵɵelementStart(0, "div", 19);
|
|
130
|
+
i0.ɵɵrepeaterCreate(1, AppNavComponent_Conditional_6_For_2_Template, 6, 11, "div", 20, i0.ɵɵcomponentInstance().trackByNavItem, true);
|
|
131
|
+
i0.ɵɵelementEnd();
|
|
132
|
+
} if (rf & 2) {
|
|
133
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
134
|
+
i0.ɵɵstyleProp("left", ctx_r2.MoreDropdownLeft, "px");
|
|
135
|
+
i0.ɵɵadvance();
|
|
136
|
+
i0.ɵɵrepeater(ctx_r2.OverflowItems);
|
|
137
|
+
} }
|
|
55
138
|
/**
|
|
56
139
|
* Horizontal navigation items for the current app.
|
|
57
140
|
* Uses OnPush change detection and reactive state management for optimal performance.
|
|
141
|
+
*
|
|
142
|
+
* Overflow behavior (priority+ pattern): when the header can't fit every nav item,
|
|
143
|
+
* trailing items collapse into a "More" dropdown instead of squeezing the header's
|
|
144
|
+
* action cluster off-screen. Item widths are measured once per nav-items change
|
|
145
|
+
* (all items render for one frame, widths are cached), then every resize recomputes
|
|
146
|
+
* the fit from the cache — no re-measure, no layout thrash.
|
|
58
147
|
*/
|
|
59
148
|
export class AppNavComponent {
|
|
60
149
|
workspaceManager;
|
|
61
150
|
sharedService;
|
|
62
151
|
cdr;
|
|
152
|
+
host;
|
|
63
153
|
destroy$ = new Subject();
|
|
64
154
|
_app = null;
|
|
65
155
|
_cachedNavItems = [];
|
|
66
|
-
_cachedAppColor = 'var(--mj-brand-primary)';
|
|
67
156
|
_servicesInjected = false;
|
|
68
157
|
/**
|
|
69
158
|
* Monotonically increasing counter used to detect and discard stale async results.
|
|
@@ -82,12 +171,49 @@ export class AppNavComponent {
|
|
|
82
171
|
_updateGeneration = 0;
|
|
83
172
|
// Map of nav item key (Route or Label) to active state
|
|
84
173
|
activeStateMap = new Map();
|
|
174
|
+
// ---- Overflow (priority+) state ----
|
|
175
|
+
/** Cached natural pixel width of each nav item, parallel to navItems order */
|
|
176
|
+
itemWidths = [];
|
|
177
|
+
/** Measured width of the More button (fallback used until first render) */
|
|
178
|
+
moreBtnWidth = 120;
|
|
179
|
+
/** Left offset (px, relative to host) where the More dropdown anchors */
|
|
180
|
+
MoreDropdownLeft = 0;
|
|
181
|
+
/** True when even one item + More can't fit — the lone visible pill
|
|
182
|
+
* shrinks with an ellipsis instead of clipping the More button. */
|
|
183
|
+
Tight = false;
|
|
184
|
+
/** Flex gap between nav items — measured from the rendered row (falls back
|
|
185
|
+
* to --mj-space-1's default of 4px until first measurement) */
|
|
186
|
+
itemGap = 4;
|
|
187
|
+
/** How many leading nav items are currently visible inline */
|
|
188
|
+
VisibleCount = Number.MAX_SAFE_INTEGER;
|
|
189
|
+
/** Whether the More dropdown is open */
|
|
190
|
+
MoreOpen = false;
|
|
191
|
+
/** True while nav items are being (re)loaded for skeleton display */
|
|
192
|
+
Loading = false;
|
|
193
|
+
resizeObserver = null;
|
|
194
|
+
_overflowEnabled = true;
|
|
195
|
+
navListRef;
|
|
196
|
+
moreBtnRef;
|
|
85
197
|
navItemClick = new EventEmitter();
|
|
86
198
|
navItemDismiss = new EventEmitter();
|
|
87
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Disable the priority+ overflow behavior. Used by the mobile drawer instance,
|
|
201
|
+
* where items stack vertically and can never overflow horizontally.
|
|
202
|
+
*/
|
|
203
|
+
set OverflowEnabled(value) {
|
|
204
|
+
this._overflowEnabled = value;
|
|
205
|
+
if (!value) {
|
|
206
|
+
this.VisibleCount = Number.MAX_SAFE_INTEGER;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
get OverflowEnabled() {
|
|
210
|
+
return this._overflowEnabled;
|
|
211
|
+
}
|
|
212
|
+
constructor(workspaceManager, sharedService, cdr, host) {
|
|
88
213
|
this.workspaceManager = workspaceManager;
|
|
89
214
|
this.sharedService = sharedService;
|
|
90
215
|
this.cdr = cdr;
|
|
216
|
+
this.host = host;
|
|
91
217
|
}
|
|
92
218
|
/**
|
|
93
219
|
* Input setter for app - triggers cache update when app changes
|
|
@@ -98,6 +224,7 @@ export class AppNavComponent {
|
|
|
98
224
|
this._cachedNavItems = []; // Clear stale items immediately so previous app's items don't flash
|
|
99
225
|
this.activeStateMap.clear();
|
|
100
226
|
this._servicesInjected = false; // Reset injection flag
|
|
227
|
+
this.Loading = value != null; // Skeleton until GetNavItems resolves
|
|
101
228
|
this.updateCachedData();
|
|
102
229
|
this.cdr.markForCheck();
|
|
103
230
|
}
|
|
@@ -117,7 +244,23 @@ export class AppNavComponent {
|
|
|
117
244
|
await this.updateCachedData();
|
|
118
245
|
});
|
|
119
246
|
}
|
|
247
|
+
ngAfterViewInit() {
|
|
248
|
+
if (this._overflowEnabled && typeof ResizeObserver !== 'undefined') {
|
|
249
|
+
this.resizeObserver = new ResizeObserver(() => this.recomputeFit());
|
|
250
|
+
this.resizeObserver.observe(this.host.nativeElement);
|
|
251
|
+
}
|
|
252
|
+
// Widths measured before webfonts arrive are stale (Explorer's Montserrat
|
|
253
|
+
// loads late over the fallback stack) — invalidate the cache once fonts
|
|
254
|
+
// settle so the fit is computed against real glyph metrics.
|
|
255
|
+
if (this._overflowEnabled && typeof document !== 'undefined' && document.fonts?.ready) {
|
|
256
|
+
document.fonts.ready.then(() => {
|
|
257
|
+
this.itemWidths = [];
|
|
258
|
+
this.recomputeFit();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
120
262
|
ngOnDestroy() {
|
|
263
|
+
this.resizeObserver?.disconnect();
|
|
121
264
|
this.destroy$.next();
|
|
122
265
|
this.destroy$.complete();
|
|
123
266
|
}
|
|
@@ -148,27 +291,197 @@ export class AppNavComponent {
|
|
|
148
291
|
}
|
|
149
292
|
// Only show items with Status 'Active' or undefined (default to Active)
|
|
150
293
|
this._cachedNavItems = items.filter(item => !item.Status || item.Status === 'Active');
|
|
151
|
-
this._cachedAppColor = this._app.GetColor() || 'var(--mj-brand-primary)';
|
|
152
294
|
}
|
|
153
295
|
else {
|
|
154
296
|
this._cachedNavItems = [];
|
|
155
|
-
this._cachedAppColor = 'var(--mj-brand-primary)';
|
|
156
297
|
}
|
|
298
|
+
this.Loading = false;
|
|
157
299
|
// Update active states after nav items change
|
|
158
300
|
const config = this.workspaceManager.GetConfiguration();
|
|
159
301
|
this.updateActiveStates(config);
|
|
302
|
+
// New item set: show everything for one frame so natural widths can be
|
|
303
|
+
// measured (the row clips, so the temporary full row is invisible),
|
|
304
|
+
// then collapse to fit. Tight mode MUST be off during measurement — its
|
|
305
|
+
// flex-shrink would record squeezed widths and poison the fit arithmetic.
|
|
306
|
+
this.VisibleCount = Number.MAX_SAFE_INTEGER;
|
|
307
|
+
this.MoreOpen = false;
|
|
308
|
+
this.Tight = false;
|
|
309
|
+
this.syncChangeDetection();
|
|
310
|
+
if (this._overflowEnabled) {
|
|
311
|
+
requestAnimationFrame(() => {
|
|
312
|
+
this.measureItemWidths();
|
|
313
|
+
this.recomputeFit();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Cache the natural width of every nav item. Called in the frame after a new
|
|
319
|
+
* item set renders (all items visible, flex-shrink: 0, so widths are natural
|
|
320
|
+
* even if the row is clipped by overflow: hidden).
|
|
321
|
+
*/
|
|
322
|
+
measureItemWidths() {
|
|
323
|
+
const list = this.navListRef?.nativeElement;
|
|
324
|
+
if (!list) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const els = Array.from(list.querySelectorAll('.nav-item:not(.nav-more-btn)'));
|
|
328
|
+
if (els.length !== this._cachedNavItems.length) {
|
|
329
|
+
return; // View out of sync (mid-CD) — the next update cycle re-measures
|
|
330
|
+
}
|
|
331
|
+
this.itemWidths = els.map(el => el.offsetWidth);
|
|
332
|
+
// Measure the real flex gap (rem-based tokens can differ from the 4px
|
|
333
|
+
// fallback if the root font-size changes)
|
|
334
|
+
const gap = parseFloat(getComputedStyle(list).columnGap);
|
|
335
|
+
if (!Number.isNaN(gap)) {
|
|
336
|
+
this.itemGap = gap;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Decide how many items fit in the host's current width; the rest collapse
|
|
341
|
+
* into the More dropdown. Pure arithmetic over cached widths — safe to call
|
|
342
|
+
* on every resize.
|
|
343
|
+
*/
|
|
344
|
+
recomputeFit() {
|
|
345
|
+
if (!this._overflowEnabled || this._cachedNavItems.length === 0) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const hostWidth = this.host.nativeElement.clientWidth;
|
|
349
|
+
if (hostWidth === 0) {
|
|
350
|
+
return; // Hidden (mobile breakpoint) — leave state alone
|
|
351
|
+
}
|
|
352
|
+
if (!this.ensureFreshWidthCache()) {
|
|
353
|
+
return; // Cache still stale — a retry frame is already scheduled
|
|
354
|
+
}
|
|
355
|
+
if (this.moreBtnRef?.nativeElement) {
|
|
356
|
+
this.moreBtnWidth = this.moreBtnRef.nativeElement.offsetWidth || this.moreBtnWidth;
|
|
357
|
+
}
|
|
358
|
+
const fit = this.computeFitCount(hostWidth);
|
|
359
|
+
this.applyFit(fit.count, fit.tight);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Self-heal for the item-width cache: if it's stale or missing (the
|
|
363
|
+
* measurement frame can race the first render after a reload/app switch),
|
|
364
|
+
* expand to the full item set, re-measure, and retry next frame until it
|
|
365
|
+
* converges. Returns true when the cache matches the current item set.
|
|
366
|
+
*/
|
|
367
|
+
ensureFreshWidthCache() {
|
|
368
|
+
if (this.itemWidths.length === this._cachedNavItems.length) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
if (this.VisibleCount < this._cachedNavItems.length || this.Tight) {
|
|
372
|
+
this.VisibleCount = Number.MAX_SAFE_INTEGER;
|
|
373
|
+
this.Tight = false; // Never measure with tight-mode shrink applied
|
|
374
|
+
this.syncChangeDetection();
|
|
375
|
+
}
|
|
376
|
+
this.measureItemWidths();
|
|
377
|
+
if (this.itemWidths.length !== this._cachedNavItems.length) {
|
|
378
|
+
requestAnimationFrame(() => this.recomputeFit());
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
/** Pure fit arithmetic: how many leading items fit beside the More button */
|
|
384
|
+
computeFitCount(hostWidth) {
|
|
385
|
+
const gap = this.itemGap;
|
|
386
|
+
const n = this.itemWidths.length;
|
|
387
|
+
const totalAll = this.itemWidths.reduce((a, b) => a + b, 0) + gap * (n - 1);
|
|
388
|
+
if (totalAll <= hostWidth) {
|
|
389
|
+
return { count: Number.MAX_SAFE_INTEGER, tight: false }; // Everything fits
|
|
390
|
+
}
|
|
391
|
+
// Reserve room for the More button, then fit as many leading items as possible
|
|
392
|
+
let used = this.moreBtnWidth;
|
|
393
|
+
let fit = 0;
|
|
394
|
+
for (let i = 0; i < n; i++) {
|
|
395
|
+
const next = used + gap + this.itemWidths[i];
|
|
396
|
+
if (next > hostWidth) {
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
used = next;
|
|
400
|
+
fit++;
|
|
401
|
+
}
|
|
402
|
+
// Never collapse below one visible item; tight = even the lone item + More overflows
|
|
403
|
+
return { count: Math.max(1, fit), tight: fit === 0 };
|
|
404
|
+
}
|
|
405
|
+
/** Commit a fit result, closing More if nothing overflows; CD only on change */
|
|
406
|
+
applyFit(count, tight) {
|
|
407
|
+
if (count === this.VisibleCount && tight === this.Tight) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.VisibleCount = count;
|
|
411
|
+
this.Tight = tight;
|
|
412
|
+
if (this.OverflowItems.length === 0) {
|
|
413
|
+
this.MoreOpen = false;
|
|
414
|
+
}
|
|
415
|
+
this.syncChangeDetection();
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* markForCheck + synchronous detectChanges. In Angular 21 zoneless mode,
|
|
419
|
+
* markForCheck() alone is unreliable when the trigger is an RxJS
|
|
420
|
+
* subscription or rAF callback not tracked by the zoneless scheduler — the
|
|
421
|
+
* dirty flag is set but no follow-up tick is scheduled. detectChanges()
|
|
422
|
+
* throws if invoked re-entrantly during an in-flight CD pass; that's
|
|
423
|
+
* harmless (the in-flight pass picks up our markForCheck), hence the catch.
|
|
424
|
+
*/
|
|
425
|
+
syncChangeDetection() {
|
|
160
426
|
this.cdr.markForCheck();
|
|
161
|
-
// In Angular 21 zoneless mode, markForCheck() alone is unreliable when the trigger
|
|
162
|
-
// is an RxJS subscription (workspaceManager.Configuration here) not tracked by the
|
|
163
|
-
// zoneless scheduler — the dirty flag is set but no follow-up tick is scheduled.
|
|
164
|
-
// detectChanges() runs CD synchronously on this view, rendering the new data
|
|
165
|
-
// immediately. Wrapped because detectChanges throws if invoked re-entrantly during
|
|
166
|
-
// another in-flight CD pass — harmless if so.
|
|
167
427
|
try {
|
|
168
428
|
this.cdr.detectChanges();
|
|
169
429
|
}
|
|
170
430
|
catch {
|
|
171
|
-
// Re-entrant CD — harmless
|
|
431
|
+
// Re-entrant CD — harmless.
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Items rendered inline in the header row */
|
|
435
|
+
get InlineItems() {
|
|
436
|
+
return this._cachedNavItems.slice(0, this.VisibleCount);
|
|
437
|
+
}
|
|
438
|
+
/** Items collapsed into the More dropdown */
|
|
439
|
+
get OverflowItems() {
|
|
440
|
+
return this.VisibleCount >= this._cachedNavItems.length
|
|
441
|
+
? []
|
|
442
|
+
: this._cachedNavItems.slice(this.VisibleCount);
|
|
443
|
+
}
|
|
444
|
+
/** True when any overflowed item is the active one (More button shows active tint) */
|
|
445
|
+
get OverflowHasActive() {
|
|
446
|
+
return this.OverflowItems.some(item => this.isActive(item));
|
|
447
|
+
}
|
|
448
|
+
/** Open/close the More dropdown, anchoring it under the More button and clamping it inside the host */
|
|
449
|
+
ToggleMore(event) {
|
|
450
|
+
event.stopPropagation();
|
|
451
|
+
this.MoreOpen = !this.MoreOpen;
|
|
452
|
+
if (this.MoreOpen) {
|
|
453
|
+
// Anchor the dropdown under the More button; once the dropdown has
|
|
454
|
+
// rendered, clamp it so its right edge stays inside the host.
|
|
455
|
+
this.MoreDropdownLeft = this.moreBtnRef?.nativeElement?.offsetLeft ?? 0;
|
|
456
|
+
requestAnimationFrame(() => {
|
|
457
|
+
const dropdown = this.host.nativeElement.querySelector('.nav-more-dropdown');
|
|
458
|
+
if (dropdown) {
|
|
459
|
+
const maxLeft = this.host.nativeElement.clientWidth - dropdown.offsetWidth;
|
|
460
|
+
this.MoreDropdownLeft = Math.max(0, Math.min(this.MoreDropdownLeft, maxLeft));
|
|
461
|
+
this.cdr.markForCheck();
|
|
462
|
+
try {
|
|
463
|
+
this.cdr.detectChanges();
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// Re-entrant CD — harmless.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
this.cdr.markForCheck();
|
|
472
|
+
}
|
|
473
|
+
/** Close the More dropdown on any outside click or Escape */
|
|
474
|
+
onDocumentClick(event) {
|
|
475
|
+
if (this.MoreOpen && !this.host.nativeElement.contains(event.target)) {
|
|
476
|
+
this.MoreOpen = false;
|
|
477
|
+
this.cdr.markForCheck();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/** Close the More dropdown on Escape */
|
|
481
|
+
onEscape() {
|
|
482
|
+
if (this.MoreOpen) {
|
|
483
|
+
this.MoreOpen = false;
|
|
484
|
+
this.cdr.markForCheck();
|
|
172
485
|
}
|
|
173
486
|
}
|
|
174
487
|
/**
|
|
@@ -237,12 +550,6 @@ export class AppNavComponent {
|
|
|
237
550
|
get navItems() {
|
|
238
551
|
return this._cachedNavItems;
|
|
239
552
|
}
|
|
240
|
-
/**
|
|
241
|
-
* Get cached app color (no computation in getter)
|
|
242
|
-
*/
|
|
243
|
-
get appColor() {
|
|
244
|
-
return this._cachedAppColor;
|
|
245
|
-
}
|
|
246
553
|
/**
|
|
247
554
|
* Check if nav item is active (uses cached state from Map)
|
|
248
555
|
*/
|
|
@@ -260,6 +567,7 @@ export class AppNavComponent {
|
|
|
260
567
|
* Handle nav item click
|
|
261
568
|
*/
|
|
262
569
|
onNavClick(item, event) {
|
|
570
|
+
this.MoreOpen = false;
|
|
263
571
|
this.navItemClick.emit({
|
|
264
572
|
item,
|
|
265
573
|
shiftKey: event?.shiftKey || false
|
|
@@ -282,26 +590,57 @@ export class AppNavComponent {
|
|
|
282
590
|
}
|
|
283
591
|
this.navItemDismiss.emit(item);
|
|
284
592
|
}
|
|
285
|
-
static ɵfac = function AppNavComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || AppNavComponent)(i0.ɵɵdirectiveInject(i1.WorkspaceStateManager), i0.ɵɵdirectiveInject(i2.SharedService), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
|
|
286
|
-
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AppNavComponent, selectors: [["mj-app-nav"]],
|
|
287
|
-
i0.ɵɵ
|
|
288
|
-
|
|
593
|
+
static ɵfac = function AppNavComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || AppNavComponent)(i0.ɵɵdirectiveInject(i1.WorkspaceStateManager), i0.ɵɵdirectiveInject(i2.SharedService), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef)); };
|
|
594
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AppNavComponent, selectors: [["mj-app-nav"]], viewQuery: function AppNavComponent_Query(rf, ctx) { if (rf & 1) {
|
|
595
|
+
i0.ɵɵviewQuery(_c0, 5)(_c1, 5);
|
|
596
|
+
} if (rf & 2) {
|
|
597
|
+
let _t;
|
|
598
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.navListRef = _t.first);
|
|
599
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.moreBtnRef = _t.first);
|
|
600
|
+
} }, hostBindings: function AppNavComponent_HostBindings(rf, ctx) { if (rf & 1) {
|
|
601
|
+
i0.ɵɵlistener("click", function AppNavComponent_click_HostBindingHandler($event) { return ctx.onDocumentClick($event); }, i0.ɵɵresolveDocument)("keydown.escape", function AppNavComponent_keydown_escape_HostBindingHandler() { return ctx.onEscape(); }, i0.ɵɵresolveDocument);
|
|
602
|
+
} }, inputs: { OverflowEnabled: "OverflowEnabled", app: "app" }, outputs: { navItemClick: "navItemClick", navItemDismiss: "navItemDismiss" }, standalone: false, decls: 7, vars: 5, consts: [["navList", ""], ["moreBtn", ""], [1, "nav-list"], ["aria-hidden", "true", 1, "nav-skeleton"], ["role", "link", 1, "nav-item", 3, "mjClickable", "testId", "active", "dynamic", "no-icon"], ["mjClickable", "More navigation items", "testId", "nav-more-btn", 1, "nav-item", "nav-more-btn", 3, "active"], [1, "nav-more-dropdown", 3, "left"], [1, "nav-skeleton-pill", 2, "width", "88px"], [1, "nav-skeleton-pill", 2, "width", "72px"], [1, "nav-skeleton-pill", 2, "width", "96px"], ["role", "link", 1, "nav-item", 3, "click", "mjClickable", "testId"], [3, "class"], [1, "badge"], ["title", "Remove", 1, "dismiss-btn"], ["title", "Remove", 1, "dismiss-btn", 3, "click"], [1, "fa-solid", "fa-xmark"], ["mjClickable", "More navigation items", "testId", "nav-more-btn", 1, "nav-item", "nav-more-btn", 3, "click"], [1, "fa-solid", "fa-ellipsis"], [1, "more-count"], [1, "nav-more-dropdown"], ["role", "link", 1, "nav-more-item", 3, "mjClickable", "testId", "active", "dynamic"], ["role", "link", 1, "nav-more-item", 3, "click", "mjClickable", "testId"], [1, "more-item-label"]], template: function AppNavComponent_Template(rf, ctx) { if (rf & 1) {
|
|
603
|
+
i0.ɵɵelementStart(0, "nav", 2, 0);
|
|
604
|
+
i0.ɵɵconditionalCreate(2, AppNavComponent_Conditional_2_Template, 4, 0, "div", 3);
|
|
605
|
+
i0.ɵɵrepeaterCreate(3, AppNavComponent_For_4_Template, 6, 13, "div", 4, ctx.trackByNavItem, true);
|
|
606
|
+
i0.ɵɵconditionalCreate(5, AppNavComponent_Conditional_5_Template, 7, 5, "div", 5);
|
|
289
607
|
i0.ɵɵelementEnd();
|
|
608
|
+
i0.ɵɵconditionalCreate(6, AppNavComponent_Conditional_6_Template, 3, 2, "div", 6);
|
|
290
609
|
} if (rf & 2) {
|
|
291
|
-
i0.ɵɵ
|
|
610
|
+
i0.ɵɵclassProp("tight", ctx.Tight);
|
|
611
|
+
i0.ɵɵadvance(2);
|
|
612
|
+
i0.ɵɵconditional(ctx.Loading && ctx.navItems.length === 0 ? 2 : -1);
|
|
292
613
|
i0.ɵɵadvance();
|
|
293
|
-
i0.ɵɵrepeater(ctx.
|
|
294
|
-
|
|
614
|
+
i0.ɵɵrepeater(ctx.InlineItems);
|
|
615
|
+
i0.ɵɵadvance(2);
|
|
616
|
+
i0.ɵɵconditional(ctx.OverflowItems.length > 0 ? 5 : -1);
|
|
617
|
+
i0.ɵɵadvance();
|
|
618
|
+
i0.ɵɵconditional(ctx.MoreOpen && ctx.OverflowItems.length > 0 ? 6 : -1);
|
|
619
|
+
} }, dependencies: [i3.MJClickableDirective], styles: ["[_nghost-%COMP%] {\n display: block;\n min-width: 0;\n position: relative;\n}\n\n\n\n\n.nav-list[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: nowrap;\n gap: var(--mj-space-1);\n overflow: hidden;\n}\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}\n\n\n\n.nav-list.tight[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%]:not(.nav-more-btn) {\n flex-shrink: 1;\n min-width: 0;\n}\n.nav-list.tight[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%]:not(.nav-more-btn) > span[_ngcontent-%COMP%]:first-of-type {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n padding: var(--mj-space-2) var(--mj-space-4);\n border-radius: var(--mj-radius-md);\n text-decoration: none;\n color: var(--mj-text-secondary);\n font-weight: var(--mj-font-medium);\n font-size: var(--mj-text-sm);\n white-space: nowrap;\n transition: all var(--mj-transition-fast);\n position: relative;\n cursor: pointer;\n user-select: none;\n}\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n\n\n\n\n\n.nav-list[_ngcontent-%COMP%] .nav-item.active[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--mj-brand-primary);\n}\n.nav-list[_ngcontent-%COMP%] .nav-item.active[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: var(--mj-text-base);\n}\n.nav-list[_ngcontent-%COMP%] .nav-item.no-icon[_ngcontent-%COMP%] {\n padding-left: var(--mj-space-3);\n padding-right: var(--mj-space-3);\n}\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] .badge[_ngcontent-%COMP%] {\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\n border-radius: var(--mj-radius-full);\n padding: var(--mj-space-0-5) var(--mj-space-1-5);\n font-size: 11px;\n font-weight: var(--mj-font-semibold);\n min-width: 18px;\n text-align: center;\n}\n\n\n\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%]:not(.dynamic) + .nav-item.dynamic[_ngcontent-%COMP%] {\n margin-left: var(--mj-space-1);\n padding-left: var(--mj-space-4);\n border-left: 1px solid var(--mj-border-default);\n}\n.nav-list[_ngcontent-%COMP%] .nav-item.dynamic[_ngcontent-%COMP%]:not(.active) {\n opacity: 0.7;\n}\n.nav-list[_ngcontent-%COMP%] .nav-item.dynamic[_ngcontent-%COMP%]:not(.active):hover {\n opacity: 1;\n}\n\n\n\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] .dismiss-btn[_ngcontent-%COMP%] {\n display: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--mj-text-disabled);\n font-size: 10px;\n cursor: pointer;\n transition: all var(--mj-transition-fast);\n margin-left: calc(-1 * var(--mj-space-0-5));\n}\n.nav-list[_ngcontent-%COMP%] .nav-item.dynamic[_ngcontent-%COMP%]:hover .dismiss-btn[_ngcontent-%COMP%] {\n display: flex;\n}\n.nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] .dismiss-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-active);\n color: var(--mj-text-secondary);\n}\n\n\n\n.nav-more-btn[_ngcontent-%COMP%] .more-count[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-active);\n color: var(--mj-text-secondary);\n border-radius: var(--mj-radius-full);\n padding: var(--mj-space-0-5) var(--mj-space-1-5);\n font-size: 11px;\n font-weight: var(--mj-font-semibold);\n min-width: 18px;\n text-align: center;\n line-height: var(--mj-leading-none);\n}\n.nav-more-btn.active[_ngcontent-%COMP%] .more-count[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 20%, transparent);\n color: var(--mj-brand-primary);\n}\n.nav-more-dropdown[_ngcontent-%COMP%] {\n position: absolute;\n top: calc(100% + var(--mj-space-2));\n left: 0; \n\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: 200px;\n width: max-content;\n max-height: calc(100vh - 80px);\n overflow-y: auto;\n padding: var(--mj-space-1) 0;\n z-index: 10000;\n}\n.nav-more-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2-5);\n padding: var(--mj-space-2-5) var(--mj-space-4);\n cursor: pointer;\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 transition: all var(--mj-transition-fast);\n}\n.nav-more-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n width: 18px;\n text-align: center;\n font-size: var(--mj-text-base);\n}\n.nav-more-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.nav-more-item[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: -2px;\n}\n.nav-more-item.active[_ngcontent-%COMP%] {\n \n\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--mj-brand-primary);\n}\n.nav-more-item[_ngcontent-%COMP%] .badge[_ngcontent-%COMP%] {\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\n border-radius: var(--mj-radius-full);\n padding: var(--mj-space-0-5) var(--mj-space-1-5);\n font-size: 11px;\n font-weight: var(--mj-font-semibold);\n min-width: 18px;\n text-align: center;\n}\n.nav-more-item[_ngcontent-%COMP%] .dismiss-btn[_ngcontent-%COMP%] {\n display: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--mj-text-disabled);\n font-size: 10px;\n cursor: pointer;\n}\n.nav-more-item.dynamic[_ngcontent-%COMP%]:hover .dismiss-btn[_ngcontent-%COMP%] {\n display: flex;\n}\n\n\n\n.nav-skeleton[_ngcontent-%COMP%] {\n display: flex;\n gap: var(--mj-space-1);\n}\n.nav-skeleton-pill[_ngcontent-%COMP%] {\n height: 34px;\n border-radius: var(--mj-radius-md);\n background: var(--mj-bg-surface-sunken);\n animation: _ngcontent-%COMP%_nav-skeleton-pulse 1.2s ease-in-out infinite;\n}\n@keyframes _ngcontent-%COMP%_nav-skeleton-pulse {\n 0%, 100% { opacity: 0.55; }\n 50% { opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) {\n .nav-skeleton-pill[_ngcontent-%COMP%] {\n animation: none;\n opacity: 0.7;\n }\n}\n\n\n\n.mobile-nav-content[_nghost-%COMP%] .nav-list[_ngcontent-%COMP%], .mobile-nav-content [_nghost-%COMP%] .nav-list[_ngcontent-%COMP%] {\n flex-direction: column;\n gap: var(--mj-space-0-5);\n padding: 0 var(--mj-space-3);\n}\n.mobile-nav-content[_nghost-%COMP%] .nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%], .mobile-nav-content [_nghost-%COMP%] .nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] {\n padding: var(--mj-space-3) var(--mj-space-4);\n border-radius: var(--mj-radius-md);\n min-height: 48px; \n\n box-sizing: border-box;\n}\n.mobile-nav-content[_nghost-%COMP%] .nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%], .mobile-nav-content [_nghost-%COMP%] .nav-list[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n width: 20px;\n text-align: center;\n}"], changeDetection: 0 });
|
|
295
620
|
}
|
|
296
621
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AppNavComponent, [{
|
|
297
622
|
type: Component,
|
|
298
|
-
args: [{ standalone: false, selector: 'mj-app-nav', changeDetection: ChangeDetectionStrategy.OnPush, template: "<nav class=\"nav-list\" [style.--app-color]=\"appColor\">\n @for (item of navItems; track trackByNavItem($index, item)) {\n <div\n class=\"nav-item\"\n [mjClickable]=\"item.Label\"\n role=\"link\"\n [testId]=\"'nav-item-' + item.Label\"\n [attr.aria-current]=\"isActive(item) ? 'page' : null\"\n [class.active]=\"isActive(item)\"\n [class.dynamic]=\"isDynamic(item)\"\n [class.no-icon]=\"!item.Icon\"\n (click)=\"onNavClick(item, $event)\">\n @if (item.Icon) {\n <i [class]=\"item.Icon\"></i>\n }\n <span>{{ item.Label }}</span>\n @if (item.Badge) {\n <span class=\"badge\">{{ item.Badge }}</span>\n }\n @if (isDynamic(item)) {\n <button class=\"dismiss-btn\" title=\"Remove\" (click)=\"onDismiss(item, $event)\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n </div>\n }\n</nav>\n", styles: [".nav-list {\n display: flex;\n gap: 4px;\n}\n.nav-list .nav-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n border-radius: 6px;\n text-decoration: none;\n color: var(--mj-text-secondary);\n font-weight: 500;\n font-size: 14px;\n transition: all 0.15s;\n position: relative;\n cursor: pointer;\n user-select: none;\n}\n.nav-list .nav-item:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.nav-list .nav-item.active {\n background: color-mix(in srgb, var(--app-color, var(--mj-brand-primary)) 12%, var(--mj-bg-surface));\n color: var(--app-color, var(--mj-brand-primary));\n}\n.nav-list .nav-item.active i {\n color: var(--app-color, var(--mj-brand-primary));\n}\n.nav-list .nav-item i {\n font-size: 16px;\n}\n.nav-list .nav-item.no-icon {\n padding-left: 12px;\n padding-right: 12px;\n}\n.nav-list .nav-item .badge {\n background: var(--mj-status-error);\n color: white;\n border-radius: 10px;\n padding: 2px 6px;\n font-size: 11px;\n font-weight: 600;\n min-width: 18px;\n text-align: center;\n}\n\n/* Dynamic nav items (recent orphan resources) \u2014 separator and dimming */\n.nav-list .nav-item:not(.dynamic) + .nav-item.dynamic {\n margin-left: 4px;\n padding-left: 16px;\n border-left: 1px solid var(--mj-border-default);\n}\n.nav-list .nav-item.dynamic:not(.active) {\n opacity: 0.7;\n}\n.nav-list .nav-item.dynamic:not(.active):hover {\n opacity: 1;\n}\n\n/* Dismiss button for dynamic nav items */\n.nav-list .nav-item .dismiss-btn {\n display: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--mj-text-disabled);\n font-size: 10px;\n cursor: pointer;\n transition: all 0.15s;\n margin-left: -2px;\n}\n.nav-list .nav-item.dynamic:hover .dismiss-btn {\n display: flex;\n}\n.nav-list .nav-item .dismiss-btn:hover {\n background: var(--mj-bg-surface-active);\n color: var(--mj-text-secondary);\n}\n\n/* Mobile drawer vertical layout */\n:host-context(.mobile-nav-content) .nav-list {\n flex-direction: column;\n gap: 2px;\n padding: 0 12px;\n}\n:host-context(.mobile-nav-content) .nav-list .nav-item {\n padding: 12px 16px;\n border-radius: 8px;\n}\n:host-context(.mobile-nav-content) .nav-list .nav-item i {\n width: 20px;\n text-align: center;\n}\n"] }]
|
|
299
|
-
}], () => [{ type: i1.WorkspaceStateManager }, { type: i2.SharedService }, { type: i0.ChangeDetectorRef }], {
|
|
623
|
+
args: [{ standalone: false, selector: 'mj-app-nav', changeDetection: ChangeDetectionStrategy.OnPush, template: "<nav class=\"nav-list\" #navList [class.tight]=\"Tight\">\n @if (Loading && navItems.length === 0) {\n <!-- Skeleton pills while GetNavItems() resolves \u2014 prevents header pop-in -->\n <div class=\"nav-skeleton\" aria-hidden=\"true\">\n <span class=\"nav-skeleton-pill\" style=\"width: 88px\"></span>\n <span class=\"nav-skeleton-pill\" style=\"width: 72px\"></span>\n <span class=\"nav-skeleton-pill\" style=\"width: 96px\"></span>\n </div>\n }\n @for (item of InlineItems; track trackByNavItem($index, item)) {\n <div\n class=\"nav-item\"\n [mjClickable]=\"item.Label\"\n role=\"link\"\n [testId]=\"'nav-item-' + item.Label\"\n [attr.aria-current]=\"isActive(item) ? 'page' : null\"\n [class.active]=\"isActive(item)\"\n [class.dynamic]=\"isDynamic(item)\"\n [class.no-icon]=\"!item.Icon\"\n (click)=\"onNavClick(item, $event)\">\n @if (item.Icon) {\n <i [class]=\"item.Icon\"></i>\n }\n <span>{{ item.Label }}</span>\n @if (item.Badge) {\n <span class=\"badge\">{{ item.Badge }}</span>\n }\n @if (isDynamic(item)) {\n <button class=\"dismiss-btn\" title=\"Remove\" (click)=\"onDismiss(item, $event)\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n </div>\n }\n @if (OverflowItems.length > 0) {\n <div\n #moreBtn\n class=\"nav-item nav-more-btn\"\n mjClickable=\"More navigation items\"\n testId=\"nav-more-btn\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-expanded]=\"MoreOpen\"\n [class.active]=\"OverflowHasActive\"\n (click)=\"ToggleMore($event)\">\n <i class=\"fa-solid fa-ellipsis\"></i>\n <span>More</span>\n <span class=\"more-count\">{{ OverflowItems.length }}</span>\n </div>\n }\n</nav>\n\n<!-- The dropdown lives OUTSIDE the clipped nav row (the row keeps\n overflow: hidden so fit-arithmetic slack can never paint over the\n header's action cluster). Anchored to the More button by measurement. -->\n@if (MoreOpen && OverflowItems.length > 0) {\n <div class=\"nav-more-dropdown\" [style.left.px]=\"MoreDropdownLeft\">\n @for (item of OverflowItems; track trackByNavItem($index, item)) {\n <div\n class=\"nav-more-item\"\n [mjClickable]=\"item.Label\"\n role=\"link\"\n [testId]=\"'nav-more-item-' + item.Label\"\n [attr.aria-current]=\"isActive(item) ? 'page' : null\"\n [class.active]=\"isActive(item)\"\n [class.dynamic]=\"isDynamic(item)\"\n (click)=\"onNavClick(item, $event)\">\n @if (item.Icon) {\n <i [class]=\"item.Icon\"></i>\n }\n <span class=\"more-item-label\">{{ item.Label }}</span>\n @if (item.Badge) {\n <span class=\"badge\">{{ item.Badge }}</span>\n }\n @if (isDynamic(item)) {\n <button class=\"dismiss-btn\" title=\"Remove\" (click)=\"onDismiss(item, $event)\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n </div>\n }\n </div>\n}\n", styles: [":host {\n display: block;\n min-width: 0;\n position: relative;\n}\n/* Always clipped: if the fit arithmetic is ever a few px off (badge widths,\n font fallbacks), the row clips inside its own box instead of painting over\n the header's action cluster. The More dropdown renders outside this row. */\n.nav-list {\n display: flex;\n flex-wrap: nowrap;\n gap: var(--mj-space-1);\n overflow: hidden;\n}\n.nav-list .nav-item {\n flex-shrink: 0;\n}\n/* Tight mode: even one item + More can't fit, so the lone visible pill\n yields with an ellipsis and the More button stays whole. */\n.nav-list.tight .nav-item:not(.nav-more-btn) {\n flex-shrink: 1;\n min-width: 0;\n}\n.nav-list.tight .nav-item:not(.nav-more-btn) > span:first-of-type {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.nav-list .nav-item {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2);\n padding: var(--mj-space-2) var(--mj-space-4);\n border-radius: var(--mj-radius-md);\n text-decoration: none;\n color: var(--mj-text-secondary);\n font-weight: var(--mj-font-medium);\n font-size: var(--mj-text-sm);\n white-space: nowrap;\n transition: all var(--mj-transition-fast);\n position: relative;\n cursor: pointer;\n user-select: none;\n}\n.nav-list .nav-item:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.nav-list .nav-item:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: 2px;\n}\n/* Active = brand-primary everywhere; the app's metadata color is an identity\n accent (icon glyphs), never a state color. Shared active-tint recipe:\n color-mix(in srgb, var(--mj-brand-primary) 10%, transparent) \u2014 keep in sync\n across nav pills, pinned app buttons, launcher cards, and omnibar rows. */\n.nav-list .nav-item.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--mj-brand-primary);\n}\n.nav-list .nav-item.active i {\n color: var(--mj-brand-primary);\n}\n.nav-list .nav-item i {\n font-size: var(--mj-text-base);\n}\n.nav-list .nav-item.no-icon {\n padding-left: var(--mj-space-3);\n padding-right: var(--mj-space-3);\n}\n.nav-list .nav-item .badge {\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\n border-radius: var(--mj-radius-full);\n padding: var(--mj-space-0-5) var(--mj-space-1-5);\n font-size: 11px;\n font-weight: var(--mj-font-semibold);\n min-width: 18px;\n text-align: center;\n}\n\n/* Dynamic nav items (recent orphan resources) \u2014 separator and dimming */\n.nav-list .nav-item:not(.dynamic) + .nav-item.dynamic {\n margin-left: var(--mj-space-1);\n padding-left: var(--mj-space-4);\n border-left: 1px solid var(--mj-border-default);\n}\n.nav-list .nav-item.dynamic:not(.active) {\n opacity: 0.7;\n}\n.nav-list .nav-item.dynamic:not(.active):hover {\n opacity: 1;\n}\n\n/* Dismiss button for dynamic nav items */\n.nav-list .nav-item .dismiss-btn {\n display: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--mj-text-disabled);\n font-size: 10px;\n cursor: pointer;\n transition: all var(--mj-transition-fast);\n margin-left: calc(-1 * var(--mj-space-0-5));\n}\n.nav-list .nav-item.dynamic:hover .dismiss-btn {\n display: flex;\n}\n.nav-list .nav-item .dismiss-btn:hover {\n background: var(--mj-bg-surface-active);\n color: var(--mj-text-secondary);\n}\n\n/* ---- Priority+ overflow: the More button + dropdown ---- */\n.nav-more-btn .more-count {\n background: var(--mj-bg-surface-active);\n color: var(--mj-text-secondary);\n border-radius: var(--mj-radius-full);\n padding: var(--mj-space-0-5) var(--mj-space-1-5);\n font-size: 11px;\n font-weight: var(--mj-font-semibold);\n min-width: 18px;\n text-align: center;\n line-height: var(--mj-leading-none);\n}\n.nav-more-btn.active .more-count {\n background: color-mix(in srgb, var(--mj-brand-primary) 20%, transparent);\n color: var(--mj-brand-primary);\n}\n.nav-more-dropdown {\n position: absolute;\n top: calc(100% + var(--mj-space-2));\n left: 0; /* Overridden by the measured [style.left.px] anchor */\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: 200px;\n width: max-content;\n max-height: calc(100vh - 80px);\n overflow-y: auto;\n padding: var(--mj-space-1) 0;\n z-index: 10000;\n}\n.nav-more-item {\n display: flex;\n align-items: center;\n gap: var(--mj-space-2-5);\n padding: var(--mj-space-2-5) var(--mj-space-4);\n cursor: pointer;\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 transition: all var(--mj-transition-fast);\n}\n.nav-more-item i {\n width: 18px;\n text-align: center;\n font-size: var(--mj-text-base);\n}\n.nav-more-item:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.nav-more-item:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: -2px;\n}\n.nav-more-item.active {\n /* Shared active-tint recipe */\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--mj-brand-primary);\n}\n.nav-more-item .badge {\n background: var(--mj-status-error);\n color: var(--mj-text-inverse);\n border-radius: var(--mj-radius-full);\n padding: var(--mj-space-0-5) var(--mj-space-1-5);\n font-size: 11px;\n font-weight: var(--mj-font-semibold);\n min-width: 18px;\n text-align: center;\n}\n.nav-more-item .dismiss-btn {\n display: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--mj-text-disabled);\n font-size: 10px;\n cursor: pointer;\n}\n.nav-more-item.dynamic:hover .dismiss-btn {\n display: flex;\n}\n\n/* ---- Skeleton pills shown while nav items load ---- */\n.nav-skeleton {\n display: flex;\n gap: var(--mj-space-1);\n}\n.nav-skeleton-pill {\n height: 34px;\n border-radius: var(--mj-radius-md);\n background: var(--mj-bg-surface-sunken);\n animation: nav-skeleton-pulse 1.2s ease-in-out infinite;\n}\n@keyframes nav-skeleton-pulse {\n 0%, 100% { opacity: 0.55; }\n 50% { opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) {\n .nav-skeleton-pill {\n animation: none;\n opacity: 0.7;\n }\n}\n\n/* Mobile drawer vertical layout */\n:host-context(.mobile-nav-content) .nav-list {\n flex-direction: column;\n gap: var(--mj-space-0-5);\n padding: 0 var(--mj-space-3);\n}\n:host-context(.mobile-nav-content) .nav-list .nav-item {\n padding: var(--mj-space-3) var(--mj-space-4);\n border-radius: var(--mj-radius-md);\n min-height: 48px; /* Touch-target parity with the app switcher's mobile items */\n box-sizing: border-box;\n}\n:host-context(.mobile-nav-content) .nav-list .nav-item i {\n width: 20px;\n text-align: center;\n}\n"] }]
|
|
624
|
+
}], () => [{ type: i1.WorkspaceStateManager }, { type: i2.SharedService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }], { navListRef: [{
|
|
625
|
+
type: ViewChild,
|
|
626
|
+
args: ['navList']
|
|
627
|
+
}], moreBtnRef: [{
|
|
628
|
+
type: ViewChild,
|
|
629
|
+
args: ['moreBtn']
|
|
630
|
+
}], navItemClick: [{
|
|
300
631
|
type: Output
|
|
301
632
|
}], navItemDismiss: [{
|
|
302
633
|
type: Output
|
|
634
|
+
}], OverflowEnabled: [{
|
|
635
|
+
type: Input
|
|
303
636
|
}], app: [{
|
|
304
637
|
type: Input
|
|
638
|
+
}], onDocumentClick: [{
|
|
639
|
+
type: HostListener,
|
|
640
|
+
args: ['document:click', ['$event']]
|
|
641
|
+
}], onEscape: [{
|
|
642
|
+
type: HostListener,
|
|
643
|
+
args: ['document:keydown.escape']
|
|
305
644
|
}] }); })();
|
|
306
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AppNavComponent, { className: "AppNavComponent", filePath: "src/lib/shell/components/header/app-nav.component.ts", lineNumber:
|
|
645
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AppNavComponent, { className: "AppNavComponent", filePath: "src/lib/shell/components/header/app-nav.component.ts", lineNumber: 31 }); })();
|
|
307
646
|
//# sourceMappingURL=app-nav.component.js.map
|