ng-hub-ui-nav 21.1.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/LICENSE +21 -0
- package/README.md +258 -0
- package/fesm2022/ng-hub-ui-nav.mjs +2214 -0
- package/fesm2022/ng-hub-ui-nav.mjs.map +1 -0
- package/ng-hub-ui-nav-21.1.0.tgz +0 -0
- package/package.json +53 -0
- package/src/lib/styles/nav-tokens.scss +149 -0
- package/types/ng-hub-ui-nav.d.ts +1280 -0
|
@@ -0,0 +1,2214 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, signal, Injectable, computed, inject, TemplateRef, Directive, input, output, ElementRef, NgZone, effect, ChangeDetectionStrategy, Component, HostListener, contentChild } from '@angular/core';
|
|
3
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
4
|
+
import { RouterLink, Router, NavigationEnd } from '@angular/router';
|
|
5
|
+
import { filter } from 'rxjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default configuration values for the `hub-nav` component.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_NAV_CONFIG = {
|
|
11
|
+
orientation: 'horizontal',
|
|
12
|
+
verticalExpandMode: 'accordion',
|
|
13
|
+
dropdownTrigger: 'click',
|
|
14
|
+
position: 'static',
|
|
15
|
+
stickyTop: '0px',
|
|
16
|
+
collapseMode: 'offcanvas',
|
|
17
|
+
collapseBreakpoint: 992,
|
|
18
|
+
offcanvasPosition: 'start',
|
|
19
|
+
ariaLabel: 'Navigation',
|
|
20
|
+
panelMaxVisible: 3,
|
|
21
|
+
sidebarSide: 'left',
|
|
22
|
+
panelWidth: '16rem'
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Injection token for providing global nav configuration at application startup.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { HUB_NAV_CONFIG } from 'ng-hub-ui-nav';
|
|
30
|
+
*
|
|
31
|
+
* export const appConfig: ApplicationConfig = {
|
|
32
|
+
* providers: [
|
|
33
|
+
* {
|
|
34
|
+
* provide: HUB_NAV_CONFIG,
|
|
35
|
+
* useValue: {
|
|
36
|
+
* orientation: 'vertical',
|
|
37
|
+
* dropdownTrigger: 'hover',
|
|
38
|
+
* collapseBreakpoint: 768
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* ]
|
|
42
|
+
* };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
const HUB_NAV_CONFIG = new InjectionToken('HUB_NAV_CONFIG');
|
|
46
|
+
/**
|
|
47
|
+
* Global configuration service for `hub-nav` components.
|
|
48
|
+
* Provides centralized configuration management with signal-based reactivity.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const configService = inject(HubNavConfigService);
|
|
53
|
+
* configService.updateConfig({ orientation: 'vertical' });
|
|
54
|
+
* const config = configService.getConfig();
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
class HubNavConfigService {
|
|
58
|
+
/** Signal containing the current global configuration. */
|
|
59
|
+
config = signal({ ...DEFAULT_NAV_CONFIG }, ...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
60
|
+
/**
|
|
61
|
+
* Returns the current configuration as a readonly signal.
|
|
62
|
+
*
|
|
63
|
+
* @returns Readonly signal with the current `HubNavConfig`.
|
|
64
|
+
*/
|
|
65
|
+
getConfig() {
|
|
66
|
+
return this.config.asReadonly();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns the default configuration values.
|
|
70
|
+
*
|
|
71
|
+
* @returns A copy of the default `HubNavConfig`.
|
|
72
|
+
*/
|
|
73
|
+
getDefaults() {
|
|
74
|
+
return { ...DEFAULT_NAV_CONFIG };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Merges the given partial configuration into the current config.
|
|
78
|
+
*
|
|
79
|
+
* @param partial - Partial configuration to merge.
|
|
80
|
+
*/
|
|
81
|
+
updateConfig(partial) {
|
|
82
|
+
this.config.update((current) => ({
|
|
83
|
+
...current,
|
|
84
|
+
...partial
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resets the configuration to factory defaults.
|
|
89
|
+
*/
|
|
90
|
+
resetToDefaults() {
|
|
91
|
+
this.config.set({ ...DEFAULT_NAV_CONFIG });
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolves a complete config by merging defaults with the given partial overrides.
|
|
95
|
+
* Used internally by `hub-nav` to merge component-level inputs with global defaults.
|
|
96
|
+
*
|
|
97
|
+
* @param overrides - Partial configuration from the component input.
|
|
98
|
+
* @returns A complete `HubNavConfig`.
|
|
99
|
+
*/
|
|
100
|
+
resolve(overrides) {
|
|
101
|
+
return {
|
|
102
|
+
...this.config(),
|
|
103
|
+
...overrides
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
107
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavConfigService, providedIn: 'root' });
|
|
108
|
+
}
|
|
109
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavConfigService, decorators: [{
|
|
110
|
+
type: Injectable,
|
|
111
|
+
args: [{
|
|
112
|
+
providedIn: 'root'
|
|
113
|
+
}]
|
|
114
|
+
}] });
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Internal state management service for a single `hub-nav` instance.
|
|
118
|
+
* Scoped per component — provided at the component level, not root.
|
|
119
|
+
*
|
|
120
|
+
* Manages:
|
|
121
|
+
* - Resolved configuration.
|
|
122
|
+
* - Open dropdown tracking.
|
|
123
|
+
* - Mobile menu state.
|
|
124
|
+
* - Active route propagation.
|
|
125
|
+
* - Panel stack for drill-down navigation.
|
|
126
|
+
*/
|
|
127
|
+
class HubNavStateService {
|
|
128
|
+
/** Currently resolved configuration. */
|
|
129
|
+
_config = signal({
|
|
130
|
+
orientation: 'horizontal',
|
|
131
|
+
verticalExpandMode: 'accordion',
|
|
132
|
+
dropdownTrigger: 'click',
|
|
133
|
+
position: 'static',
|
|
134
|
+
stickyTop: '0px',
|
|
135
|
+
collapseMode: 'offcanvas',
|
|
136
|
+
collapseBreakpoint: 992,
|
|
137
|
+
offcanvasPosition: 'start',
|
|
138
|
+
ariaLabel: 'Navigation',
|
|
139
|
+
panelMaxVisible: 3,
|
|
140
|
+
sidebarSide: 'left',
|
|
141
|
+
panelWidth: '16rem'
|
|
142
|
+
}, ...(ngDevMode ? [{ debugName: "_config" }] : []));
|
|
143
|
+
/** Set of dropdown item IDs that are currently open. */
|
|
144
|
+
_openDropdowns = signal(new Set(), ...(ngDevMode ? [{ debugName: "_openDropdowns" }] : []));
|
|
145
|
+
/** Whether the mobile panel is open. */
|
|
146
|
+
_mobileOpen = signal(false, ...(ngDevMode ? [{ debugName: "_mobileOpen" }] : []));
|
|
147
|
+
/** Whether the viewport is below the collapse breakpoint. */
|
|
148
|
+
_collapsed = signal(false, ...(ngDevMode ? [{ debugName: "_collapsed" }] : []));
|
|
149
|
+
/** Stack of open panels for panel expand mode. */
|
|
150
|
+
_panelStack = signal([], ...(ngDevMode ? [{ debugName: "_panelStack" }] : []));
|
|
151
|
+
/** Auto-incrementing counter for unique panel IDs. */
|
|
152
|
+
_panelIdCounter = 0;
|
|
153
|
+
/** Readonly config signal. */
|
|
154
|
+
config = this._config.asReadonly();
|
|
155
|
+
/** Readonly mobile-open signal. */
|
|
156
|
+
mobileOpen = this._mobileOpen.asReadonly();
|
|
157
|
+
/** Readonly collapsed signal. */
|
|
158
|
+
collapsed = this._collapsed.asReadonly();
|
|
159
|
+
/** Readonly panel stack signal. */
|
|
160
|
+
panelStack = this._panelStack.asReadonly();
|
|
161
|
+
/** Number of currently open panels. */
|
|
162
|
+
panelCount = computed(() => this._panelStack().length, ...(ngDevMode ? [{ debugName: "panelCount" }] : []));
|
|
163
|
+
/** Computed orientation shortcut. */
|
|
164
|
+
orientation = computed(() => this._config().orientation, ...(ngDevMode ? [{ debugName: "orientation" }] : []));
|
|
165
|
+
/** Computed vertical expand mode shortcut. */
|
|
166
|
+
verticalExpandMode = computed(() => this._config().verticalExpandMode, ...(ngDevMode ? [{ debugName: "verticalExpandMode" }] : []));
|
|
167
|
+
/** Computed dropdown trigger shortcut. */
|
|
168
|
+
dropdownTrigger = computed(() => this._config().dropdownTrigger, ...(ngDevMode ? [{ debugName: "dropdownTrigger" }] : []));
|
|
169
|
+
/**
|
|
170
|
+
* Updates the resolved configuration.
|
|
171
|
+
*
|
|
172
|
+
* @param config - Full resolved configuration.
|
|
173
|
+
*/
|
|
174
|
+
setConfig(config) {
|
|
175
|
+
this._config.set(config);
|
|
176
|
+
this.reconcilePanelStackWithConfig(config);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Normalizes the panel stack after config changes.
|
|
180
|
+
*
|
|
181
|
+
* This is required when `panelMaxVisible` changes after panels were already
|
|
182
|
+
* opened (for example during initial input binding in the host app shell).
|
|
183
|
+
* Without this reconciliation, the UI can temporarily render more panels
|
|
184
|
+
* than allowed or keep a last panel without header while its parent panel
|
|
185
|
+
* is no longer visible.
|
|
186
|
+
*
|
|
187
|
+
* @param config - The latest resolved configuration.
|
|
188
|
+
*/
|
|
189
|
+
reconcilePanelStackWithConfig(config) {
|
|
190
|
+
const effectiveMaxVisible = Math.max(1, config.panelMaxVisible - 1);
|
|
191
|
+
const current = this._panelStack();
|
|
192
|
+
if (current.length <= effectiveMaxVisible) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const hiddenPanelsCount = current.length - effectiveMaxVisible;
|
|
196
|
+
const trimmed = current.slice(-effectiveMaxVisible).map((panel, index) => {
|
|
197
|
+
// If some parent panels were trimmed, the first visible panel must
|
|
198
|
+
// render a header because its parent context is now hidden.
|
|
199
|
+
if (hiddenPanelsCount > 0 && index === 0 && !panel.isDrillDown) {
|
|
200
|
+
return {
|
|
201
|
+
...panel,
|
|
202
|
+
isDrillDown: true
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return panel;
|
|
206
|
+
});
|
|
207
|
+
this._panelStack.set(trimmed);
|
|
208
|
+
}
|
|
209
|
+
// ──────────────────────────────────────────────
|
|
210
|
+
// Dropdown management
|
|
211
|
+
// ──────────────────────────────────────────────
|
|
212
|
+
/**
|
|
213
|
+
* Checks whether a dropdown with the given item ID is open.
|
|
214
|
+
*
|
|
215
|
+
* @param id - Item ID to check.
|
|
216
|
+
* @returns `true` if the dropdown is open.
|
|
217
|
+
*/
|
|
218
|
+
isDropdownOpen(id) {
|
|
219
|
+
return this._openDropdowns().has(id);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Opens a dropdown by item ID.
|
|
223
|
+
* In accordion mode, closes sibling dropdowns at the same level.
|
|
224
|
+
*
|
|
225
|
+
* @param id - Item ID to open.
|
|
226
|
+
* @param siblings - Optional array of sibling item IDs to close when opening this one.
|
|
227
|
+
*/
|
|
228
|
+
openDropdown(id, siblings) {
|
|
229
|
+
this._openDropdowns.update((set) => {
|
|
230
|
+
const next = new Set(set);
|
|
231
|
+
// In accordion mode, close sibling dropdowns
|
|
232
|
+
if (siblings && this._config().verticalExpandMode === 'accordion') {
|
|
233
|
+
siblings.forEach((siblingId) => {
|
|
234
|
+
if (siblingId !== id) {
|
|
235
|
+
next.delete(siblingId);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
next.add(id);
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Closes a dropdown by item ID.
|
|
245
|
+
*
|
|
246
|
+
* @param id - Item ID to close.
|
|
247
|
+
*/
|
|
248
|
+
closeDropdown(id) {
|
|
249
|
+
this._openDropdowns.update((set) => {
|
|
250
|
+
const next = new Set(set);
|
|
251
|
+
next.delete(id);
|
|
252
|
+
return next;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Toggles a dropdown open/closed by item ID.
|
|
257
|
+
*
|
|
258
|
+
* @param id - Item ID to toggle.
|
|
259
|
+
* @returns `true` if the dropdown is now open.
|
|
260
|
+
*/
|
|
261
|
+
toggleDropdown(id) {
|
|
262
|
+
if (this.isDropdownOpen(id)) {
|
|
263
|
+
this.closeDropdown(id);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
this.openDropdown(id);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
/** Closes all open dropdowns. */
|
|
270
|
+
closeAllDropdowns() {
|
|
271
|
+
this._openDropdowns.set(new Set());
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Synchronizes the open dropdown set with the current active route.
|
|
275
|
+
* Only ancestor items in the active trail remain open.
|
|
276
|
+
*
|
|
277
|
+
* @param items - Root navigation items to inspect.
|
|
278
|
+
* @param activeRoute - Current router URL including optional fragment.
|
|
279
|
+
*/
|
|
280
|
+
syncDropdownsWithRoute(items, activeRoute) {
|
|
281
|
+
const nextOpenIds = new Set(this.findActiveDropdownTrail(items, activeRoute));
|
|
282
|
+
this._openDropdowns.set(nextOpenIds);
|
|
283
|
+
}
|
|
284
|
+
// ──────────────────────────────────────────────
|
|
285
|
+
// Mobile state
|
|
286
|
+
// ──────────────────────────────────────────────
|
|
287
|
+
/**
|
|
288
|
+
* Sets the mobile panel open state.
|
|
289
|
+
*
|
|
290
|
+
* @param open - Whether the mobile panel should be open.
|
|
291
|
+
*/
|
|
292
|
+
setMobileOpen(open) {
|
|
293
|
+
this._mobileOpen.set(open);
|
|
294
|
+
}
|
|
295
|
+
/** Toggles the mobile panel. */
|
|
296
|
+
toggleMobile() {
|
|
297
|
+
this._mobileOpen.update((v) => !v);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Sets the collapsed state (below breakpoint).
|
|
301
|
+
*
|
|
302
|
+
* @param collapsed - Whether the viewport is below the collapse breakpoint.
|
|
303
|
+
*/
|
|
304
|
+
setCollapsed(collapsed) {
|
|
305
|
+
if (this._collapsed() === collapsed) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this._collapsed.set(collapsed);
|
|
309
|
+
if (!collapsed) {
|
|
310
|
+
this._mobileOpen.set(false);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// ──────────────────────────────────────────────
|
|
314
|
+
// Panel stack management
|
|
315
|
+
// ──────────────────────────────────────────────
|
|
316
|
+
/**
|
|
317
|
+
* Opens a new panel for the given parent item's children.
|
|
318
|
+
* If the panel count has reached `panelMaxVisible`, drills down within the last panel instead.
|
|
319
|
+
*
|
|
320
|
+
* @param parentItem - The item whose children should be displayed in the panel.
|
|
321
|
+
* @param rootItems - The root-level navigation items (to determine if parentItem is at root level).
|
|
322
|
+
*/
|
|
323
|
+
openPanel(parentItem, rootItems) {
|
|
324
|
+
if (!parentItem.children || parentItem.children.length === 0) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const maxVisible = this._config().panelMaxVisible;
|
|
328
|
+
const currentStack = this._panelStack();
|
|
329
|
+
// Check if this exact item already has a panel open
|
|
330
|
+
const existingIndex = currentStack.findIndex((p) => p.parentItem.id === parentItem.id);
|
|
331
|
+
if (existingIndex >= 0) {
|
|
332
|
+
// Panel for this item already exists — close it and all after it
|
|
333
|
+
this.closePanelsFromIndex(existingIndex);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// If parentItem is a root-level item and there are already panels open,
|
|
337
|
+
// close all existing panels before opening the new one (same-level replacement)
|
|
338
|
+
const isRootLevel = rootItems?.some((item) => item.id === parentItem.id);
|
|
339
|
+
if (isRootLevel && currentStack.length > 0) {
|
|
340
|
+
this.closeAllPanels();
|
|
341
|
+
}
|
|
342
|
+
// Determine the depth of the new panel
|
|
343
|
+
const newPanelDepth = this.findPanelDepth(parentItem, currentStack);
|
|
344
|
+
// Close panels at the same depth or deeper (sibling replacement)
|
|
345
|
+
if (newPanelDepth >= 0) {
|
|
346
|
+
// Close all panels from this depth onward
|
|
347
|
+
this.closePanelsFromIndex(newPanelDepth);
|
|
348
|
+
}
|
|
349
|
+
const updatedStack = this._panelStack();
|
|
350
|
+
// `panelMaxVisible` represents the total visible columns in panel mode,
|
|
351
|
+
// including the root column. The panel stack stores only subpanels, so
|
|
352
|
+
// subtract one to get the visible subpanel limit.
|
|
353
|
+
const effectiveMaxVisible = Math.max(1, maxVisible - 1);
|
|
354
|
+
if (updatedStack.length >= effectiveMaxVisible) {
|
|
355
|
+
// Max panels reached — drill-down within the last panel
|
|
356
|
+
const lastPanel = updatedStack[updatedStack.length - 1];
|
|
357
|
+
this.drillDownInPanel(lastPanel.id, parentItem, parentItem.children);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Add a new panel
|
|
361
|
+
const panel = {
|
|
362
|
+
id: `panel-${++this._panelIdCounter}`,
|
|
363
|
+
parentItem,
|
|
364
|
+
items: parentItem.children,
|
|
365
|
+
history: [],
|
|
366
|
+
isDrillDown: false
|
|
367
|
+
};
|
|
368
|
+
this._panelStack.update((stack) => [...stack, panel]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Finds the depth (index) where a panel for the given item should be placed.
|
|
373
|
+
* Returns the index of the first panel whose items contain parentItem,
|
|
374
|
+
* or -1 if parentItem is not found in any panel.
|
|
375
|
+
*
|
|
376
|
+
* @param parentItem - The item to search for.
|
|
377
|
+
* @param stack - The current panel stack.
|
|
378
|
+
* @returns The panel index, or -1 if not found.
|
|
379
|
+
*/
|
|
380
|
+
findPanelDepth(parentItem, stack) {
|
|
381
|
+
for (let i = 0; i < stack.length; i++) {
|
|
382
|
+
const panel = stack[i];
|
|
383
|
+
if (panel.items.some((item) => item.id === parentItem.id)) {
|
|
384
|
+
return i + 1; // New panel should go after this one
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return -1;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Finds the ordered list of dropdown ancestor IDs for the active route.
|
|
391
|
+
*
|
|
392
|
+
* @param items - Items to search recursively.
|
|
393
|
+
* @param activeRoute - Current router URL.
|
|
394
|
+
* @param trail - Accumulated ancestor trail.
|
|
395
|
+
* @returns Ancestor IDs that must remain expanded.
|
|
396
|
+
*/
|
|
397
|
+
findActiveDropdownTrail(items, activeRoute, trail = []) {
|
|
398
|
+
for (const item of items) {
|
|
399
|
+
if (!this.isItemOrDescendantActive(item, activeRoute)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const nextTrail = item.children?.length ? [...trail, item.id] : trail;
|
|
403
|
+
if (!item.children?.length) {
|
|
404
|
+
return nextTrail;
|
|
405
|
+
}
|
|
406
|
+
const childTrail = this.findActiveDropdownTrail(item.children, activeRoute, nextTrail);
|
|
407
|
+
return childTrail.length > 0 ? childTrail : nextTrail;
|
|
408
|
+
}
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Closes a panel by its ID and all panels opened after it.
|
|
413
|
+
*
|
|
414
|
+
* @param panelId - ID of the panel to close.
|
|
415
|
+
*/
|
|
416
|
+
closePanel(panelId) {
|
|
417
|
+
const index = this._panelStack().findIndex((p) => p.id === panelId);
|
|
418
|
+
if (index >= 0) {
|
|
419
|
+
this.closePanelsFromIndex(index);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Closes all panels from the given index onward.
|
|
424
|
+
*
|
|
425
|
+
* @param index - Zero-based index from which to start closing.
|
|
426
|
+
*/
|
|
427
|
+
closePanelsFromIndex(index) {
|
|
428
|
+
this._panelStack.update((stack) => stack.slice(0, index));
|
|
429
|
+
}
|
|
430
|
+
/** Closes all open panels. */
|
|
431
|
+
closeAllPanels() {
|
|
432
|
+
this._panelStack.set([]);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Performs a drill-down within an existing panel: pushes current items to history
|
|
436
|
+
* and replaces them with the new item's children.
|
|
437
|
+
*
|
|
438
|
+
* @param panelId - ID of the panel to drill down in.
|
|
439
|
+
* @param parentItem - The item whose children will replace the panel content.
|
|
440
|
+
* @param items - The new items to display.
|
|
441
|
+
*/
|
|
442
|
+
drillDownInPanel(panelId, parentItem, items) {
|
|
443
|
+
this._panelStack.update((stack) => stack.map((panel) => {
|
|
444
|
+
if (panel.id !== panelId) {
|
|
445
|
+
return panel;
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
...panel,
|
|
449
|
+
history: [
|
|
450
|
+
...panel.history,
|
|
451
|
+
{ items: panel.items, parentItem: panel.parentItem }
|
|
452
|
+
],
|
|
453
|
+
parentItem,
|
|
454
|
+
items,
|
|
455
|
+
isDrillDown: true
|
|
456
|
+
};
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Navigates back one level in a panel's drill-down history.
|
|
461
|
+
* If there is no history, the panel is closed.
|
|
462
|
+
*
|
|
463
|
+
* @param panelId - ID of the panel to navigate back in.
|
|
464
|
+
*/
|
|
465
|
+
navigateBackInPanel(panelId) {
|
|
466
|
+
const panel = this._panelStack().find((p) => p.id === panelId);
|
|
467
|
+
if (!panel) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (panel.history.length === 0) {
|
|
471
|
+
this.closePanel(panelId);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this._panelStack.update((stack) => stack.map((p) => {
|
|
475
|
+
if (p.id !== panelId) {
|
|
476
|
+
return p;
|
|
477
|
+
}
|
|
478
|
+
const newHistory = [...p.history];
|
|
479
|
+
const previous = newHistory.pop();
|
|
480
|
+
return {
|
|
481
|
+
...p,
|
|
482
|
+
items: previous.items,
|
|
483
|
+
parentItem: previous.parentItem,
|
|
484
|
+
history: newHistory,
|
|
485
|
+
isDrillDown: newHistory.length > 0
|
|
486
|
+
};
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Retrieves a panel by its ID.
|
|
491
|
+
*
|
|
492
|
+
* @param panelId - ID of the panel to find.
|
|
493
|
+
* @returns The panel state, or `undefined` if not found.
|
|
494
|
+
*/
|
|
495
|
+
getPanelById(panelId) {
|
|
496
|
+
return this._panelStack().find((p) => p.id === panelId);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Checks whether there is an open panel whose parent item matches the given ID.
|
|
500
|
+
*
|
|
501
|
+
* @param itemId - Parent item ID to match against the panel stack.
|
|
502
|
+
* @returns `true` when a panel for the given item is currently open.
|
|
503
|
+
*/
|
|
504
|
+
isPanelOpenForItem(itemId) {
|
|
505
|
+
return this._panelStack().some((panel) => panel.parentItem.id === itemId);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Resolves the effective expand mode for an item, considering:
|
|
509
|
+
* 1. Per-item `expandMode` override.
|
|
510
|
+
* 2. Global `verticalExpandMode` from config.
|
|
511
|
+
* 3. Fallback to `'accordion'` when collapsed (mobile).
|
|
512
|
+
*
|
|
513
|
+
* @param item - The item to resolve the expand mode for.
|
|
514
|
+
* @returns The effective expand mode.
|
|
515
|
+
*/
|
|
516
|
+
getEffectiveExpandMode(item) {
|
|
517
|
+
// In horizontal mode, allow panel mode when explicitly configured.
|
|
518
|
+
// Otherwise, keep flyout/dropdown behavior.
|
|
519
|
+
if (this._config().orientation === 'horizontal') {
|
|
520
|
+
return this._config().verticalExpandMode === 'panel' ? 'panel' : 'flyout';
|
|
521
|
+
}
|
|
522
|
+
const mode = item.expandMode ?? this._config().verticalExpandMode;
|
|
523
|
+
// On mobile (collapsed), panel mode falls back to accordion
|
|
524
|
+
if (mode === 'panel' && this._collapsed()) {
|
|
525
|
+
return 'accordion';
|
|
526
|
+
}
|
|
527
|
+
return mode;
|
|
528
|
+
}
|
|
529
|
+
// ──────────────────────────────────────────────
|
|
530
|
+
// Active route detection
|
|
531
|
+
// ──────────────────────────────────────────────
|
|
532
|
+
/**
|
|
533
|
+
* Recursively checks whether the given item or any of its descendants
|
|
534
|
+
* match the provided active route.
|
|
535
|
+
*
|
|
536
|
+
* @param item - Item to check.
|
|
537
|
+
* @param activeRoute - Currently active route path.
|
|
538
|
+
* @returns `true` if the item or a descendant is active.
|
|
539
|
+
*/
|
|
540
|
+
isItemOrDescendantActive(item, activeRoute) {
|
|
541
|
+
if (item.route) {
|
|
542
|
+
const route = Array.isArray(item.route) ? item.route.join('/') : item.route;
|
|
543
|
+
const [activePath, activeFragment] = activeRoute.split('#');
|
|
544
|
+
if (route === activePath) {
|
|
545
|
+
// When an item defines a fragment, require exact fragment match.
|
|
546
|
+
if (item.fragment) {
|
|
547
|
+
if (item.fragment === activeFragment) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Also handle activeRoute values without fragment split side effects.
|
|
556
|
+
if (!item.fragment && route === activeRoute) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (item.children) {
|
|
561
|
+
return item.children.some((child) => this.isItemOrDescendantActive(child, activeRoute));
|
|
562
|
+
}
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
566
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavStateService });
|
|
567
|
+
}
|
|
568
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavStateService, decorators: [{
|
|
569
|
+
type: Injectable
|
|
570
|
+
}] });
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Structural directive that projects content into the navigation start slot.
|
|
574
|
+
*
|
|
575
|
+
* Slot behavior:
|
|
576
|
+
* - Horizontal nav: appears at the visual start (left in LTR, right in RTL).
|
|
577
|
+
* - Vertical nav: appears at the top of the primary column.
|
|
578
|
+
*
|
|
579
|
+
* The template receives a {@link HubNavStartTemplateContext}.
|
|
580
|
+
*/
|
|
581
|
+
class HubNavStartDirective {
|
|
582
|
+
/** Reference to the projected template. */
|
|
583
|
+
template = inject((TemplateRef));
|
|
584
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavStartDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
585
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.2", type: HubNavStartDirective, isStandalone: true, selector: "[hubNavStart]", ngImport: i0 });
|
|
586
|
+
}
|
|
587
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavStartDirective, decorators: [{
|
|
588
|
+
type: Directive,
|
|
589
|
+
args: [{
|
|
590
|
+
selector: '[hubNavStart]',
|
|
591
|
+
standalone: true
|
|
592
|
+
}]
|
|
593
|
+
}] });
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Structural directive that projects content into the navigation end slot.
|
|
597
|
+
*
|
|
598
|
+
* Slot behavior:
|
|
599
|
+
* - Horizontal nav: appears at the visual end (right in LTR, left in RTL).
|
|
600
|
+
* - Vertical nav: appears at the bottom of the primary column.
|
|
601
|
+
*
|
|
602
|
+
* The template receives a {@link HubNavEndTemplateContext}.
|
|
603
|
+
*/
|
|
604
|
+
class HubNavEndDirective {
|
|
605
|
+
/** Reference to the projected template. */
|
|
606
|
+
template = inject((TemplateRef));
|
|
607
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavEndDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
608
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.2", type: HubNavEndDirective, isStandalone: true, selector: "[hubNavEnd]", ngImport: i0 });
|
|
609
|
+
}
|
|
610
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavEndDirective, decorators: [{
|
|
611
|
+
type: Directive,
|
|
612
|
+
args: [{
|
|
613
|
+
selector: '[hubNavEnd]',
|
|
614
|
+
standalone: true
|
|
615
|
+
}]
|
|
616
|
+
}] });
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Structural directive that provides a custom template for rendering individual nav items.
|
|
620
|
+
* When present, each nav item is rendered using this template instead of the default markup.
|
|
621
|
+
*
|
|
622
|
+
* The template receives a {@link HubNavItemTemplateContext} with `$implicit` (the item),
|
|
623
|
+
* `active`, `expanded`, and `depth`.
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```html
|
|
627
|
+
* <hub-nav [items]="menuItems">
|
|
628
|
+
* <ng-template hubNavItemTemplate let-item let-active="active" let-depth="depth">
|
|
629
|
+
* <div class="custom-item" [class.active]="active">
|
|
630
|
+
* <i [class]="item.icon"></i>
|
|
631
|
+
* {{ item.label }}
|
|
632
|
+
* </div>
|
|
633
|
+
* </ng-template>
|
|
634
|
+
* </hub-nav>
|
|
635
|
+
* ```
|
|
636
|
+
*/
|
|
637
|
+
class HubNavItemTemplateDirective {
|
|
638
|
+
/** Reference to the template provided by the consumer. */
|
|
639
|
+
template = inject((TemplateRef));
|
|
640
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavItemTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
641
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.2", type: HubNavItemTemplateDirective, isStandalone: true, selector: "[hubNavItemTemplate]", ngImport: i0 });
|
|
642
|
+
}
|
|
643
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavItemTemplateDirective, decorators: [{
|
|
644
|
+
type: Directive,
|
|
645
|
+
args: [{
|
|
646
|
+
selector: '[hubNavItemTemplate]',
|
|
647
|
+
standalone: true
|
|
648
|
+
}]
|
|
649
|
+
}] });
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Scroll spy container directive that tracks the currently visible section and
|
|
653
|
+
* emits section changes using `IntersectionObserver`.
|
|
654
|
+
*/
|
|
655
|
+
class HubNavScrollSpyDirective {
|
|
656
|
+
/** Enables or disables section tracking. */
|
|
657
|
+
enabled = input(true, { ...(ngDevMode ? { debugName: "enabled" } : {}), alias: 'hubNavScrollSpy' });
|
|
658
|
+
/** Top offset in pixels to compensate sticky headers. */
|
|
659
|
+
offset = input(120, ...(ngDevMode ? [{ debugName: "offset" }] : []));
|
|
660
|
+
/** CSS selector used to locate section elements inside the host container. */
|
|
661
|
+
sectionSelector = input('[data-hub-nav-scroll-spy-section]', ...(ngDevMode ? [{ debugName: "sectionSelector" }] : []));
|
|
662
|
+
/** Emits the active section id whenever it changes. */
|
|
663
|
+
activeSectionChange = output();
|
|
664
|
+
el = inject((ElementRef));
|
|
665
|
+
zone = inject(NgZone);
|
|
666
|
+
observer = null;
|
|
667
|
+
activeId = signal(null, ...(ngDevMode ? [{ debugName: "activeId" }] : []));
|
|
668
|
+
initialized = signal(false, ...(ngDevMode ? [{ debugName: "initialized" }] : []));
|
|
669
|
+
/** Current active section id (readonly signal). */
|
|
670
|
+
activeSectionId = computed(() => this.activeId(), ...(ngDevMode ? [{ debugName: "activeSectionId" }] : []));
|
|
671
|
+
configEffect = effect(() => {
|
|
672
|
+
if (!this.initialized()) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const isEnabled = this.enabled();
|
|
676
|
+
const topOffset = this.offset();
|
|
677
|
+
const selector = this.sectionSelector();
|
|
678
|
+
void topOffset;
|
|
679
|
+
void selector;
|
|
680
|
+
if (!isEnabled) {
|
|
681
|
+
this.destroyObserver();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.scheduleInit();
|
|
685
|
+
}, ...(ngDevMode ? [{ debugName: "configEffect" }] : []));
|
|
686
|
+
/** @inheritDoc */
|
|
687
|
+
ngAfterViewInit() {
|
|
688
|
+
this.initialized.set(true);
|
|
689
|
+
if (this.enabled()) {
|
|
690
|
+
this.scheduleInit();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/** @inheritDoc */
|
|
694
|
+
ngOnDestroy() {
|
|
695
|
+
this.destroyObserver();
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Scrolls to a tracked section by id.
|
|
699
|
+
*
|
|
700
|
+
* @param sectionId - Target section id.
|
|
701
|
+
* @param behavior - Native scroll behavior.
|
|
702
|
+
* @returns `true` when the target exists and scroll was requested.
|
|
703
|
+
*/
|
|
704
|
+
scrollTo(sectionId, behavior = 'smooth') {
|
|
705
|
+
const target = this.getSectionElements().find((element) => this.getSectionId(element) === sectionId);
|
|
706
|
+
if (!target) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
target.scrollIntoView({
|
|
710
|
+
behavior,
|
|
711
|
+
block: 'start',
|
|
712
|
+
inline: 'nearest'
|
|
713
|
+
});
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
scheduleInit() {
|
|
717
|
+
this.zone.runOutsideAngular(() => {
|
|
718
|
+
requestAnimationFrame(() => {
|
|
719
|
+
requestAnimationFrame(() => {
|
|
720
|
+
this.zone.run(() => this.initializeObserver());
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
initializeObserver() {
|
|
726
|
+
this.destroyObserver();
|
|
727
|
+
const sections = this.getSectionElements();
|
|
728
|
+
if (sections.length === 0) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const topOffset = this.offset();
|
|
732
|
+
this.observer = new IntersectionObserver((entries) => this.handleObserverEntries(entries), {
|
|
733
|
+
root: null,
|
|
734
|
+
rootMargin: `-${topOffset}px 0px -55% 0px`,
|
|
735
|
+
threshold: [0.05, 0.2, 0.4, 0.6, 0.8]
|
|
736
|
+
});
|
|
737
|
+
sections.forEach((section) => this.observer?.observe(section));
|
|
738
|
+
}
|
|
739
|
+
destroyObserver() {
|
|
740
|
+
this.observer?.disconnect();
|
|
741
|
+
this.observer = null;
|
|
742
|
+
}
|
|
743
|
+
getSectionElements() {
|
|
744
|
+
return Array.from(this.el.nativeElement.querySelectorAll(this.sectionSelector())).filter((node) => node instanceof HTMLElement);
|
|
745
|
+
}
|
|
746
|
+
getSectionId(element) {
|
|
747
|
+
return element.getAttribute('data-hub-nav-scroll-spy-section') ?? element.id ?? null;
|
|
748
|
+
}
|
|
749
|
+
handleObserverEntries(entries) {
|
|
750
|
+
const visible = entries
|
|
751
|
+
.filter((entry) => entry.isIntersecting)
|
|
752
|
+
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
|
|
753
|
+
if (visible.length === 0) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const top = visible[0];
|
|
757
|
+
const id = this.getSectionId(top.target);
|
|
758
|
+
if (!id || id === this.activeId()) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
this.activeId.set(id);
|
|
762
|
+
this.activeSectionChange.emit(id);
|
|
763
|
+
}
|
|
764
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavScrollSpyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
765
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.2", type: HubNavScrollSpyDirective, isStandalone: true, selector: "[hubNavScrollSpy]", inputs: { enabled: { classPropertyName: "enabled", publicName: "hubNavScrollSpy", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, sectionSelector: { classPropertyName: "sectionSelector", publicName: "sectionSelector", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeSectionChange: "activeSectionChange" }, exportAs: ["hubNavScrollSpy"], ngImport: i0 });
|
|
766
|
+
}
|
|
767
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavScrollSpyDirective, decorators: [{
|
|
768
|
+
type: Directive,
|
|
769
|
+
args: [{
|
|
770
|
+
selector: '[hubNavScrollSpy]',
|
|
771
|
+
standalone: true,
|
|
772
|
+
exportAs: 'hubNavScrollSpy'
|
|
773
|
+
}]
|
|
774
|
+
}], propDecorators: { enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "hubNavScrollSpy", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], sectionSelector: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionSelector", required: false }] }], activeSectionChange: [{ type: i0.Output, args: ["activeSectionChange"] }] } });
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Marks an element as a trackable section for `HubNavScrollSpyDirective`.
|
|
778
|
+
*
|
|
779
|
+
* If no explicit section id is provided, the directive falls back to the host
|
|
780
|
+
* element `id` attribute.
|
|
781
|
+
*/
|
|
782
|
+
class HubNavScrollSpySectionDirective {
|
|
783
|
+
/** Optional section id. When omitted, host `id` is used. */
|
|
784
|
+
sectionId = input(null, { ...(ngDevMode ? { debugName: "sectionId" } : {}), alias: 'hubNavScrollSpySection' });
|
|
785
|
+
el = inject((ElementRef));
|
|
786
|
+
/** Effective section id exposed to the container directive. */
|
|
787
|
+
resolvedSectionId = computed(() => this.sectionId() ?? this.el.nativeElement.id ?? null, ...(ngDevMode ? [{ debugName: "resolvedSectionId" }] : []));
|
|
788
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavScrollSpySectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
789
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.2", type: HubNavScrollSpySectionDirective, isStandalone: true, selector: "[hubNavScrollSpySection]", inputs: { sectionId: { classPropertyName: "sectionId", publicName: "hubNavScrollSpySection", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-hub-nav-scroll-spy-section": "resolvedSectionId() || null" } }, ngImport: i0 });
|
|
790
|
+
}
|
|
791
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavScrollSpySectionDirective, decorators: [{
|
|
792
|
+
type: Directive,
|
|
793
|
+
args: [{
|
|
794
|
+
selector: '[hubNavScrollSpySection]',
|
|
795
|
+
standalone: true,
|
|
796
|
+
host: {
|
|
797
|
+
'[attr.data-hub-nav-scroll-spy-section]': 'resolvedSectionId() || null'
|
|
798
|
+
}
|
|
799
|
+
}]
|
|
800
|
+
}], propDecorators: { sectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "hubNavScrollSpySection", required: false }] }] } });
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Renders a single navigation item (link, dropdown trigger, header, or custom content).
|
|
804
|
+
* Handles routing integration, active state, badges, icons, and disabled state.
|
|
805
|
+
*
|
|
806
|
+
* @internal Used internally by `HubNavItemListComponent`.
|
|
807
|
+
*/
|
|
808
|
+
class HubNavItemComponent {
|
|
809
|
+
/** The navigation item data to render. */
|
|
810
|
+
item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
|
|
811
|
+
/** Nesting depth (0 = root). */
|
|
812
|
+
depth = input(0, ...(ngDevMode ? [{ debugName: "depth" }] : []));
|
|
813
|
+
/** Whether this item is currently active (set by parent list). */
|
|
814
|
+
isActive = input(false, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
|
|
815
|
+
/** Whether the dropdown is currently expanded (only for dropdown items). */
|
|
816
|
+
isExpanded = input(false, ...(ngDevMode ? [{ debugName: "isExpanded" }] : []));
|
|
817
|
+
/** Optional custom template for rendering the item content. */
|
|
818
|
+
itemTemplate = input(null, ...(ngDevMode ? [{ debugName: "itemTemplate" }] : []));
|
|
819
|
+
/**
|
|
820
|
+
* Forces the item to behave as part of a mobile accordion list.
|
|
821
|
+
* This keeps indicators and layout aligned with the collapsed nav panel.
|
|
822
|
+
*/
|
|
823
|
+
forceAccordionMode = input(false, ...(ngDevMode ? [{ debugName: "forceAccordionMode" }] : []));
|
|
824
|
+
/** Emitted when the item is clicked. */
|
|
825
|
+
clicked = output();
|
|
826
|
+
/** Emitted when the dropdown toggle is activated. */
|
|
827
|
+
toggleDropdown = output();
|
|
828
|
+
/** Reference to the nav state service. */
|
|
829
|
+
state = inject(HubNavStateService);
|
|
830
|
+
/** Whether this item has child items. */
|
|
831
|
+
hasChildren = computed(() => {
|
|
832
|
+
const children = this.item().children;
|
|
833
|
+
return !!children && children.length > 0;
|
|
834
|
+
}, ...(ngDevMode ? [{ debugName: "hasChildren" }] : []));
|
|
835
|
+
/** Whether this item is a header type. */
|
|
836
|
+
isHeader = computed(() => this.item().type === 'header', ...(ngDevMode ? [{ debugName: "isHeader" }] : []));
|
|
837
|
+
/** Whether this item is a link type. */
|
|
838
|
+
isLink = computed(() => this.item().type === 'link', ...(ngDevMode ? [{ debugName: "isLink" }] : []));
|
|
839
|
+
/** Whether this item is a dropdown type. */
|
|
840
|
+
isDropdown = computed(() => this.item().type === 'dropdown', ...(ngDevMode ? [{ debugName: "isDropdown" }] : []));
|
|
841
|
+
/** Whether this item is a custom type. */
|
|
842
|
+
isCustom = computed(() => this.item().type === 'custom', ...(ngDevMode ? [{ debugName: "isCustom" }] : []));
|
|
843
|
+
/** Resolved route for routerLink binding. */
|
|
844
|
+
resolvedRoute = computed(() => {
|
|
845
|
+
const item = this.item();
|
|
846
|
+
if (!item.route) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
return Array.isArray(item.route) ? item.route : [item.route];
|
|
850
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedRoute" }] : []));
|
|
851
|
+
/** Template context for custom item templates. */
|
|
852
|
+
templateContext = computed(() => ({
|
|
853
|
+
$implicit: this.item(),
|
|
854
|
+
active: this.isActive(),
|
|
855
|
+
expanded: this.isExpanded(),
|
|
856
|
+
depth: this.depth()
|
|
857
|
+
}), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
|
|
858
|
+
/**
|
|
859
|
+
* Whether the dropdown caret should be visible for this item.
|
|
860
|
+
* In panel mode, caret is hidden because expansion happens in side panels.
|
|
861
|
+
*/
|
|
862
|
+
showCaret = computed(() => {
|
|
863
|
+
if (!(this.isDropdown() || this.hasChildren())) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
if (this.forceAccordionMode()) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
return this.state.getEffectiveExpandMode(this.item()) !== 'panel';
|
|
870
|
+
}, ...(ngDevMode ? [{ debugName: "showCaret" }] : []));
|
|
871
|
+
/**
|
|
872
|
+
* Handles click events on the item.
|
|
873
|
+
* Delegates to dropdown toggle or emits a click event depending on item type.
|
|
874
|
+
*
|
|
875
|
+
* @param event - The original DOM event.
|
|
876
|
+
*/
|
|
877
|
+
onItemClick(event) {
|
|
878
|
+
const item = this.item();
|
|
879
|
+
if (item.disabled) {
|
|
880
|
+
event.preventDefault();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (item.type === 'header' || item.type === 'separator') {
|
|
884
|
+
event.preventDefault();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (this.hasChildren()) {
|
|
888
|
+
this.toggleDropdown.emit(item);
|
|
889
|
+
}
|
|
890
|
+
this.clicked.emit({ item, event });
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Handles clicks on the label/link area of a routable dropdown item.
|
|
894
|
+
* In this case the click only performs navigation and does not toggle
|
|
895
|
+
* the submenu; submenu expansion is delegated to the dedicated caret.
|
|
896
|
+
*
|
|
897
|
+
* @param event - The original DOM event.
|
|
898
|
+
*/
|
|
899
|
+
onRouteItemClick(event) {
|
|
900
|
+
const item = this.item();
|
|
901
|
+
if (item.disabled) {
|
|
902
|
+
event.preventDefault();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.clicked.emit({ item, event });
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Handles clicks on the dedicated caret button for routable dropdown items.
|
|
909
|
+
* The caret only toggles submenu visibility and must never trigger routing.
|
|
910
|
+
*
|
|
911
|
+
* @param event - The original DOM event.
|
|
912
|
+
*/
|
|
913
|
+
onCaretClick(event) {
|
|
914
|
+
const item = this.item();
|
|
915
|
+
event.preventDefault();
|
|
916
|
+
event.stopPropagation();
|
|
917
|
+
if (item.disabled || !this.hasChildren()) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
this.toggleDropdown.emit(item);
|
|
921
|
+
}
|
|
922
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
923
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: HubNavItemComponent, isStandalone: true, selector: "hub-nav-item", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, isActive: { classPropertyName: "isActive", publicName: "isActive", isSignal: true, isRequired: false, transformFunction: null }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, forceAccordionMode: { classPropertyName: "forceAccordionMode", publicName: "forceAccordionMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked", toggleDropdown: "toggleDropdown" }, host: { attributes: { "role": "none" }, properties: { "class.hub-nav-item--active": "isActive()", "class.hub-nav-item--disabled": "item().disabled", "class.hub-nav-item--has-children": "hasChildren()", "class.hub-nav-item--expanded": "isExpanded()", "class.hub-nav-item--header": "isHeader()", "attr.data-item-id": "item().id" }, classAttribute: "hub-nav-item" }, ngImport: i0, template: "@if (itemTemplate()) {\n\t<!-- Custom template rendering -->\n\t<ng-container [ngTemplateOutlet]=\"itemTemplate()!\" [ngTemplateOutletContext]=\"templateContext()\" />\n} @else if (isHeader()) {\n\t<!-- Header item: non-interactive section label -->\n\t<span class=\"hub-nav-item__header\" role=\"presentation\">{{ item().label }}</span>\n} @else if (isLink() && resolvedRoute()) {\n\t<!-- Link item: routable anchor -->\n\t<a\n\t\tclass=\"hub-nav-item__link\"\n\t\t[class.hub-nav-item__link--active]=\"isActive()\"\n\t\trole=\"menuitem\"\n\t\t[routerLink]=\"resolvedRoute()\"\n\t\t[queryParams]=\"item().queryParams\"\n\t\t[fragment]=\"item().fragment\"\n\t\t[class.hub-nav-item__link--disabled]=\"item().disabled\"\n\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t[attr.aria-current]=\"isActive() ? 'page' : null\"\n\t\t(click)=\"onItemClick($event)\"\n\t>\n\t\t@if (item().icon) {\n\t\t\t<span class=\"hub-nav-item__icon\" [class]=\"item().icon\" aria-hidden=\"true\"></span>\n\t\t}\n\t\t<span class=\"hub-nav-item__label\">{{ item().label }}</span>\n\t\t@if (item().badge) {\n\t\t\t<span class=\"hub-nav-item__badge\" [class]=\"item().badgeClass\">{{ item().badge }}</span>\n\t\t}\n\t</a>\n} @else if ((isDropdown() || hasChildren()) && resolvedRoute()) {\n\t<!-- Routable dropdown: link area navigates, caret area toggles submenu -->\n\t<div class=\"hub-nav-item__compound\">\n\t\t<a\n\t\t\tclass=\"hub-nav-item__link\"\n\t\t\t[class.hub-nav-item__link--split]=\"showCaret()\"\n\t\t\trole=\"menuitem\"\n\t\t\t[class.hub-nav-item__link--active]=\"isActive()\"\n\t\t\t[routerLink]=\"resolvedRoute()\"\n\t\t\t[queryParams]=\"item().queryParams\"\n\t\t\t[fragment]=\"item().fragment\"\n\t\t\t[class.hub-nav-item__link--disabled]=\"item().disabled\"\n\t\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t\t[attr.aria-current]=\"isActive() ? 'page' : null\"\n\t\t\t(click)=\"onRouteItemClick($event)\"\n\t\t>\n\t\t\t@if (item().icon) {\n\t\t\t\t<span class=\"hub-nav-item__icon\" [class]=\"item().icon\" aria-hidden=\"true\"></span>\n\t\t\t}\n\t\t\t<span class=\"hub-nav-item__label\">{{ item().label }}</span>\n\t\t\t@if (item().badge) {\n\t\t\t\t<span class=\"hub-nav-item__badge\" [class]=\"item().badgeClass\">{{ item().badge }}</span>\n\t\t\t}\n\t\t</a>\n\n\t\t@if (showCaret()) {\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"hub-nav-item__caret-button\"\n\t\t\t\t[class.hub-nav-item__caret-button--active]=\"isActive()\"\n\t\t\t\t[class.hub-nav-item__caret-button--expanded]=\"isExpanded()\"\n\t\t\t\t[class.hub-nav-item__caret-button--disabled]=\"item().disabled\"\n\t\t\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t\t\t[attr.aria-label]=\"'Toggle ' + item().label\"\n\t\t\t\t[attr.aria-expanded]=\"isExpanded()\"\n\t\t\t\t[attr.aria-haspopup]=\"true\"\n\t\t\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t\t\t(click)=\"onCaretClick($event)\"\n\t\t\t>\n\t\t\t\t<span class=\"hub-nav-item__caret\" aria-hidden=\"true\"></span>\n\t\t\t</button>\n\t\t}\n\t</div>\n} @else if (isDropdown() || hasChildren()) {\n\t<!-- Dropdown trigger: button that toggles a submenu -->\n\t<button\n\t\ttype=\"button\"\n\t\tclass=\"hub-nav-item__link hub-nav-item__dropdown-toggle\"\n\t\trole=\"menuitem\"\n\t\t[class.hub-nav-item__link--active]=\"isActive()\"\n\t\t[class.hub-nav-item__link--disabled]=\"item().disabled\"\n\t\t[attr.aria-expanded]=\"isExpanded()\"\n\t\t[attr.aria-haspopup]=\"true\"\n\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t(click)=\"onItemClick($event)\"\n\t>\n\t\t@if (item().icon) {\n\t\t\t<span class=\"hub-nav-item__icon\" [class]=\"item().icon\" aria-hidden=\"true\"></span>\n\t\t}\n\t\t<span class=\"hub-nav-item__label\">{{ item().label }}</span>\n\t\t@if (item().badge) {\n\t\t\t<span class=\"hub-nav-item__badge\" [class]=\"item().badgeClass\">{{ item().badge }}</span>\n\t\t}\n\t\t@if (showCaret()) {\n\t\t\t<span class=\"hub-nav-item__caret\" aria-hidden=\"true\"></span>\n\t\t}\n\t</button>\n} @else {\n\t<!-- Fallback: non-routable item (custom or link without route) -->\n\t<button\n\t\ttype=\"button\"\n\t\tclass=\"hub-nav-item__link\"\n\t\trole=\"menuitem\"\n\t\t[class.hub-nav-item__link--disabled]=\"item().disabled\"\n\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t(click)=\"onItemClick($event)\"\n\t>\n\t\t@if (item().icon) {\n\t\t\t<span class=\"hub-nav-item__icon\" [class]=\"item().icon\" aria-hidden=\"true\"></span>\n\t\t}\n\t\t<span class=\"hub-nav-item__label\">{{ item().label }}</span>\n\t\t@if (item().badge) {\n\t\t\t<span class=\"hub-nav-item__badge\" [class]=\"item().badgeClass\">{{ item().badge }}</span>\n\t\t}\n\t</button>\n}\n", styles: [":host{display:block;position:relative}:host-context(.hub-nav-item-list--force-accordion){display:block;width:100%}.hub-nav-item__link{display:flex;align-items:center;gap:var(--hub-nav-icon-gap, .5rem);width:100%;padding:var(--hub-nav-item-padding-y, .5rem) var(--hub-nav-item-padding-x, 1rem);font-size:var(--hub-nav-item-font-size, 1rem);font-weight:var(--hub-nav-item-font-weight, 400);color:var(--hub-nav-item-color, #212529);text-decoration:none;background:none;border:0;border-radius:var(--hub-nav-item-border-radius, .25rem);cursor:pointer;transition:background-color var(--hub-nav-item-transition, .15s ease),color var(--hub-nav-item-transition, .15s ease);white-space:nowrap;font-family:inherit;line-height:1.5;text-align:start}.hub-nav-item__link:hover:not(.hub-nav-item__link--disabled){background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04));color:var(--hub-nav-item-hover-color, #0d6efd)}.hub-nav-item__link--active{background-color:var(--hub-nav-item-active-bg, #0d6efd);color:var(--hub-nav-item-active-color, #fff);font-weight:var(--hub-nav-item-active-font-weight, 600)}.hub-nav-item__link--disabled{color:var(--hub-nav-item-disabled-color, #6c757d);opacity:var(--hub-nav-item-disabled-opacity, .65);pointer-events:none;cursor:default}.hub-nav-item__link:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-item__compound{display:flex;align-items:stretch;width:100%}.hub-nav-item__link--split{flex:1 1 auto;min-width:0;border-start-end-radius:0;border-end-end-radius:0}.hub-nav-item__caret-button{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;min-width:calc(var(--hub-nav-item-font-size, 1rem) * 2.5);padding:0 var(--hub-nav-item-padding-x, 1rem);color:var(--hub-nav-item-color, #212529);background:none;border:0;border-start-start-radius:0;border-end-start-radius:0;border-start-end-radius:var(--hub-nav-item-border-radius, .25rem);border-end-end-radius:var(--hub-nav-item-border-radius, .25rem);cursor:pointer;transition:background-color var(--hub-nav-item-transition, .15s ease),color var(--hub-nav-item-transition, .15s ease)}.hub-nav-item__caret-button:hover:not(.hub-nav-item__caret-button--disabled){background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04));color:var(--hub-nav-item-hover-color, #0d6efd)}.hub-nav-item__caret-button--active{background-color:var(--hub-nav-item-active-bg, #0d6efd);color:var(--hub-nav-item-active-color, #fff)}.hub-nav-item__caret-button--disabled{color:var(--hub-nav-item-disabled-color, #6c757d);opacity:var(--hub-nav-item-disabled-opacity, .65);cursor:default}.hub-nav-item__caret-button:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-item__caret-button--expanded:not(.hub-nav-item__caret-button--active){background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04));color:var(--hub-nav-item-hover-color, #0d6efd)}.hub-nav-item__icon{display:inline-flex;align-items:center;justify-content:center;width:var(--hub-nav-icon-size, 1.25rem);height:var(--hub-nav-icon-size, 1.25rem);flex-shrink:0}.hub-nav-item__label{flex:1;overflow:hidden;text-overflow:ellipsis}.hub-nav-item__badge{display:inline-flex;align-items:center;justify-content:center;padding:var(--hub-nav-badge-padding-y, .25em) var(--hub-nav-badge-padding-x, .5em);font-size:var(--hub-nav-badge-font-size, .75rem);font-weight:600;line-height:1;color:var(--hub-nav-badge-color, #fff);background-color:var(--hub-nav-badge-bg, #dc3545);border-radius:var(--hub-nav-badge-border-radius, 50rem);flex-shrink:0}.hub-nav-item__caret{display:inline-block;width:0;height:0;vertical-align:middle;border-top:var(--hub-nav-caret-size, .3rem) solid var(--hub-nav-caret-color, currentColor);border-right:var(--hub-nav-caret-size, .3rem) solid transparent;border-left:var(--hub-nav-caret-size, .3rem) solid transparent;transition:transform var(--hub-nav-item-transition, .15s ease);flex-shrink:0}:host(.hub-nav-item--expanded) .hub-nav-item__caret{transform:rotate(180deg)}:host(.hub-nav-item--expanded) .hub-nav-item__link:not(.hub-nav-item__link--active){background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04));color:var(--hub-nav-item-hover-color, #0d6efd);font-weight:var(--hub-nav-item-active-font-weight, 600)}.hub-nav-item__header{display:block;padding:var(--hub-nav-header-padding-y, .25rem) var(--hub-nav-header-padding-x, 1rem);margin-top:var(--hub-nav-header-margin-top, .75rem);font-size:var(--hub-nav-header-font-size, .875rem);font-weight:var(--hub-nav-header-font-weight, 600);color:var(--hub-nav-header-color, #6c757d);text-transform:uppercase;letter-spacing:.05em;cursor:default;white-space:nowrap}:host-context(.hub-nav--horizontal) .hub-nav-item__header{display:inline-flex;align-items:center;margin-top:0;margin-inline-start:0;padding-block:var(--hub-nav-header-padding-y, .25rem);padding-inline-end:var(--hub-nav-header-padding-x, 1rem);padding-inline-start:var(--hub-nav-header-horizontal-padding-inline-start, 2rem)}:host-context(.hub-nav-dropdown) .hub-nav-item__header,:host-context(.hub-nav-accordion) .hub-nav-item__header,:host-context(.hub-nav-panel__body) .hub-nav-item__header,:host-context(.hub-nav-mobile-panel__body) .hub-nav-item__header{display:block;margin-top:var(--hub-nav-header-margin-top, .75rem);margin-inline-start:0}:host-context(.hub-nav--horizontal){display:inline-flex}:host-context(.hub-nav--horizontal) .hub-nav-item__link{white-space:nowrap}:host-context(.hub-nav--horizontal):host-context(.hub-nav-dropdown) .hub-nav-item__caret{border-top:var(--hub-nav-caret-size, .3rem) solid transparent;border-bottom:var(--hub-nav-caret-size, .3rem) solid transparent;border-left:var(--hub-nav-caret-size, .3rem) solid var(--hub-nav-caret-color, currentColor);border-right:0}:host-context(.hub-nav-dropdown){display:block}:host-context(.hub-nav-item-list--force-accordion) .hub-nav-item__link{width:100%}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
924
|
+
}
|
|
925
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavItemComponent, decorators: [{
|
|
926
|
+
type: Component,
|
|
927
|
+
args: [{ selector: 'hub-nav-item', standalone: true, imports: [NgTemplateOutlet, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
928
|
+
class: 'hub-nav-item',
|
|
929
|
+
'[class.hub-nav-item--active]': 'isActive()',
|
|
930
|
+
'[class.hub-nav-item--disabled]': 'item().disabled',
|
|
931
|
+
'[class.hub-nav-item--has-children]': 'hasChildren()',
|
|
932
|
+
'[class.hub-nav-item--expanded]': 'isExpanded()',
|
|
933
|
+
'[class.hub-nav-item--header]': 'isHeader()',
|
|
934
|
+
'[attr.data-item-id]': 'item().id',
|
|
935
|
+
role: 'none'
|
|
936
|
+
}, template: "@if (itemTemplate()) {\n\t<!-- Custom template rendering -->\n\t<ng-container [ngTemplateOutlet]=\"itemTemplate()!\" [ngTemplateOutletContext]=\"templateContext()\" />\n} @else if (isHeader()) {\n\t<!-- Header item: non-interactive section label -->\n\t<span class=\"hub-nav-item__header\" role=\"presentation\">{{ item().label }}</span>\n} @else if (isLink() && resolvedRoute()) {\n\t<!-- Link item: routable anchor -->\n\t<a\n\t\tclass=\"hub-nav-item__link\"\n\t\t[class.hub-nav-item__link--active]=\"isActive()\"\n\t\trole=\"menuitem\"\n\t\t[routerLink]=\"resolvedRoute()\"\n\t\t[queryParams]=\"item().queryParams\"\n\t\t[fragment]=\"item().fragment\"\n\t\t[class.hub-nav-item__link--disabled]=\"item().disabled\"\n\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t[attr.aria-current]=\"isActive() ? 'page' : null\"\n\t\t(click)=\"onItemClick($event)\"\n\t>\n\t\t@if (item().icon) {\n\t\t\t<span class=\"hub-nav-item__icon\" [class]=\"item().icon\" aria-hidden=\"true\"></span>\n\t\t}\n\t\t<span class=\"hub-nav-item__label\">{{ item().label }}</span>\n\t\t@if (item().badge) {\n\t\t\t<span class=\"hub-nav-item__badge\" [class]=\"item().badgeClass\">{{ item().badge }}</span>\n\t\t}\n\t</a>\n} @else if ((isDropdown() || hasChildren()) && resolvedRoute()) {\n\t<!-- Routable dropdown: link area navigates, caret area toggles submenu -->\n\t<div class=\"hub-nav-item__compound\">\n\t\t<a\n\t\t\tclass=\"hub-nav-item__link\"\n\t\t\t[class.hub-nav-item__link--split]=\"showCaret()\"\n\t\t\trole=\"menuitem\"\n\t\t\t[class.hub-nav-item__link--active]=\"isActive()\"\n\t\t\t[routerLink]=\"resolvedRoute()\"\n\t\t\t[queryParams]=\"item().queryParams\"\n\t\t\t[fragment]=\"item().fragment\"\n\t\t\t[class.hub-nav-item__link--disabled]=\"item().disabled\"\n\t\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t\t[attr.aria-current]=\"isActive() ? 'page' : null\"\n\t\t\t(click)=\"onRouteItemClick($event)\"\n\t\t>\n\t\t\t@if (item().icon) {\n\t\t\t\t<span class=\"hub-nav-item__icon\" [class]=\"item().icon\" aria-hidden=\"true\"></span>\n\t\t\t}\n\t\t\t<span class=\"hub-nav-item__label\">{{ item().label }}</span>\n\t\t\t@if (item().badge) {\n\t\t\t\t<span class=\"hub-nav-item__badge\" [class]=\"item().badgeClass\">{{ item().badge }}</span>\n\t\t\t}\n\t\t</a>\n\n\t\t@if (showCaret()) {\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"hub-nav-item__caret-button\"\n\t\t\t\t[class.hub-nav-item__caret-button--active]=\"isActive()\"\n\t\t\t\t[class.hub-nav-item__caret-button--expanded]=\"isExpanded()\"\n\t\t\t\t[class.hub-nav-item__caret-button--disabled]=\"item().disabled\"\n\t\t\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t\t\t[attr.aria-label]=\"'Toggle ' + item().label\"\n\t\t\t\t[attr.aria-expanded]=\"isExpanded()\"\n\t\t\t\t[attr.aria-haspopup]=\"true\"\n\t\t\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t\t\t(click)=\"onCaretClick($event)\"\n\t\t\t>\n\t\t\t\t<span class=\"hub-nav-item__caret\" aria-hidden=\"true\"></span>\n\t\t\t</button>\n\t\t}\n\t</div>\n} @else if (isDropdown() || hasChildren()) {\n\t<!-- Dropdown trigger: button that toggles a submenu -->\n\t<button\n\t\ttype=\"button\"\n\t\tclass=\"hub-nav-item__link hub-nav-item__dropdown-toggle\"\n\t\trole=\"menuitem\"\n\t\t[class.hub-nav-item__link--active]=\"isActive()\"\n\t\t[class.hub-nav-item__link--disabled]=\"item().disabled\"\n\t\t[attr.aria-expanded]=\"isExpanded()\"\n\t\t[attr.aria-haspopup]=\"true\"\n\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t(click)=\"onItemClick($event)\"\n\t>\n\t\t@if (item().icon) {\n\t\t\t<span class=\"hub-nav-item__icon\" [class]=\"item().icon\" aria-hidden=\"true\"></span>\n\t\t}\n\t\t<span class=\"hub-nav-item__label\">{{ item().label }}</span>\n\t\t@if (item().badge) {\n\t\t\t<span class=\"hub-nav-item__badge\" [class]=\"item().badgeClass\">{{ item().badge }}</span>\n\t\t}\n\t\t@if (showCaret()) {\n\t\t\t<span class=\"hub-nav-item__caret\" aria-hidden=\"true\"></span>\n\t\t}\n\t</button>\n} @else {\n\t<!-- Fallback: non-routable item (custom or link without route) -->\n\t<button\n\t\ttype=\"button\"\n\t\tclass=\"hub-nav-item__link\"\n\t\trole=\"menuitem\"\n\t\t[class.hub-nav-item__link--disabled]=\"item().disabled\"\n\t\t[attr.tabindex]=\"item().disabled ? -1 : 0\"\n\t\t[attr.aria-disabled]=\"item().disabled || null\"\n\t\t(click)=\"onItemClick($event)\"\n\t>\n\t\t@if (item().icon) {\n\t\t\t<span class=\"hub-nav-item__icon\" [class]=\"item().icon\" aria-hidden=\"true\"></span>\n\t\t}\n\t\t<span class=\"hub-nav-item__label\">{{ item().label }}</span>\n\t\t@if (item().badge) {\n\t\t\t<span class=\"hub-nav-item__badge\" [class]=\"item().badgeClass\">{{ item().badge }}</span>\n\t\t}\n\t</button>\n}\n", styles: [":host{display:block;position:relative}:host-context(.hub-nav-item-list--force-accordion){display:block;width:100%}.hub-nav-item__link{display:flex;align-items:center;gap:var(--hub-nav-icon-gap, .5rem);width:100%;padding:var(--hub-nav-item-padding-y, .5rem) var(--hub-nav-item-padding-x, 1rem);font-size:var(--hub-nav-item-font-size, 1rem);font-weight:var(--hub-nav-item-font-weight, 400);color:var(--hub-nav-item-color, #212529);text-decoration:none;background:none;border:0;border-radius:var(--hub-nav-item-border-radius, .25rem);cursor:pointer;transition:background-color var(--hub-nav-item-transition, .15s ease),color var(--hub-nav-item-transition, .15s ease);white-space:nowrap;font-family:inherit;line-height:1.5;text-align:start}.hub-nav-item__link:hover:not(.hub-nav-item__link--disabled){background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04));color:var(--hub-nav-item-hover-color, #0d6efd)}.hub-nav-item__link--active{background-color:var(--hub-nav-item-active-bg, #0d6efd);color:var(--hub-nav-item-active-color, #fff);font-weight:var(--hub-nav-item-active-font-weight, 600)}.hub-nav-item__link--disabled{color:var(--hub-nav-item-disabled-color, #6c757d);opacity:var(--hub-nav-item-disabled-opacity, .65);pointer-events:none;cursor:default}.hub-nav-item__link:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-item__compound{display:flex;align-items:stretch;width:100%}.hub-nav-item__link--split{flex:1 1 auto;min-width:0;border-start-end-radius:0;border-end-end-radius:0}.hub-nav-item__caret-button{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;min-width:calc(var(--hub-nav-item-font-size, 1rem) * 2.5);padding:0 var(--hub-nav-item-padding-x, 1rem);color:var(--hub-nav-item-color, #212529);background:none;border:0;border-start-start-radius:0;border-end-start-radius:0;border-start-end-radius:var(--hub-nav-item-border-radius, .25rem);border-end-end-radius:var(--hub-nav-item-border-radius, .25rem);cursor:pointer;transition:background-color var(--hub-nav-item-transition, .15s ease),color var(--hub-nav-item-transition, .15s ease)}.hub-nav-item__caret-button:hover:not(.hub-nav-item__caret-button--disabled){background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04));color:var(--hub-nav-item-hover-color, #0d6efd)}.hub-nav-item__caret-button--active{background-color:var(--hub-nav-item-active-bg, #0d6efd);color:var(--hub-nav-item-active-color, #fff)}.hub-nav-item__caret-button--disabled{color:var(--hub-nav-item-disabled-color, #6c757d);opacity:var(--hub-nav-item-disabled-opacity, .65);cursor:default}.hub-nav-item__caret-button:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-item__caret-button--expanded:not(.hub-nav-item__caret-button--active){background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04));color:var(--hub-nav-item-hover-color, #0d6efd)}.hub-nav-item__icon{display:inline-flex;align-items:center;justify-content:center;width:var(--hub-nav-icon-size, 1.25rem);height:var(--hub-nav-icon-size, 1.25rem);flex-shrink:0}.hub-nav-item__label{flex:1;overflow:hidden;text-overflow:ellipsis}.hub-nav-item__badge{display:inline-flex;align-items:center;justify-content:center;padding:var(--hub-nav-badge-padding-y, .25em) var(--hub-nav-badge-padding-x, .5em);font-size:var(--hub-nav-badge-font-size, .75rem);font-weight:600;line-height:1;color:var(--hub-nav-badge-color, #fff);background-color:var(--hub-nav-badge-bg, #dc3545);border-radius:var(--hub-nav-badge-border-radius, 50rem);flex-shrink:0}.hub-nav-item__caret{display:inline-block;width:0;height:0;vertical-align:middle;border-top:var(--hub-nav-caret-size, .3rem) solid var(--hub-nav-caret-color, currentColor);border-right:var(--hub-nav-caret-size, .3rem) solid transparent;border-left:var(--hub-nav-caret-size, .3rem) solid transparent;transition:transform var(--hub-nav-item-transition, .15s ease);flex-shrink:0}:host(.hub-nav-item--expanded) .hub-nav-item__caret{transform:rotate(180deg)}:host(.hub-nav-item--expanded) .hub-nav-item__link:not(.hub-nav-item__link--active){background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04));color:var(--hub-nav-item-hover-color, #0d6efd);font-weight:var(--hub-nav-item-active-font-weight, 600)}.hub-nav-item__header{display:block;padding:var(--hub-nav-header-padding-y, .25rem) var(--hub-nav-header-padding-x, 1rem);margin-top:var(--hub-nav-header-margin-top, .75rem);font-size:var(--hub-nav-header-font-size, .875rem);font-weight:var(--hub-nav-header-font-weight, 600);color:var(--hub-nav-header-color, #6c757d);text-transform:uppercase;letter-spacing:.05em;cursor:default;white-space:nowrap}:host-context(.hub-nav--horizontal) .hub-nav-item__header{display:inline-flex;align-items:center;margin-top:0;margin-inline-start:0;padding-block:var(--hub-nav-header-padding-y, .25rem);padding-inline-end:var(--hub-nav-header-padding-x, 1rem);padding-inline-start:var(--hub-nav-header-horizontal-padding-inline-start, 2rem)}:host-context(.hub-nav-dropdown) .hub-nav-item__header,:host-context(.hub-nav-accordion) .hub-nav-item__header,:host-context(.hub-nav-panel__body) .hub-nav-item__header,:host-context(.hub-nav-mobile-panel__body) .hub-nav-item__header{display:block;margin-top:var(--hub-nav-header-margin-top, .75rem);margin-inline-start:0}:host-context(.hub-nav--horizontal){display:inline-flex}:host-context(.hub-nav--horizontal) .hub-nav-item__link{white-space:nowrap}:host-context(.hub-nav--horizontal):host-context(.hub-nav-dropdown) .hub-nav-item__caret{border-top:var(--hub-nav-caret-size, .3rem) solid transparent;border-bottom:var(--hub-nav-caret-size, .3rem) solid transparent;border-left:var(--hub-nav-caret-size, .3rem) solid var(--hub-nav-caret-color, currentColor);border-right:0}:host-context(.hub-nav-dropdown){display:block}:host-context(.hub-nav-item-list--force-accordion) .hub-nav-item__link{width:100%}\n"] }]
|
|
937
|
+
}], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], isActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "isActive", required: false }] }], isExpanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "isExpanded", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], forceAccordionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceAccordionMode", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }], toggleDropdown: [{ type: i0.Output, args: ["toggleDropdown"] }] } });
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Visual separator between navigation item groups.
|
|
941
|
+
* Renders as a horizontal rule in vertical list contexts.
|
|
942
|
+
* Root horizontal nav bars hide separators because structural grouping
|
|
943
|
+
* should not consume inline menu space there.
|
|
944
|
+
*
|
|
945
|
+
* @internal This component is used internally by `HubNavItemListComponent`.
|
|
946
|
+
*/
|
|
947
|
+
class HubNavSeparatorComponent {
|
|
948
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavSeparatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
949
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.2", type: HubNavSeparatorComponent, isStandalone: true, selector: "hub-nav-separator", host: { attributes: { "role": "separator" }, classAttribute: "hub-nav-separator" }, ngImport: i0, template: '', isInline: true, styles: [":host{display:block;border:0;opacity:1}:host-context(.hub-nav--horizontal){display:none}:host-context(.hub-nav--vertical){height:0;border-top:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-separator-color, var(--hub-sys-border-color-default, #dee2e6));margin:var(--hub-nav-separator-margin-y, .5rem) 0}:host-context(.hub-nav-dropdown),:host-context(.hub-nav-accordion),:host-context(.hub-nav-panel__body),:host-context(.hub-nav-mobile-panel__body){display:block;width:100%;height:0;align-self:stretch;border-left:0;border-top:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-separator-color, var(--hub-sys-border-color-default, #dee2e6));margin:var(--hub-nav-separator-margin-y, .5rem) 0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
950
|
+
}
|
|
951
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavSeparatorComponent, decorators: [{
|
|
952
|
+
type: Component,
|
|
953
|
+
args: [{ selector: 'hub-nav-separator', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
954
|
+
class: 'hub-nav-separator',
|
|
955
|
+
role: 'separator'
|
|
956
|
+
}, template: '', styles: [":host{display:block;border:0;opacity:1}:host-context(.hub-nav--horizontal){display:none}:host-context(.hub-nav--vertical){height:0;border-top:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-separator-color, var(--hub-sys-border-color-default, #dee2e6));margin:var(--hub-nav-separator-margin-y, .5rem) 0}:host-context(.hub-nav-dropdown),:host-context(.hub-nav-accordion),:host-context(.hub-nav-panel__body),:host-context(.hub-nav-mobile-panel__body){display:block;width:100%;height:0;align-self:stretch;border-left:0;border-top:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-separator-color, var(--hub-sys-border-color-default, #dee2e6));margin:var(--hub-nav-separator-margin-y, .5rem) 0}\n"] }]
|
|
957
|
+
}] });
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Renders a list of navigation items at a given depth level.
|
|
961
|
+
* Self-recursive: when an item has children and its dropdown is open,
|
|
962
|
+
* this component renders another `hub-nav-item-list` for the children.
|
|
963
|
+
*
|
|
964
|
+
* Includes keyboard navigation following WAI-ARIA menubar/menu patterns.
|
|
965
|
+
*
|
|
966
|
+
* @internal Used internally by `HubNavComponent`.
|
|
967
|
+
*/
|
|
968
|
+
class HubNavItemListComponent {
|
|
969
|
+
/** The list of items to render. */
|
|
970
|
+
items = input.required(...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
971
|
+
/** Nesting depth (0 = root level). */
|
|
972
|
+
depth = input(0, ...(ngDevMode ? [{ debugName: "depth" }] : []));
|
|
973
|
+
/** Optional custom template for rendering item content. */
|
|
974
|
+
itemTemplate = input(null, ...(ngDevMode ? [{ debugName: "itemTemplate" }] : []));
|
|
975
|
+
/**
|
|
976
|
+
* Forces all nested items in this list to use accordion expansion.
|
|
977
|
+
* Primarily used by the collapsed/mobile panel to avoid flyout/panel
|
|
978
|
+
* behaviors that are not suitable for small viewports.
|
|
979
|
+
*/
|
|
980
|
+
forceAccordionMode = input(false, ...(ngDevMode ? [{ debugName: "forceAccordionMode" }] : []));
|
|
981
|
+
/** Emitted when any item in the list is clicked. */
|
|
982
|
+
itemClick = output();
|
|
983
|
+
/** Emitted when a dropdown toggle is requested. */
|
|
984
|
+
dropdownToggle = output();
|
|
985
|
+
/** Emitted when an item with panel expand mode is clicked and needs a new panel. */
|
|
986
|
+
panelOpen = output();
|
|
987
|
+
/** Reference to the nav state service. */
|
|
988
|
+
state = inject(HubNavStateService);
|
|
989
|
+
/** Reference to the Angular router for active state detection. */
|
|
990
|
+
router = inject(Router);
|
|
991
|
+
/** Reactive URL value used to recompute active states on navigation changes. */
|
|
992
|
+
currentUrl = signal(this.router.url, ...(ngDevMode ? [{ debugName: "currentUrl" }] : []));
|
|
993
|
+
/** Router events subscription lifecycle handle. */
|
|
994
|
+
routerSub;
|
|
995
|
+
/** Host element reference. */
|
|
996
|
+
el = inject((ElementRef));
|
|
997
|
+
/**
|
|
998
|
+
* Whether children should render in accordion mode (inline with indentation).
|
|
999
|
+
* True when orientation is vertical and expand mode is 'accordion'.
|
|
1000
|
+
*/
|
|
1001
|
+
isAccordionMode = computed(() => this.forceAccordionMode() ||
|
|
1002
|
+
(this.state.orientation() === 'vertical' && this.state.verticalExpandMode() === 'accordion'), ...(ngDevMode ? [{ debugName: "isAccordionMode" }] : []));
|
|
1003
|
+
/** Whether this is a horizontal root-level nav (affects arrow key mapping). */
|
|
1004
|
+
get isHorizontalRoot() {
|
|
1005
|
+
if (this.forceAccordionMode()) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
return this.state.orientation() === 'horizontal' && this.depth() === 0;
|
|
1009
|
+
}
|
|
1010
|
+
// ──────────────────────────────────────────────
|
|
1011
|
+
// Data methods
|
|
1012
|
+
// ──────────────────────────────────────────────
|
|
1013
|
+
/**
|
|
1014
|
+
* Checks if the given item or any of its descendants is currently active.
|
|
1015
|
+
*
|
|
1016
|
+
* @param item - The item to check.
|
|
1017
|
+
* @returns `true` if the item or a descendant matches the current route.
|
|
1018
|
+
*/
|
|
1019
|
+
isItemActive(item) {
|
|
1020
|
+
return this.state.isItemOrDescendantActive(item, this.currentUrl());
|
|
1021
|
+
}
|
|
1022
|
+
/** @inheritDoc */
|
|
1023
|
+
ngOnInit() {
|
|
1024
|
+
this.routerSub = this.router.events
|
|
1025
|
+
.pipe(filter((event) => event instanceof NavigationEnd))
|
|
1026
|
+
.subscribe(() => {
|
|
1027
|
+
this.currentUrl.set(this.router.url);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
/** @inheritDoc */
|
|
1031
|
+
ngOnDestroy() {
|
|
1032
|
+
this.routerSub?.unsubscribe();
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Checks if a dropdown is currently expanded.
|
|
1036
|
+
*
|
|
1037
|
+
* @param item - The dropdown item to check.
|
|
1038
|
+
* @returns `true` if the dropdown is open.
|
|
1039
|
+
*/
|
|
1040
|
+
isExpanded(item) {
|
|
1041
|
+
// In panel mode, "expanded" means "there is an open panel for this item".
|
|
1042
|
+
if (this.getItemExpandMode(item) === 'panel') {
|
|
1043
|
+
return this.state.isPanelOpenForItem(item.id);
|
|
1044
|
+
}
|
|
1045
|
+
return this.state.isDropdownOpen(item.id);
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Whether the given item has child items.
|
|
1049
|
+
*
|
|
1050
|
+
* @param item - The item to check.
|
|
1051
|
+
* @returns `true` if the item has children.
|
|
1052
|
+
*/
|
|
1053
|
+
hasChildren(item) {
|
|
1054
|
+
return !!item.children && item.children.length > 0;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Resolves the effective expand mode for an item.
|
|
1058
|
+
* Considers per-item override, global config, and mobile fallback.
|
|
1059
|
+
*
|
|
1060
|
+
* @param item - The item to resolve the expand mode for.
|
|
1061
|
+
* @returns The effective expand mode.
|
|
1062
|
+
*/
|
|
1063
|
+
getItemExpandMode(item) {
|
|
1064
|
+
if (this.forceAccordionMode()) {
|
|
1065
|
+
return 'accordion';
|
|
1066
|
+
}
|
|
1067
|
+
return this.state.getEffectiveExpandMode(item);
|
|
1068
|
+
}
|
|
1069
|
+
// ──────────────────────────────────────────────
|
|
1070
|
+
// Event handlers
|
|
1071
|
+
// ──────────────────────────────────────────────
|
|
1072
|
+
/**
|
|
1073
|
+
* Handles click events from a child item component.
|
|
1074
|
+
*
|
|
1075
|
+
* @param payload - The item and original event.
|
|
1076
|
+
*/
|
|
1077
|
+
onItemClick(payload) {
|
|
1078
|
+
this.itemClick.emit(payload);
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Handles dropdown toggle events from a child item component.
|
|
1082
|
+
* In panel mode, emits `panelOpen` instead of toggling the dropdown.
|
|
1083
|
+
*
|
|
1084
|
+
* @param item - The dropdown item to toggle.
|
|
1085
|
+
*/
|
|
1086
|
+
onDropdownToggle(item) {
|
|
1087
|
+
const mode = this.getItemExpandMode(item);
|
|
1088
|
+
if (mode === 'panel') {
|
|
1089
|
+
this.panelOpen.emit(item);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
// Get sibling IDs for accordion auto-close behavior
|
|
1093
|
+
const siblingIds = this.items()
|
|
1094
|
+
.filter((i) => i.type === 'dropdown' || this.hasChildren(i))
|
|
1095
|
+
.map((i) => i.id);
|
|
1096
|
+
if (this.state.isDropdownOpen(item.id)) {
|
|
1097
|
+
this.state.closeDropdown(item.id);
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
this.state.openDropdown(item.id, siblingIds);
|
|
1101
|
+
}
|
|
1102
|
+
this.dropdownToggle.emit(item);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Handles hover enter events on items with children.
|
|
1106
|
+
* Opens the dropdown if the trigger mode includes hover.
|
|
1107
|
+
*
|
|
1108
|
+
* @param item - The hovered item.
|
|
1109
|
+
*/
|
|
1110
|
+
onItemMouseEnter(item) {
|
|
1111
|
+
if (this.forceAccordionMode()) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const trigger = this.state.dropdownTrigger();
|
|
1115
|
+
if ((trigger === 'hover' || trigger === 'both') && this.hasChildren(item)) {
|
|
1116
|
+
// Get sibling IDs for accordion auto-close behavior
|
|
1117
|
+
const siblingIds = this.items()
|
|
1118
|
+
.filter((i) => i.type === 'dropdown' || this.hasChildren(i))
|
|
1119
|
+
.map((i) => i.id);
|
|
1120
|
+
this.state.openDropdown(item.id, siblingIds);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Handles hover leave events on items with children.
|
|
1125
|
+
* Closes the dropdown if the trigger mode includes hover.
|
|
1126
|
+
*
|
|
1127
|
+
* @param item - The item that lost hover.
|
|
1128
|
+
*/
|
|
1129
|
+
onItemMouseLeave(item) {
|
|
1130
|
+
if (this.forceAccordionMode()) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const trigger = this.state.dropdownTrigger();
|
|
1134
|
+
if ((trigger === 'hover' || trigger === 'both') && this.hasChildren(item)) {
|
|
1135
|
+
this.state.closeDropdown(item.id);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// ──────────────────────────────────────────────
|
|
1139
|
+
// Keyboard navigation (WAI-ARIA menubar/menu)
|
|
1140
|
+
// ──────────────────────────────────────────────
|
|
1141
|
+
/**
|
|
1142
|
+
* Handles keyboard events for accessible navigation.
|
|
1143
|
+
*
|
|
1144
|
+
* @param event - The keyboard event.
|
|
1145
|
+
*/
|
|
1146
|
+
onKeyDown(event) {
|
|
1147
|
+
const items = this.getFocusableItems();
|
|
1148
|
+
const currentIndex = this.getCurrentFocusIndex(items);
|
|
1149
|
+
switch (event.key) {
|
|
1150
|
+
case 'ArrowRight':
|
|
1151
|
+
event.preventDefault();
|
|
1152
|
+
event.stopPropagation();
|
|
1153
|
+
if (this.isHorizontalRoot) {
|
|
1154
|
+
this.focusItem(items, currentIndex + 1);
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
this.openFocusedDropdown(items, currentIndex);
|
|
1158
|
+
}
|
|
1159
|
+
break;
|
|
1160
|
+
case 'ArrowLeft':
|
|
1161
|
+
event.preventDefault();
|
|
1162
|
+
event.stopPropagation();
|
|
1163
|
+
if (this.isHorizontalRoot) {
|
|
1164
|
+
this.focusItem(items, currentIndex - 1);
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
this.closeFocusedDropdown(items, currentIndex);
|
|
1168
|
+
}
|
|
1169
|
+
break;
|
|
1170
|
+
case 'ArrowDown':
|
|
1171
|
+
event.preventDefault();
|
|
1172
|
+
event.stopPropagation();
|
|
1173
|
+
if (this.isHorizontalRoot) {
|
|
1174
|
+
this.openFocusedDropdown(items, currentIndex);
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
this.focusItem(items, currentIndex + 1);
|
|
1178
|
+
}
|
|
1179
|
+
break;
|
|
1180
|
+
case 'ArrowUp':
|
|
1181
|
+
event.preventDefault();
|
|
1182
|
+
event.stopPropagation();
|
|
1183
|
+
if (this.isHorizontalRoot) {
|
|
1184
|
+
this.closeFocusedDropdown(items, currentIndex);
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
this.focusItem(items, currentIndex - 1);
|
|
1188
|
+
}
|
|
1189
|
+
break;
|
|
1190
|
+
case 'Home':
|
|
1191
|
+
event.preventDefault();
|
|
1192
|
+
event.stopPropagation();
|
|
1193
|
+
this.focusItem(items, 0);
|
|
1194
|
+
break;
|
|
1195
|
+
case 'End':
|
|
1196
|
+
event.preventDefault();
|
|
1197
|
+
event.stopPropagation();
|
|
1198
|
+
this.focusItem(items, items.length - 1);
|
|
1199
|
+
break;
|
|
1200
|
+
case 'Escape':
|
|
1201
|
+
event.preventDefault();
|
|
1202
|
+
event.stopPropagation();
|
|
1203
|
+
this.state.closeAllDropdowns();
|
|
1204
|
+
// Return focus to parent trigger if in a submenu
|
|
1205
|
+
if (this.depth() > 0) {
|
|
1206
|
+
const parentWrapper = this.el.nativeElement.closest('.hub-nav-item-wrapper');
|
|
1207
|
+
const trigger = parentWrapper?.querySelector(':scope > hub-nav-item .hub-nav-item__dropdown-toggle');
|
|
1208
|
+
trigger?.focus();
|
|
1209
|
+
}
|
|
1210
|
+
break;
|
|
1211
|
+
default:
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
/** Returns all focusable item elements (direct children only). */
|
|
1216
|
+
getFocusableItems() {
|
|
1217
|
+
const host = this.el.nativeElement;
|
|
1218
|
+
const selectors = ':scope > .hub-nav-item-wrapper > hub-nav-item .hub-nav-item__link,' +
|
|
1219
|
+
':scope > .hub-nav-item-wrapper > hub-nav-item .hub-nav-item__dropdown-toggle';
|
|
1220
|
+
const elements = host.querySelectorAll(selectors);
|
|
1221
|
+
return Array.from(elements).filter((el) => el.getAttribute('aria-disabled') !== 'true' && el.tabIndex !== -1);
|
|
1222
|
+
}
|
|
1223
|
+
/** Returns the index of the currently focused item. */
|
|
1224
|
+
getCurrentFocusIndex(items) {
|
|
1225
|
+
return items.indexOf(document.activeElement);
|
|
1226
|
+
}
|
|
1227
|
+
/** Moves focus to the item at the given index (wraps around). */
|
|
1228
|
+
focusItem(items, index) {
|
|
1229
|
+
if (items.length === 0) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const wrapped = ((index % items.length) + items.length) % items.length;
|
|
1233
|
+
items[wrapped]?.focus();
|
|
1234
|
+
}
|
|
1235
|
+
/** Opens the dropdown of the focused item and focuses its first child. */
|
|
1236
|
+
openFocusedDropdown(items, currentIndex) {
|
|
1237
|
+
if (currentIndex < 0) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const wrapper = items[currentIndex].closest('.hub-nav-item-wrapper');
|
|
1241
|
+
const itemId = wrapper?.querySelector('hub-nav-item')?.getAttribute('data-item-id');
|
|
1242
|
+
if (itemId) {
|
|
1243
|
+
// Get sibling IDs for accordion auto-close behavior
|
|
1244
|
+
const siblingIds = this.items()
|
|
1245
|
+
.filter((i) => i.type === 'dropdown' || this.hasChildren(i))
|
|
1246
|
+
.map((i) => i.id);
|
|
1247
|
+
this.state.openDropdown(itemId, siblingIds);
|
|
1248
|
+
requestAnimationFrame(() => {
|
|
1249
|
+
const submenu = wrapper?.querySelector('.hub-nav-dropdown, .hub-nav-accordion');
|
|
1250
|
+
const firstChild = submenu?.querySelector('.hub-nav-item__link, .hub-nav-item__dropdown-toggle');
|
|
1251
|
+
firstChild?.focus();
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/** Closes the dropdown of the focused item. */
|
|
1256
|
+
closeFocusedDropdown(items, currentIndex) {
|
|
1257
|
+
if (currentIndex < 0) {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const wrapper = items[currentIndex].closest('.hub-nav-item-wrapper');
|
|
1261
|
+
const itemId = wrapper?.querySelector('hub-nav-item')?.getAttribute('data-item-id');
|
|
1262
|
+
if (itemId && this.state.isDropdownOpen(itemId)) {
|
|
1263
|
+
this.state.closeDropdown(itemId);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavItemListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1267
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: HubNavItemListComponent, isStandalone: true, selector: "hub-nav-item-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, forceAccordionMode: { classPropertyName: "forceAccordionMode", publicName: "forceAccordionMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", dropdownToggle: "dropdownToggle", panelOpen: "panelOpen" }, host: { listeners: { "keydown": "onKeyDown($event)" }, properties: { "class.hub-nav-item-list--force-accordion": "forceAccordionMode()", "attr.role": "depth() === 0 ? \"menubar\" : \"menu\"" }, classAttribute: "hub-nav-item-list" }, ngImport: i0, template: "@for (item of items(); track item.id) {\n\t@if (item.type === 'separator') {\n\t\t<hub-nav-separator />\n\t} @else {\n\t\t<div\n\t\t\tclass=\"hub-nav-item-wrapper\"\n\t\t\t[class.hub-nav-item-wrapper--has-dropdown]=\"hasChildren(item)\"\n\t\t\t(mouseenter)=\"onItemMouseEnter(item)\"\n\t\t\t(mouseleave)=\"onItemMouseLeave(item)\"\n\t\t>\n\t\t\t<hub-nav-item\n\t\t\t\t[item]=\"item\"\n\t\t\t\t[depth]=\"depth()\"\n\t\t\t\t[isActive]=\"isItemActive(item)\"\n\t\t\t\t[isExpanded]=\"isExpanded(item)\"\n\t\t\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t\t\t[forceAccordionMode]=\"forceAccordionMode()\"\n\t\t\t\t(clicked)=\"onItemClick($event)\"\n\t\t\t\t(toggleDropdown)=\"onDropdownToggle($event)\"\n\t\t\t/>\n\n\t\t\t<!-- Children: accordion (inline), flyout/dropdown (positioned), or panel (external) -->\n\t\t\t@if (hasChildren(item) && isExpanded(item) && getItemExpandMode(item) !== 'panel') {\n\t\t\t\t@if (getItemExpandMode(item) === 'accordion') {\n\t\t\t\t\t<!-- Accordion: children expand inline with indentation -->\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"hub-nav-accordion\"\n\t\t\t\t\t\trole=\"group\"\n\t\t\t\t\t\t[style.padding-left]=\"'var(--hub-nav-accordion-indent, 1rem)'\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<hub-nav-item-list\n\t\t\t\t\t\t\t[items]=\"item.children!\"\n\t\t\t\t\t\t\t[depth]=\"depth() + 1\"\n\t\t\t\t\t\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t\t\t\t\t\t[forceAccordionMode]=\"forceAccordionMode()\"\n\t\t\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t\t\t(dropdownToggle)=\"dropdownToggle.emit($event)\"\n\t\t\t\t\t\t\t(panelOpen)=\"panelOpen.emit($event)\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t} @else {\n\t\t\t\t\t<!-- Flyout / Dropdown: positioned panel -->\n\t\t\t\t\t<div class=\"hub-nav-dropdown\" role=\"menu\">\n\t\t\t\t\t\t<hub-nav-item-list\n\t\t\t\t\t\t\t[items]=\"item.children!\"\n\t\t\t\t\t\t\t[depth]=\"depth() + 1\"\n\t\t\t\t\t\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t\t\t\t\t\t[forceAccordionMode]=\"forceAccordionMode()\"\n\t\t\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t\t\t(dropdownToggle)=\"dropdownToggle.emit($event)\"\n\t\t\t\t\t\t\t(panelOpen)=\"panelOpen.emit($event)\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t}\n\t\t</div>\n\t}\n}\n", styles: [":host{display:flex;flex-direction:column;gap:var(--hub-nav-gap, .25rem);list-style:none;margin:0;padding:0}.hub-nav-item-wrapper{position:relative}.hub-nav-dropdown{position:absolute;z-index:var(--hub-nav-dropdown-z-index, 1000);min-width:var(--hub-nav-dropdown-min-width, 12rem);background-color:var(--hub-nav-dropdown-bg, #fff);color:var(--hub-nav-dropdown-color, #212529);border:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-dropdown-border-color, #dee2e6);border-radius:var(--hub-nav-dropdown-border-radius, .375rem);box-shadow:var(--hub-nav-dropdown-shadow, 0 .5rem 1rem rgba(0, 0, 0, .15));padding:var(--hub-nav-dropdown-padding-y, .5rem) 0}.hub-nav-accordion{overflow:hidden;animation:hubNavAccordionExpand var(--hub-nav-accordion-transition, .35s ease) forwards}@keyframes hubNavAccordionExpand{0%{max-height:0;opacity:0}to{max-height:100vh;opacity:1}}:host-context(.hub-nav--horizontal){flex-direction:row;align-items:center}:host-context(.hub-nav--horizontal)>.hub-nav-item-wrapper{flex:0 0 auto}:host-context(.hub-nav--horizontal) .hub-nav-item-wrapper>.hub-nav-dropdown{top:100%;left:0;margin-top:4px}:host-context(.hub-nav--horizontal) .hub-nav-dropdown .hub-nav-item-wrapper>.hub-nav-dropdown{top:0;left:100%;margin-top:0;margin-left:4px}:host-context(.hub-nav--vertical) .hub-nav-item-wrapper>.hub-nav-dropdown{top:0;left:100%;margin-left:4px}:host-context(.hub-nav--vertical) .hub-nav-dropdown .hub-nav-item-wrapper>.hub-nav-dropdown{top:0;left:100%;margin-left:4px;margin-top:0}:host-context(.hub-nav-dropdown){flex-direction:column;align-items:stretch}:host-context(.hub-nav-accordion){flex-direction:column;align-items:stretch}:host(.hub-nav-item-list--force-accordion){width:100%;flex-direction:column;align-items:stretch;gap:var(--hub-nav-mobile-accordion-gap, .25rem)}:host(.hub-nav-item-list--force-accordion)>.hub-nav-item-wrapper{display:block;width:100%;flex:1 1 auto}:host(.hub-nav-item-list--force-accordion) .hub-nav-item-wrapper>.hub-nav-dropdown{position:static;top:auto;left:auto;margin:0;min-width:0;width:100%;box-shadow:none}:host(.hub-nav-item-list--force-accordion) .hub-nav-accordion{margin-block-start:var(--hub-nav-mobile-accordion-nested-spacing, .25rem)}@media(prefers-reduced-motion:reduce){.hub-nav-accordion{animation:none}}\n"], dependencies: [{ kind: "component", type: HubNavItemListComponent, selector: "hub-nav-item-list", inputs: ["items", "depth", "itemTemplate", "forceAccordionMode"], outputs: ["itemClick", "dropdownToggle", "panelOpen"] }, { kind: "component", type: HubNavItemComponent, selector: "hub-nav-item", inputs: ["item", "depth", "isActive", "isExpanded", "itemTemplate", "forceAccordionMode"], outputs: ["clicked", "toggleDropdown"] }, { kind: "component", type: HubNavSeparatorComponent, selector: "hub-nav-separator" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1268
|
+
}
|
|
1269
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavItemListComponent, decorators: [{
|
|
1270
|
+
type: Component,
|
|
1271
|
+
args: [{ selector: 'hub-nav-item-list', standalone: true, imports: [HubNavItemComponent, HubNavSeparatorComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1272
|
+
class: 'hub-nav-item-list',
|
|
1273
|
+
'[class.hub-nav-item-list--force-accordion]': 'forceAccordionMode()',
|
|
1274
|
+
'[attr.role]': 'depth() === 0 ? "menubar" : "menu"'
|
|
1275
|
+
}, template: "@for (item of items(); track item.id) {\n\t@if (item.type === 'separator') {\n\t\t<hub-nav-separator />\n\t} @else {\n\t\t<div\n\t\t\tclass=\"hub-nav-item-wrapper\"\n\t\t\t[class.hub-nav-item-wrapper--has-dropdown]=\"hasChildren(item)\"\n\t\t\t(mouseenter)=\"onItemMouseEnter(item)\"\n\t\t\t(mouseleave)=\"onItemMouseLeave(item)\"\n\t\t>\n\t\t\t<hub-nav-item\n\t\t\t\t[item]=\"item\"\n\t\t\t\t[depth]=\"depth()\"\n\t\t\t\t[isActive]=\"isItemActive(item)\"\n\t\t\t\t[isExpanded]=\"isExpanded(item)\"\n\t\t\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t\t\t[forceAccordionMode]=\"forceAccordionMode()\"\n\t\t\t\t(clicked)=\"onItemClick($event)\"\n\t\t\t\t(toggleDropdown)=\"onDropdownToggle($event)\"\n\t\t\t/>\n\n\t\t\t<!-- Children: accordion (inline), flyout/dropdown (positioned), or panel (external) -->\n\t\t\t@if (hasChildren(item) && isExpanded(item) && getItemExpandMode(item) !== 'panel') {\n\t\t\t\t@if (getItemExpandMode(item) === 'accordion') {\n\t\t\t\t\t<!-- Accordion: children expand inline with indentation -->\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"hub-nav-accordion\"\n\t\t\t\t\t\trole=\"group\"\n\t\t\t\t\t\t[style.padding-left]=\"'var(--hub-nav-accordion-indent, 1rem)'\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<hub-nav-item-list\n\t\t\t\t\t\t\t[items]=\"item.children!\"\n\t\t\t\t\t\t\t[depth]=\"depth() + 1\"\n\t\t\t\t\t\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t\t\t\t\t\t[forceAccordionMode]=\"forceAccordionMode()\"\n\t\t\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t\t\t(dropdownToggle)=\"dropdownToggle.emit($event)\"\n\t\t\t\t\t\t\t(panelOpen)=\"panelOpen.emit($event)\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t} @else {\n\t\t\t\t\t<!-- Flyout / Dropdown: positioned panel -->\n\t\t\t\t\t<div class=\"hub-nav-dropdown\" role=\"menu\">\n\t\t\t\t\t\t<hub-nav-item-list\n\t\t\t\t\t\t\t[items]=\"item.children!\"\n\t\t\t\t\t\t\t[depth]=\"depth() + 1\"\n\t\t\t\t\t\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t\t\t\t\t\t[forceAccordionMode]=\"forceAccordionMode()\"\n\t\t\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t\t\t(dropdownToggle)=\"dropdownToggle.emit($event)\"\n\t\t\t\t\t\t\t(panelOpen)=\"panelOpen.emit($event)\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t}\n\t\t</div>\n\t}\n}\n", styles: [":host{display:flex;flex-direction:column;gap:var(--hub-nav-gap, .25rem);list-style:none;margin:0;padding:0}.hub-nav-item-wrapper{position:relative}.hub-nav-dropdown{position:absolute;z-index:var(--hub-nav-dropdown-z-index, 1000);min-width:var(--hub-nav-dropdown-min-width, 12rem);background-color:var(--hub-nav-dropdown-bg, #fff);color:var(--hub-nav-dropdown-color, #212529);border:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-dropdown-border-color, #dee2e6);border-radius:var(--hub-nav-dropdown-border-radius, .375rem);box-shadow:var(--hub-nav-dropdown-shadow, 0 .5rem 1rem rgba(0, 0, 0, .15));padding:var(--hub-nav-dropdown-padding-y, .5rem) 0}.hub-nav-accordion{overflow:hidden;animation:hubNavAccordionExpand var(--hub-nav-accordion-transition, .35s ease) forwards}@keyframes hubNavAccordionExpand{0%{max-height:0;opacity:0}to{max-height:100vh;opacity:1}}:host-context(.hub-nav--horizontal){flex-direction:row;align-items:center}:host-context(.hub-nav--horizontal)>.hub-nav-item-wrapper{flex:0 0 auto}:host-context(.hub-nav--horizontal) .hub-nav-item-wrapper>.hub-nav-dropdown{top:100%;left:0;margin-top:4px}:host-context(.hub-nav--horizontal) .hub-nav-dropdown .hub-nav-item-wrapper>.hub-nav-dropdown{top:0;left:100%;margin-top:0;margin-left:4px}:host-context(.hub-nav--vertical) .hub-nav-item-wrapper>.hub-nav-dropdown{top:0;left:100%;margin-left:4px}:host-context(.hub-nav--vertical) .hub-nav-dropdown .hub-nav-item-wrapper>.hub-nav-dropdown{top:0;left:100%;margin-left:4px;margin-top:0}:host-context(.hub-nav-dropdown){flex-direction:column;align-items:stretch}:host-context(.hub-nav-accordion){flex-direction:column;align-items:stretch}:host(.hub-nav-item-list--force-accordion){width:100%;flex-direction:column;align-items:stretch;gap:var(--hub-nav-mobile-accordion-gap, .25rem)}:host(.hub-nav-item-list--force-accordion)>.hub-nav-item-wrapper{display:block;width:100%;flex:1 1 auto}:host(.hub-nav-item-list--force-accordion) .hub-nav-item-wrapper>.hub-nav-dropdown{position:static;top:auto;left:auto;margin:0;min-width:0;width:100%;box-shadow:none}:host(.hub-nav-item-list--force-accordion) .hub-nav-accordion{margin-block-start:var(--hub-nav-mobile-accordion-nested-spacing, .25rem)}@media(prefers-reduced-motion:reduce){.hub-nav-accordion{animation:none}}\n"] }]
|
|
1276
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], forceAccordionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceAccordionMode", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], dropdownToggle: [{ type: i0.Output, args: ["dropdownToggle"] }], panelOpen: [{ type: i0.Output, args: ["panelOpen"] }], onKeyDown: [{
|
|
1277
|
+
type: HostListener,
|
|
1278
|
+
args: ['keydown', ['$event']]
|
|
1279
|
+
}] } });
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Hamburger toggle button that shows/hides the mobile navigation panel.
|
|
1283
|
+
* Renders an animated three-line icon that transforms into an X when open.
|
|
1284
|
+
*
|
|
1285
|
+
* @internal Used internally by `HubNavComponent` when the viewport is below the collapse breakpoint.
|
|
1286
|
+
*/
|
|
1287
|
+
class HubNavTogglerComponent {
|
|
1288
|
+
/** Whether the mobile panel is currently open. */
|
|
1289
|
+
isOpen = input(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
1290
|
+
/** Emitted when the toggler is clicked. */
|
|
1291
|
+
toggle = output();
|
|
1292
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavTogglerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1293
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.2", type: HubNavTogglerComponent, isStandalone: true, selector: "hub-nav-toggler", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggle: "toggle" }, host: { classAttribute: "hub-nav-toggler" }, ngImport: i0, template: `
|
|
1294
|
+
<button
|
|
1295
|
+
type="button"
|
|
1296
|
+
class="hub-nav-toggler__button"
|
|
1297
|
+
[class.hub-nav-toggler__button--open]="isOpen()"
|
|
1298
|
+
[attr.aria-expanded]="isOpen()"
|
|
1299
|
+
aria-label="Toggle navigation"
|
|
1300
|
+
(click)="toggle.emit()"
|
|
1301
|
+
>
|
|
1302
|
+
<span class="hub-nav-toggler__icon">
|
|
1303
|
+
<span class="hub-nav-toggler__bar"></span>
|
|
1304
|
+
<span class="hub-nav-toggler__bar"></span>
|
|
1305
|
+
<span class="hub-nav-toggler__bar"></span>
|
|
1306
|
+
</span>
|
|
1307
|
+
</button>
|
|
1308
|
+
`, isInline: true, styles: [":host{display:inline-flex;flex-shrink:0}.hub-nav-toggler__button{display:inline-flex;align-items:center;justify-content:center;padding:var(--hub-nav-toggler-padding-y, .25rem) var(--hub-nav-toggler-padding-x, .5rem);font-size:var(--hub-nav-toggler-font-size, 1.25rem);color:var(--hub-nav-toggler-color, #212529);background:none;border:1px solid var(--hub-nav-toggler-border-color, #dee2e6);border-radius:var(--hub-nav-toggler-border-radius, .25rem);cursor:pointer;transition:background-color var(--hub-nav-item-transition, .15s ease)}.hub-nav-toggler__button:hover{background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04))}.hub-nav-toggler__button:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-toggler__icon{display:flex;flex-direction:column;gap:4px;width:1.5em;height:1.5em;justify-content:center}.hub-nav-toggler__bar{display:block;width:100%;height:2px;background-color:currentColor;border-radius:1px;transition:transform .3s ease,opacity .3s ease;transform-origin:center}.hub-nav-toggler__button--open .hub-nav-toggler__bar:nth-child(1){transform:translateY(6px) rotate(45deg)}.hub-nav-toggler__button--open .hub-nav-toggler__bar:nth-child(2){opacity:0}.hub-nav-toggler__button--open .hub-nav-toggler__bar:nth-child(3){transform:translateY(-6px) rotate(-45deg)}@media(prefers-reduced-motion:reduce){.hub-nav-toggler__bar{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1309
|
+
}
|
|
1310
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavTogglerComponent, decorators: [{
|
|
1311
|
+
type: Component,
|
|
1312
|
+
args: [{ selector: 'hub-nav-toggler', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1313
|
+
class: 'hub-nav-toggler'
|
|
1314
|
+
}, template: `
|
|
1315
|
+
<button
|
|
1316
|
+
type="button"
|
|
1317
|
+
class="hub-nav-toggler__button"
|
|
1318
|
+
[class.hub-nav-toggler__button--open]="isOpen()"
|
|
1319
|
+
[attr.aria-expanded]="isOpen()"
|
|
1320
|
+
aria-label="Toggle navigation"
|
|
1321
|
+
(click)="toggle.emit()"
|
|
1322
|
+
>
|
|
1323
|
+
<span class="hub-nav-toggler__icon">
|
|
1324
|
+
<span class="hub-nav-toggler__bar"></span>
|
|
1325
|
+
<span class="hub-nav-toggler__bar"></span>
|
|
1326
|
+
<span class="hub-nav-toggler__bar"></span>
|
|
1327
|
+
</span>
|
|
1328
|
+
</button>
|
|
1329
|
+
`, styles: [":host{display:inline-flex;flex-shrink:0}.hub-nav-toggler__button{display:inline-flex;align-items:center;justify-content:center;padding:var(--hub-nav-toggler-padding-y, .25rem) var(--hub-nav-toggler-padding-x, .5rem);font-size:var(--hub-nav-toggler-font-size, 1.25rem);color:var(--hub-nav-toggler-color, #212529);background:none;border:1px solid var(--hub-nav-toggler-border-color, #dee2e6);border-radius:var(--hub-nav-toggler-border-radius, .25rem);cursor:pointer;transition:background-color var(--hub-nav-item-transition, .15s ease)}.hub-nav-toggler__button:hover{background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04))}.hub-nav-toggler__button:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-toggler__icon{display:flex;flex-direction:column;gap:4px;width:1.5em;height:1.5em;justify-content:center}.hub-nav-toggler__bar{display:block;width:100%;height:2px;background-color:currentColor;border-radius:1px;transition:transform .3s ease,opacity .3s ease;transform-origin:center}.hub-nav-toggler__button--open .hub-nav-toggler__bar:nth-child(1){transform:translateY(6px) rotate(45deg)}.hub-nav-toggler__button--open .hub-nav-toggler__bar:nth-child(2){opacity:0}.hub-nav-toggler__button--open .hub-nav-toggler__bar:nth-child(3){transform:translateY(-6px) rotate(-45deg)}@media(prefers-reduced-motion:reduce){.hub-nav-toggler__bar{transition:none}}\n"] }]
|
|
1330
|
+
}], propDecorators: { isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], toggle: [{ type: i0.Output, args: ["toggle"] }] } });
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Mobile navigation panel that renders when the viewport is below the collapse breakpoint.
|
|
1334
|
+
* Supports three display modes: offcanvas (slide-in drawer), dropdown (panel below toggler),
|
|
1335
|
+
* and fullscreen (overlay).
|
|
1336
|
+
*
|
|
1337
|
+
* @internal Used internally by `HubNavComponent`.
|
|
1338
|
+
*/
|
|
1339
|
+
class HubNavMobilePanelComponent {
|
|
1340
|
+
/** Navigation items to render in the panel. */
|
|
1341
|
+
items = input.required(...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1342
|
+
/** Whether the panel is currently open. */
|
|
1343
|
+
isOpen = input(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
1344
|
+
/** Collapse display mode. */
|
|
1345
|
+
mode = input('offcanvas', ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
1346
|
+
/** Offcanvas slide-in direction. */
|
|
1347
|
+
offcanvasPosition = input('start', ...(ngDevMode ? [{ debugName: "offcanvasPosition" }] : []));
|
|
1348
|
+
/** Emitted when the close button or backdrop is clicked. */
|
|
1349
|
+
closePanel = output();
|
|
1350
|
+
/** Emitted when an item is clicked. */
|
|
1351
|
+
itemClick = output();
|
|
1352
|
+
/** Emitted when a dropdown toggle is requested. */
|
|
1353
|
+
dropdownToggle = output();
|
|
1354
|
+
/**
|
|
1355
|
+
* Handles backdrop click to close the panel.
|
|
1356
|
+
*/
|
|
1357
|
+
onBackdropClick() {
|
|
1358
|
+
this.closePanel.emit();
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Handles close button click.
|
|
1362
|
+
*/
|
|
1363
|
+
onCloseClick() {
|
|
1364
|
+
this.closePanel.emit();
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Handles item click events from the inner list.
|
|
1368
|
+
*
|
|
1369
|
+
* @param payload - The clicked item and original event.
|
|
1370
|
+
*/
|
|
1371
|
+
onItemClick(payload) {
|
|
1372
|
+
this.itemClick.emit(payload);
|
|
1373
|
+
// Close panel whenever the click performed a route navigation.
|
|
1374
|
+
// Dropdown caret toggles do not reach this handler.
|
|
1375
|
+
if (payload.item.route) {
|
|
1376
|
+
this.closePanel.emit();
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Handles dropdown toggle events from the inner list.
|
|
1381
|
+
*
|
|
1382
|
+
* @param item - The dropdown item.
|
|
1383
|
+
*/
|
|
1384
|
+
onDropdownToggle(item) {
|
|
1385
|
+
this.dropdownToggle.emit(item);
|
|
1386
|
+
}
|
|
1387
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavMobilePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1388
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: HubNavMobilePanelComponent, isStandalone: true, selector: "hub-nav-mobile-panel", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, offcanvasPosition: { classPropertyName: "offcanvasPosition", publicName: "offcanvasPosition", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closePanel: "closePanel", itemClick: "itemClick", dropdownToggle: "dropdownToggle" }, host: { properties: { "class.hub-nav-mobile-panel--open": "isOpen()", "class.hub-nav-mobile-panel--offcanvas": "mode() === \"offcanvas\"", "class.hub-nav-mobile-panel--dropdown": "mode() === \"dropdown\"", "class.hub-nav-mobile-panel--fullscreen": "mode() === \"fullscreen\"", "class.hub-nav-mobile-panel--start": "offcanvasPosition() === \"start\"", "class.hub-nav-mobile-panel--end": "offcanvasPosition() === \"end\"", "class.hub-nav-mobile-panel--top": "offcanvasPosition() === \"top\"", "class.hub-nav-mobile-panel--bottom": "offcanvasPosition() === \"bottom\"" }, classAttribute: "hub-nav-mobile-panel" }, ngImport: i0, template: "@if (isOpen()) {\n\t<!-- Backdrop (offcanvas and fullscreen modes) -->\n\t@if (mode() !== 'dropdown') {\n\t\t<div class=\"hub-nav-mobile-panel__backdrop\" (click)=\"onBackdropClick()\"></div>\n\t}\n\n\t<!-- Panel content -->\n\t<div class=\"hub-nav-mobile-panel__content\">\n\t\t<!-- Close button -->\n\t\t<div class=\"hub-nav-mobile-panel__header\">\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"hub-nav-mobile-panel__close\"\n\t\t\t\taria-label=\"Close navigation\"\n\t\t\t\t(click)=\"onCloseClick()\"\n\t\t\t>\n\t\t\t\t<span aria-hidden=\"true\">×</span>\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- Navigation items in vertical accordion mode -->\n\t\t<div class=\"hub-nav-mobile-panel__body\">\n\t\t\t<hub-nav-item-list\n\t\t\t\t[items]=\"items()\"\n\t\t\t\t[depth]=\"0\"\n\t\t\t\t[forceAccordionMode]=\"true\"\n\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t/>\n\t\t</div>\n\t</div>\n}\n", styles: [".hub-nav-mobile-panel__backdrop{position:var(--hub-nav-mobile-overlay-position, fixed);inset:0;z-index:calc(var(--hub-nav-mobile-z-index, 1030) - 1);background-color:var(--hub-nav-mobile-backdrop-bg, rgba(0, 0, 0, .5))}.hub-nav-mobile-panel__content{position:var(--hub-nav-mobile-overlay-position, fixed);z-index:var(--hub-nav-mobile-z-index, 1030);display:flex;flex-direction:column;background-color:var(--hub-nav-mobile-bg, var(--hub-sys-surface-elevated, #fff));color:var(--hub-nav-mobile-color, var(--hub-sys-text-primary, #212529));transition:transform var(--hub-nav-mobile-transition, .3s ease);overflow-y:auto}.hub-nav-mobile-panel__header{display:flex;justify-content:flex-end;padding:var(--hub-nav-padding-y, .5rem) var(--hub-nav-padding-x, 1rem)}.hub-nav-mobile-panel__close{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;font-size:1.5rem;line-height:1;color:var(--hub-nav-color, #212529);background:none;border:0;border-radius:var(--hub-nav-item-border-radius, .25rem);cursor:pointer}.hub-nav-mobile-panel__close:hover{background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04))}.hub-nav-mobile-panel__close:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-mobile-panel__body{flex:1;padding:0 var(--hub-nav-mobile-body-padding-inline, var(--hub-nav-padding-x, 1rem)) var(--hub-nav-mobile-body-padding-block-end, var(--hub-nav-padding-y, .5rem));--hub-nav-item-padding-x: var(--hub-nav-mobile-item-padding-inline, .75rem)}.hub-nav-mobile-panel__body>hub-nav-item-list{display:flex;flex-direction:column;align-items:stretch;width:100%;padding-inline:var(--hub-nav-mobile-root-padding-inline, .5rem);box-sizing:border-box}:host(.hub-nav-mobile-panel--offcanvas) .hub-nav-mobile-panel__content{width:var(--hub-nav-mobile-width, 18rem);height:100%;box-shadow:var(--hub-nav-mobile-shadow, 0 .5rem 1rem rgba(0, 0, 0, .18));border-inline-end:1px solid var(--hub-nav-mobile-border-color, rgba(0, 0, 0, .08))}:host(.hub-nav-mobile-panel--offcanvas).hub-nav-mobile-panel--start .hub-nav-mobile-panel__content{top:0;left:0}:host(.hub-nav-mobile-panel--offcanvas).hub-nav-mobile-panel--end .hub-nav-mobile-panel__content{top:0;right:0}:host(.hub-nav-mobile-panel--offcanvas).hub-nav-mobile-panel--top .hub-nav-mobile-panel__content{top:0;left:0;width:100%;height:auto;max-height:80vh}:host(.hub-nav-mobile-panel--offcanvas).hub-nav-mobile-panel--bottom .hub-nav-mobile-panel__content{bottom:0;left:0;width:100%;height:auto;max-height:80vh}:host(.hub-nav-mobile-panel--dropdown) .hub-nav-mobile-panel__content{position:relative;width:100%;border-top:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-border-color, #dee2e6)}:host(.hub-nav-mobile-panel--fullscreen) .hub-nav-mobile-panel__content{inset:0;width:100%;height:100%}:host(.hub-nav-mobile-panel--fullscreen) .hub-nav-mobile-panel__body{display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center}:host(.hub-nav-mobile-panel--fullscreen) .hub-nav-mobile-panel__body hub-nav-item-list{width:100%;max-width:20rem}:host:not(.hub-nav-mobile-panel--open){display:none}@media(prefers-reduced-motion:reduce){.hub-nav-mobile-panel__content{transition:none}}\n"], dependencies: [{ kind: "component", type: HubNavItemListComponent, selector: "hub-nav-item-list", inputs: ["items", "depth", "itemTemplate", "forceAccordionMode"], outputs: ["itemClick", "dropdownToggle", "panelOpen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1389
|
+
}
|
|
1390
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavMobilePanelComponent, decorators: [{
|
|
1391
|
+
type: Component,
|
|
1392
|
+
args: [{ selector: 'hub-nav-mobile-panel', standalone: true, imports: [HubNavItemListComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1393
|
+
class: 'hub-nav-mobile-panel',
|
|
1394
|
+
'[class.hub-nav-mobile-panel--open]': 'isOpen()',
|
|
1395
|
+
'[class.hub-nav-mobile-panel--offcanvas]': 'mode() === "offcanvas"',
|
|
1396
|
+
'[class.hub-nav-mobile-panel--dropdown]': 'mode() === "dropdown"',
|
|
1397
|
+
'[class.hub-nav-mobile-panel--fullscreen]': 'mode() === "fullscreen"',
|
|
1398
|
+
'[class.hub-nav-mobile-panel--start]': 'offcanvasPosition() === "start"',
|
|
1399
|
+
'[class.hub-nav-mobile-panel--end]': 'offcanvasPosition() === "end"',
|
|
1400
|
+
'[class.hub-nav-mobile-panel--top]': 'offcanvasPosition() === "top"',
|
|
1401
|
+
'[class.hub-nav-mobile-panel--bottom]': 'offcanvasPosition() === "bottom"'
|
|
1402
|
+
}, template: "@if (isOpen()) {\n\t<!-- Backdrop (offcanvas and fullscreen modes) -->\n\t@if (mode() !== 'dropdown') {\n\t\t<div class=\"hub-nav-mobile-panel__backdrop\" (click)=\"onBackdropClick()\"></div>\n\t}\n\n\t<!-- Panel content -->\n\t<div class=\"hub-nav-mobile-panel__content\">\n\t\t<!-- Close button -->\n\t\t<div class=\"hub-nav-mobile-panel__header\">\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"hub-nav-mobile-panel__close\"\n\t\t\t\taria-label=\"Close navigation\"\n\t\t\t\t(click)=\"onCloseClick()\"\n\t\t\t>\n\t\t\t\t<span aria-hidden=\"true\">×</span>\n\t\t\t</button>\n\t\t</div>\n\n\t\t<!-- Navigation items in vertical accordion mode -->\n\t\t<div class=\"hub-nav-mobile-panel__body\">\n\t\t\t<hub-nav-item-list\n\t\t\t\t[items]=\"items()\"\n\t\t\t\t[depth]=\"0\"\n\t\t\t\t[forceAccordionMode]=\"true\"\n\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t/>\n\t\t</div>\n\t</div>\n}\n", styles: [".hub-nav-mobile-panel__backdrop{position:var(--hub-nav-mobile-overlay-position, fixed);inset:0;z-index:calc(var(--hub-nav-mobile-z-index, 1030) - 1);background-color:var(--hub-nav-mobile-backdrop-bg, rgba(0, 0, 0, .5))}.hub-nav-mobile-panel__content{position:var(--hub-nav-mobile-overlay-position, fixed);z-index:var(--hub-nav-mobile-z-index, 1030);display:flex;flex-direction:column;background-color:var(--hub-nav-mobile-bg, var(--hub-sys-surface-elevated, #fff));color:var(--hub-nav-mobile-color, var(--hub-sys-text-primary, #212529));transition:transform var(--hub-nav-mobile-transition, .3s ease);overflow-y:auto}.hub-nav-mobile-panel__header{display:flex;justify-content:flex-end;padding:var(--hub-nav-padding-y, .5rem) var(--hub-nav-padding-x, 1rem)}.hub-nav-mobile-panel__close{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;font-size:1.5rem;line-height:1;color:var(--hub-nav-color, #212529);background:none;border:0;border-radius:var(--hub-nav-item-border-radius, .25rem);cursor:pointer}.hub-nav-mobile-panel__close:hover{background-color:var(--hub-nav-item-hover-bg, rgba(0, 0, 0, .04))}.hub-nav-mobile-panel__close:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-mobile-panel__body{flex:1;padding:0 var(--hub-nav-mobile-body-padding-inline, var(--hub-nav-padding-x, 1rem)) var(--hub-nav-mobile-body-padding-block-end, var(--hub-nav-padding-y, .5rem));--hub-nav-item-padding-x: var(--hub-nav-mobile-item-padding-inline, .75rem)}.hub-nav-mobile-panel__body>hub-nav-item-list{display:flex;flex-direction:column;align-items:stretch;width:100%;padding-inline:var(--hub-nav-mobile-root-padding-inline, .5rem);box-sizing:border-box}:host(.hub-nav-mobile-panel--offcanvas) .hub-nav-mobile-panel__content{width:var(--hub-nav-mobile-width, 18rem);height:100%;box-shadow:var(--hub-nav-mobile-shadow, 0 .5rem 1rem rgba(0, 0, 0, .18));border-inline-end:1px solid var(--hub-nav-mobile-border-color, rgba(0, 0, 0, .08))}:host(.hub-nav-mobile-panel--offcanvas).hub-nav-mobile-panel--start .hub-nav-mobile-panel__content{top:0;left:0}:host(.hub-nav-mobile-panel--offcanvas).hub-nav-mobile-panel--end .hub-nav-mobile-panel__content{top:0;right:0}:host(.hub-nav-mobile-panel--offcanvas).hub-nav-mobile-panel--top .hub-nav-mobile-panel__content{top:0;left:0;width:100%;height:auto;max-height:80vh}:host(.hub-nav-mobile-panel--offcanvas).hub-nav-mobile-panel--bottom .hub-nav-mobile-panel__content{bottom:0;left:0;width:100%;height:auto;max-height:80vh}:host(.hub-nav-mobile-panel--dropdown) .hub-nav-mobile-panel__content{position:relative;width:100%;border-top:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-border-color, #dee2e6)}:host(.hub-nav-mobile-panel--fullscreen) .hub-nav-mobile-panel__content{inset:0;width:100%;height:100%}:host(.hub-nav-mobile-panel--fullscreen) .hub-nav-mobile-panel__body{display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center}:host(.hub-nav-mobile-panel--fullscreen) .hub-nav-mobile-panel__body hub-nav-item-list{width:100%;max-width:20rem}:host:not(.hub-nav-mobile-panel--open){display:none}@media(prefers-reduced-motion:reduce){.hub-nav-mobile-panel__content{transition:none}}\n"] }]
|
|
1403
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], offcanvasPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "offcanvasPosition", required: false }] }], closePanel: [{ type: i0.Output, args: ["closePanel"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], dropdownToggle: [{ type: i0.Output, args: ["dropdownToggle"] }] } });
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Renders a single side panel in the stacked panel navigation.
|
|
1407
|
+
* Contains a header with the parent item's label (plus back/close buttons)
|
|
1408
|
+
* and a body with a `hub-nav-item-list` for the panel's items.
|
|
1409
|
+
*
|
|
1410
|
+
* Includes keyboard navigation:
|
|
1411
|
+
* - **Escape**: closes the panel and returns focus to the parent trigger.
|
|
1412
|
+
* - **ArrowLeft**: navigates back (if drill-down) or closes the panel.
|
|
1413
|
+
*
|
|
1414
|
+
* @internal Used internally by `HubNavPanelContainerComponent`.
|
|
1415
|
+
*/
|
|
1416
|
+
class HubNavPanelComponent {
|
|
1417
|
+
/** The panel state to render. */
|
|
1418
|
+
panel = input.required(...(ngDevMode ? [{ debugName: "panel" }] : []));
|
|
1419
|
+
/** Width of this panel (CSS value). */
|
|
1420
|
+
panelWidth = input('16rem', ...(ngDevMode ? [{ debugName: "panelWidth" }] : []));
|
|
1421
|
+
/** Optional custom template for rendering item content. */
|
|
1422
|
+
itemTemplate = input(null, ...(ngDevMode ? [{ debugName: "itemTemplate" }] : []));
|
|
1423
|
+
/**
|
|
1424
|
+
* Controls whether the panel header is rendered.
|
|
1425
|
+
* Used by horizontal layouts to avoid duplicating the selected root item
|
|
1426
|
+
* title when that item is already visible in the top row.
|
|
1427
|
+
*/
|
|
1428
|
+
showHeader = input(true, ...(ngDevMode ? [{ debugName: "showHeader" }] : []));
|
|
1429
|
+
/** Emitted when the panel close button is clicked. Carries the panel ID. */
|
|
1430
|
+
closePanel = output();
|
|
1431
|
+
/** Emitted when the back button is clicked. Carries the panel ID. */
|
|
1432
|
+
backClick = output();
|
|
1433
|
+
/** Emitted when an item in the panel is clicked. */
|
|
1434
|
+
itemClick = output();
|
|
1435
|
+
/** Emitted when a dropdown toggle is requested within the panel. */
|
|
1436
|
+
dropdownToggle = output();
|
|
1437
|
+
/** Emitted when an item with panel expand mode is clicked (needs a new panel). */
|
|
1438
|
+
panelOpen = output();
|
|
1439
|
+
/** Host element reference. */
|
|
1440
|
+
el = inject((ElementRef));
|
|
1441
|
+
/** The label displayed in the panel header. */
|
|
1442
|
+
currentLabel = computed(() => this.panel().parentItem.label, ...(ngDevMode ? [{ debugName: "currentLabel" }] : []));
|
|
1443
|
+
/** Accessible label for the panel navigation landmark. */
|
|
1444
|
+
ariaLabel = computed(() => `${this.panel().parentItem.label} navigation`, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
1445
|
+
/**
|
|
1446
|
+
* Handles keyboard events for panel navigation.
|
|
1447
|
+
*
|
|
1448
|
+
* @param event - The keyboard event.
|
|
1449
|
+
*/
|
|
1450
|
+
onKeyDown(event) {
|
|
1451
|
+
switch (event.key) {
|
|
1452
|
+
case 'Escape':
|
|
1453
|
+
event.preventDefault();
|
|
1454
|
+
event.stopPropagation();
|
|
1455
|
+
this.closePanel.emit(this.panel().id);
|
|
1456
|
+
break;
|
|
1457
|
+
case 'ArrowLeft':
|
|
1458
|
+
// Only handle ArrowLeft at the panel level (not inside item-list submenus)
|
|
1459
|
+
if (this.isDirectPanelFocus(event.target)) {
|
|
1460
|
+
event.preventDefault();
|
|
1461
|
+
event.stopPropagation();
|
|
1462
|
+
if (this.panel().isDrillDown) {
|
|
1463
|
+
this.backClick.emit(this.panel().id);
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
this.closePanel.emit(this.panel().id);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
break;
|
|
1470
|
+
default:
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Handles close button click.
|
|
1476
|
+
*/
|
|
1477
|
+
onCloseClick() {
|
|
1478
|
+
this.closePanel.emit(this.panel().id);
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Handles back button click for drill-down navigation.
|
|
1482
|
+
*/
|
|
1483
|
+
onBackClick() {
|
|
1484
|
+
this.backClick.emit(this.panel().id);
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Forwards item click events from the inner item list.
|
|
1488
|
+
*
|
|
1489
|
+
* @param payload - The clicked item and DOM event.
|
|
1490
|
+
*/
|
|
1491
|
+
onItemClick(payload) {
|
|
1492
|
+
this.itemClick.emit(payload);
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Forwards dropdown toggle events from the inner item list.
|
|
1496
|
+
*
|
|
1497
|
+
* @param item - The dropdown item that was toggled.
|
|
1498
|
+
*/
|
|
1499
|
+
onDropdownToggle(item) {
|
|
1500
|
+
this.dropdownToggle.emit(item);
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Forwards panel open events from the inner item list.
|
|
1504
|
+
*
|
|
1505
|
+
* @param item - The item requesting a new panel.
|
|
1506
|
+
*/
|
|
1507
|
+
onPanelOpen(item) {
|
|
1508
|
+
this.panelOpen.emit(item);
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Focuses the first focusable item inside this panel.
|
|
1512
|
+
*/
|
|
1513
|
+
focusFirstItem() {
|
|
1514
|
+
requestAnimationFrame(() => {
|
|
1515
|
+
const firstItem = this.el.nativeElement.querySelector('.hub-nav-item__link, .hub-nav-item__caret-button, .hub-nav-item__dropdown-toggle');
|
|
1516
|
+
firstItem?.focus();
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Checks whether the event target is a direct child of this panel's item-list
|
|
1521
|
+
* (not inside a nested flyout/accordion submenu rendered by descendants).
|
|
1522
|
+
*
|
|
1523
|
+
* @param target - The DOM element that received the event.
|
|
1524
|
+
* @returns `true` if the target belongs directly to this panel's navigation items.
|
|
1525
|
+
*/
|
|
1526
|
+
isDirectPanelFocus(target) {
|
|
1527
|
+
const owningList = target.closest('hub-nav-item-list');
|
|
1528
|
+
const directList = this.el.nativeElement.querySelector(':scope > .hub-nav-panel__body > hub-nav-item-list');
|
|
1529
|
+
return !!owningList && owningList === directList;
|
|
1530
|
+
}
|
|
1531
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1532
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: HubNavPanelComponent, isStandalone: true, selector: "hub-nav-panel", inputs: { panel: { classPropertyName: "panel", publicName: "panel", isSignal: true, isRequired: true, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closePanel: "closePanel", backClick: "backClick", itemClick: "itemClick", dropdownToggle: "dropdownToggle", panelOpen: "panelOpen" }, host: { attributes: { "role": "navigation", "tabindex": "-1" }, listeners: { "keydown": "onKeyDown($event)" }, properties: { "class.hub-nav-panel--drill-down": "panel().isDrillDown", "style.width": "panelWidth()", "attr.aria-label": "ariaLabel()" }, classAttribute: "hub-nav-panel" }, ngImport: i0, template: "@if (showHeader()) {\n\t<div class=\"hub-nav-panel__header\">\n\t\t@if (panel().isDrillDown) {\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"hub-nav-panel__back\"\n\t\t\t\taria-label=\"Go back\"\n\t\t\t\t(click)=\"onBackClick()\"\n\t\t\t>\n\t\t\t\t<span aria-hidden=\"true\">←</span>\n\t\t\t</button>\n\t\t}\n\t\t<span class=\"hub-nav-panel__title\">{{ currentLabel() }}</span>\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tclass=\"hub-nav-panel__close\"\n\t\t\taria-label=\"Close panel\"\n\t\t\t(click)=\"onCloseClick()\"\n\t\t>\n\t\t\t<span aria-hidden=\"true\">×</span>\n\t\t</button>\n\t</div>\n}\n\n<div class=\"hub-nav-panel__body\">\n\t<hub-nav-item-list\n\t\t[items]=\"panel().items\"\n\t\t[depth]=\"0\"\n\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t(itemClick)=\"onItemClick($event)\"\n\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t/>\n</div>\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-width:0;flex-shrink:0;position:relative;z-index:var(--hub-nav-panel-z-index, 100);background:var(--hub-nav-panel-bg);color:var(--hub-nav-panel-color);border-inline-start:var(--hub-nav-panel-border-width) solid var(--hub-nav-panel-border-color);box-shadow:var(--hub-nav-panel-shadow);overflow:hidden}:host-context(.hub-nav--horizontal){width:100%!important;height:auto;min-height:0;flex-direction:row;border-inline-start:none;border-inline-end:none;border-top:var(--hub-nav-panel-border-width) solid var(--hub-nav-panel-border-color);box-shadow:none;animation:none}.hub-nav-panel__header{display:flex;align-items:center;gap:var(--hub-ref-space-2, .5rem);height:var(--hub-nav-panel-header-height);min-height:var(--hub-nav-panel-header-height);padding:0 var(--hub-nav-panel-header-padding-x);background:var(--hub-nav-panel-header-bg);border-bottom:var(--hub-nav-panel-border-width, 1px) solid var(--hub-nav-panel-header-border-color);font-size:var(--hub-nav-panel-header-font-size);font-weight:var(--hub-nav-panel-header-font-weight)}:host-context(.hub-nav--horizontal) .hub-nav-panel__header{flex:0 0 var(--hub-nav-panel-width, 16rem);height:auto;min-height:0;max-height:none;border-bottom:none;border-inline-end:var(--hub-nav-panel-border-width, 1px) solid var(--hub-nav-panel-header-border-color)}:host-context(.hub-nav--horizontal):host-context([dir=rtl]) .hub-nav-panel__header{order:2;border-inline-end:none;border-inline-start:var(--hub-nav-panel-border-width, 1px) solid var(--hub-nav-panel-header-border-color)}.hub-nav-panel__title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hub-nav-panel__back,.hub-nav-panel__close{display:inline-flex;align-items:center;justify-content:center;width:var(--hub-nav-panel-back-size);height:var(--hub-nav-panel-back-size);padding:0;border:none;border-radius:var(--hub-nav-item-border-radius, .25rem);background:none;color:var(--hub-nav-panel-back-color);font-size:1.25rem;cursor:pointer;transition:background-color var(--hub-nav-item-transition, .15s ease);flex-shrink:0}.hub-nav-panel__back:hover,.hub-nav-panel__close:hover{background:var(--hub-nav-panel-back-hover-bg)}.hub-nav-panel__back:focus-visible,.hub-nav-panel__close:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-panel__close{margin-inline-start:auto}.hub-nav-panel__body{flex:1;overflow-y:auto;overflow-x:hidden;padding-block:var(--hub-nav-vertical-panel-padding-block, 0);padding-inline:var(--hub-nav-vertical-panel-padding-inline, .5rem);-ms-overflow-style:none;scrollbar-width:var(--hub-nav-scrollbar-width, none);scrollbar-color:var(--hub-nav-scrollbar-color, transparent transparent)}.hub-nav-panel__body::-webkit-scrollbar{width:var(--hub-nav-scrollbar-webkit-size, 0);height:var(--hub-nav-scrollbar-webkit-size, 0)}.hub-nav-panel__body::-webkit-scrollbar-thumb{background-color:var(--hub-nav-scrollbar-thumb-color, transparent);border-radius:var(--hub-nav-scrollbar-thumb-radius, 999px)}.hub-nav-panel__body::-webkit-scrollbar-track{background-color:var(--hub-nav-scrollbar-track-color, transparent)}:host-context(.hub-nav--horizontal) .hub-nav-panel__body{display:flex;align-items:center;justify-content:flex-start;min-width:0;overflow-x:auto;overflow-y:hidden;padding:var(--hub-nav-horizontal-panel-padding-y, .5rem) var(--hub-nav-horizontal-panel-padding-x, 0)}:host-context(.hub-nav--horizontal) .hub-nav-panel__body>hub-nav-item-list{flex:0 0 auto;width:max-content;min-width:max-content;margin-inline:auto}:host{animation:hubNavPanelEnter .3s ease forwards}@keyframes hubNavPanelEnter{0%{opacity:0;transform:translate(-1rem)}to{opacity:1;transform:translate(0)}}:host-context(.hub-nav-panel-container--right){border-inline-start:none;border-inline-end:var(--hub-nav-panel-border-width) solid var(--hub-nav-panel-border-color);animation-name:hubNavPanelEnterRight}@keyframes hubNavPanelEnterRight{0%{opacity:0;transform:translate(1rem)}to{opacity:1;transform:translate(0)}}:host(.hub-nav-panel--drill-down) .hub-nav-panel__body{animation:hubNavPanelDrillDown .2s ease forwards}@keyframes hubNavPanelDrillDown{0%{opacity:0;transform:translate(.5rem)}to{opacity:1;transform:translate(0)}}@media(prefers-reduced-motion:reduce){:host{animation:none}:host(.hub-nav-panel--drill-down) .hub-nav-panel__body{animation:none}}\n"], dependencies: [{ kind: "component", type: HubNavItemListComponent, selector: "hub-nav-item-list", inputs: ["items", "depth", "itemTemplate", "forceAccordionMode"], outputs: ["itemClick", "dropdownToggle", "panelOpen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1533
|
+
}
|
|
1534
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavPanelComponent, decorators: [{
|
|
1535
|
+
type: Component,
|
|
1536
|
+
args: [{ selector: 'hub-nav-panel', standalone: true, imports: [HubNavItemListComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1537
|
+
class: 'hub-nav-panel',
|
|
1538
|
+
'[class.hub-nav-panel--drill-down]': 'panel().isDrillDown',
|
|
1539
|
+
'[style.width]': 'panelWidth()',
|
|
1540
|
+
role: 'navigation',
|
|
1541
|
+
'[attr.aria-label]': 'ariaLabel()',
|
|
1542
|
+
tabindex: '-1'
|
|
1543
|
+
}, template: "@if (showHeader()) {\n\t<div class=\"hub-nav-panel__header\">\n\t\t@if (panel().isDrillDown) {\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"hub-nav-panel__back\"\n\t\t\t\taria-label=\"Go back\"\n\t\t\t\t(click)=\"onBackClick()\"\n\t\t\t>\n\t\t\t\t<span aria-hidden=\"true\">←</span>\n\t\t\t</button>\n\t\t}\n\t\t<span class=\"hub-nav-panel__title\">{{ currentLabel() }}</span>\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tclass=\"hub-nav-panel__close\"\n\t\t\taria-label=\"Close panel\"\n\t\t\t(click)=\"onCloseClick()\"\n\t\t>\n\t\t\t<span aria-hidden=\"true\">×</span>\n\t\t</button>\n\t</div>\n}\n\n<div class=\"hub-nav-panel__body\">\n\t<hub-nav-item-list\n\t\t[items]=\"panel().items\"\n\t\t[depth]=\"0\"\n\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t(itemClick)=\"onItemClick($event)\"\n\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t/>\n</div>\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-width:0;flex-shrink:0;position:relative;z-index:var(--hub-nav-panel-z-index, 100);background:var(--hub-nav-panel-bg);color:var(--hub-nav-panel-color);border-inline-start:var(--hub-nav-panel-border-width) solid var(--hub-nav-panel-border-color);box-shadow:var(--hub-nav-panel-shadow);overflow:hidden}:host-context(.hub-nav--horizontal){width:100%!important;height:auto;min-height:0;flex-direction:row;border-inline-start:none;border-inline-end:none;border-top:var(--hub-nav-panel-border-width) solid var(--hub-nav-panel-border-color);box-shadow:none;animation:none}.hub-nav-panel__header{display:flex;align-items:center;gap:var(--hub-ref-space-2, .5rem);height:var(--hub-nav-panel-header-height);min-height:var(--hub-nav-panel-header-height);padding:0 var(--hub-nav-panel-header-padding-x);background:var(--hub-nav-panel-header-bg);border-bottom:var(--hub-nav-panel-border-width, 1px) solid var(--hub-nav-panel-header-border-color);font-size:var(--hub-nav-panel-header-font-size);font-weight:var(--hub-nav-panel-header-font-weight)}:host-context(.hub-nav--horizontal) .hub-nav-panel__header{flex:0 0 var(--hub-nav-panel-width, 16rem);height:auto;min-height:0;max-height:none;border-bottom:none;border-inline-end:var(--hub-nav-panel-border-width, 1px) solid var(--hub-nav-panel-header-border-color)}:host-context(.hub-nav--horizontal):host-context([dir=rtl]) .hub-nav-panel__header{order:2;border-inline-end:none;border-inline-start:var(--hub-nav-panel-border-width, 1px) solid var(--hub-nav-panel-header-border-color)}.hub-nav-panel__title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hub-nav-panel__back,.hub-nav-panel__close{display:inline-flex;align-items:center;justify-content:center;width:var(--hub-nav-panel-back-size);height:var(--hub-nav-panel-back-size);padding:0;border:none;border-radius:var(--hub-nav-item-border-radius, .25rem);background:none;color:var(--hub-nav-panel-back-color);font-size:1.25rem;cursor:pointer;transition:background-color var(--hub-nav-item-transition, .15s ease);flex-shrink:0}.hub-nav-panel__back:hover,.hub-nav-panel__close:hover{background:var(--hub-nav-panel-back-hover-bg)}.hub-nav-panel__back:focus-visible,.hub-nav-panel__close:focus-visible{outline:2px solid var(--hub-sys-focus-ring-color, rgba(13, 110, 253, .25));outline-offset:-2px}.hub-nav-panel__close{margin-inline-start:auto}.hub-nav-panel__body{flex:1;overflow-y:auto;overflow-x:hidden;padding-block:var(--hub-nav-vertical-panel-padding-block, 0);padding-inline:var(--hub-nav-vertical-panel-padding-inline, .5rem);-ms-overflow-style:none;scrollbar-width:var(--hub-nav-scrollbar-width, none);scrollbar-color:var(--hub-nav-scrollbar-color, transparent transparent)}.hub-nav-panel__body::-webkit-scrollbar{width:var(--hub-nav-scrollbar-webkit-size, 0);height:var(--hub-nav-scrollbar-webkit-size, 0)}.hub-nav-panel__body::-webkit-scrollbar-thumb{background-color:var(--hub-nav-scrollbar-thumb-color, transparent);border-radius:var(--hub-nav-scrollbar-thumb-radius, 999px)}.hub-nav-panel__body::-webkit-scrollbar-track{background-color:var(--hub-nav-scrollbar-track-color, transparent)}:host-context(.hub-nav--horizontal) .hub-nav-panel__body{display:flex;align-items:center;justify-content:flex-start;min-width:0;overflow-x:auto;overflow-y:hidden;padding:var(--hub-nav-horizontal-panel-padding-y, .5rem) var(--hub-nav-horizontal-panel-padding-x, 0)}:host-context(.hub-nav--horizontal) .hub-nav-panel__body>hub-nav-item-list{flex:0 0 auto;width:max-content;min-width:max-content;margin-inline:auto}:host{animation:hubNavPanelEnter .3s ease forwards}@keyframes hubNavPanelEnter{0%{opacity:0;transform:translate(-1rem)}to{opacity:1;transform:translate(0)}}:host-context(.hub-nav-panel-container--right){border-inline-start:none;border-inline-end:var(--hub-nav-panel-border-width) solid var(--hub-nav-panel-border-color);animation-name:hubNavPanelEnterRight}@keyframes hubNavPanelEnterRight{0%{opacity:0;transform:translate(1rem)}to{opacity:1;transform:translate(0)}}:host(.hub-nav-panel--drill-down) .hub-nav-panel__body{animation:hubNavPanelDrillDown .2s ease forwards}@keyframes hubNavPanelDrillDown{0%{opacity:0;transform:translate(.5rem)}to{opacity:1;transform:translate(0)}}@media(prefers-reduced-motion:reduce){:host{animation:none}:host(.hub-nav-panel--drill-down) .hub-nav-panel__body{animation:none}}\n"] }]
|
|
1544
|
+
}], propDecorators: { panel: [{ type: i0.Input, args: [{ isSignal: true, alias: "panel", required: true }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], closePanel: [{ type: i0.Output, args: ["closePanel"] }], backClick: [{ type: i0.Output, args: ["backClick"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], dropdownToggle: [{ type: i0.Output, args: ["dropdownToggle"] }], panelOpen: [{ type: i0.Output, args: ["panelOpen"] }], onKeyDown: [{
|
|
1545
|
+
type: HostListener,
|
|
1546
|
+
args: ['keydown', ['$event']]
|
|
1547
|
+
}] } });
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Container that renders the stack of side panels in a horizontal flex layout.
|
|
1551
|
+
* Panels are stacked left-to-right or right-to-left depending on `sidebarSide`.
|
|
1552
|
+
*
|
|
1553
|
+
* @internal Used internally by `HubNavComponent`.
|
|
1554
|
+
*/
|
|
1555
|
+
class HubNavPanelContainerComponent {
|
|
1556
|
+
/** Array of panel states to render. */
|
|
1557
|
+
panels = input.required(...(ngDevMode ? [{ debugName: "panels" }] : []));
|
|
1558
|
+
/** Physical side where the sidebar is placed. */
|
|
1559
|
+
sidebarSide = input('left', ...(ngDevMode ? [{ debugName: "sidebarSide" }] : []));
|
|
1560
|
+
/** Width for each panel. */
|
|
1561
|
+
panelWidth = input('16rem', ...(ngDevMode ? [{ debugName: "panelWidth" }] : []));
|
|
1562
|
+
/** Optional custom template for rendering item content. */
|
|
1563
|
+
itemTemplate = input(null, ...(ngDevMode ? [{ debugName: "itemTemplate" }] : []));
|
|
1564
|
+
/**
|
|
1565
|
+
* When true, the first panel hides its header while it is not in drill-down
|
|
1566
|
+
* mode. This is primarily used by horizontal nav where the selected root
|
|
1567
|
+
* item is already visible in the top row.
|
|
1568
|
+
*/
|
|
1569
|
+
hideFirstPanelHeader = input(false, ...(ngDevMode ? [{ debugName: "hideFirstPanelHeader" }] : []));
|
|
1570
|
+
/** Emitted when a panel close button is clicked. Carries the panel ID. */
|
|
1571
|
+
panelClose = output();
|
|
1572
|
+
/** Emitted when a panel back button is clicked. Carries the panel ID. */
|
|
1573
|
+
panelBack = output();
|
|
1574
|
+
/** Emitted when an item inside a panel is clicked. */
|
|
1575
|
+
itemClick = output();
|
|
1576
|
+
/** Emitted when a dropdown toggle is requested inside a panel. */
|
|
1577
|
+
dropdownToggle = output();
|
|
1578
|
+
/** Emitted when an item inside a panel requests a new panel. */
|
|
1579
|
+
panelOpen = output();
|
|
1580
|
+
/**
|
|
1581
|
+
* Resolves whether a panel header should be rendered for a given index.
|
|
1582
|
+
*
|
|
1583
|
+
* When `hideFirstPanelHeader` is true (used in layouts where parent context
|
|
1584
|
+
* is always visible as a sibling column), headers are suppressed on all panels
|
|
1585
|
+
* that are not in drill-down mode. A header is only shown when the panel is in
|
|
1586
|
+
* drill-down mode — meaning its parent level has been replaced and is no longer
|
|
1587
|
+
* visible.
|
|
1588
|
+
*
|
|
1589
|
+
* @param index - Zero-based panel index in the rendered stack.
|
|
1590
|
+
* @param panel - Panel state at the given index.
|
|
1591
|
+
* @returns `true` when the header should be visible.
|
|
1592
|
+
*/
|
|
1593
|
+
shouldShowHeader(index, panel) {
|
|
1594
|
+
if (!this.hideFirstPanelHeader()) {
|
|
1595
|
+
return true;
|
|
1596
|
+
}
|
|
1597
|
+
// Only show header when the parent context is no longer visible as a
|
|
1598
|
+
// sibling column (drill-down replaced it). When all parent levels are
|
|
1599
|
+
// visible, no header is needed — the panel is closed by re-clicking
|
|
1600
|
+
// the parent item in the sidebar.
|
|
1601
|
+
return panel.isDrillDown;
|
|
1602
|
+
}
|
|
1603
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavPanelContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1604
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: HubNavPanelContainerComponent, isStandalone: true, selector: "hub-nav-panel-container", inputs: { panels: { classPropertyName: "panels", publicName: "panels", isSignal: true, isRequired: true, transformFunction: null }, sidebarSide: { classPropertyName: "sidebarSide", publicName: "sidebarSide", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, hideFirstPanelHeader: { classPropertyName: "hideFirstPanelHeader", publicName: "hideFirstPanelHeader", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { panelClose: "panelClose", panelBack: "panelBack", itemClick: "itemClick", dropdownToggle: "dropdownToggle", panelOpen: "panelOpen" }, host: { properties: { "class.hub-nav-panel-container--left": "sidebarSide() === \"left\"", "class.hub-nav-panel-container--right": "sidebarSide() === \"right\"" }, classAttribute: "hub-nav-panel-container" }, ngImport: i0, template: "@for (panel of panels(); track panel.id; let index = $index) {\n\t<hub-nav-panel\n\t\t[panel]=\"panel\"\n\t\t[panelWidth]=\"panelWidth()\"\n\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t[showHeader]=\"shouldShowHeader(index, panel)\"\n\t\t(closePanel)=\"panelClose.emit($event)\"\n\t\t(backClick)=\"panelBack.emit($event)\"\n\t\t(itemClick)=\"itemClick.emit($event)\"\n\t\t(dropdownToggle)=\"dropdownToggle.emit($event)\"\n\t\t(panelOpen)=\"panelOpen.emit($event)\"\n\t/>\n}\n", styles: [":host{display:flex;height:100%;align-items:stretch}:host>hub-nav-panel{height:100%}:host(.hub-nav-panel-container--left){flex-direction:row}:host(.hub-nav-panel-container--right){flex-direction:row-reverse}:host-context(.hub-nav--horizontal){display:block;width:100%;height:auto;min-width:0}:host-context(.hub-nav--horizontal)>hub-nav-panel{height:auto;min-width:0}\n"], dependencies: [{ kind: "component", type: HubNavPanelComponent, selector: "hub-nav-panel", inputs: ["panel", "panelWidth", "itemTemplate", "showHeader"], outputs: ["closePanel", "backClick", "itemClick", "dropdownToggle", "panelOpen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1605
|
+
}
|
|
1606
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavPanelContainerComponent, decorators: [{
|
|
1607
|
+
type: Component,
|
|
1608
|
+
args: [{ selector: 'hub-nav-panel-container', standalone: true, imports: [HubNavPanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1609
|
+
class: 'hub-nav-panel-container',
|
|
1610
|
+
'[class.hub-nav-panel-container--left]': 'sidebarSide() === "left"',
|
|
1611
|
+
'[class.hub-nav-panel-container--right]': 'sidebarSide() === "right"'
|
|
1612
|
+
}, template: "@for (panel of panels(); track panel.id; let index = $index) {\n\t<hub-nav-panel\n\t\t[panel]=\"panel\"\n\t\t[panelWidth]=\"panelWidth()\"\n\t\t[itemTemplate]=\"itemTemplate()\"\n\t\t[showHeader]=\"shouldShowHeader(index, panel)\"\n\t\t(closePanel)=\"panelClose.emit($event)\"\n\t\t(backClick)=\"panelBack.emit($event)\"\n\t\t(itemClick)=\"itemClick.emit($event)\"\n\t\t(dropdownToggle)=\"dropdownToggle.emit($event)\"\n\t\t(panelOpen)=\"panelOpen.emit($event)\"\n\t/>\n}\n", styles: [":host{display:flex;height:100%;align-items:stretch}:host>hub-nav-panel{height:100%}:host(.hub-nav-panel-container--left){flex-direction:row}:host(.hub-nav-panel-container--right){flex-direction:row-reverse}:host-context(.hub-nav--horizontal){display:block;width:100%;height:auto;min-width:0}:host-context(.hub-nav--horizontal)>hub-nav-panel{height:auto;min-width:0}\n"] }]
|
|
1613
|
+
}], propDecorators: { panels: [{ type: i0.Input, args: [{ isSignal: true, alias: "panels", required: true }] }], sidebarSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "sidebarSide", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], hideFirstPanelHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideFirstPanelHeader", required: false }] }], panelClose: [{ type: i0.Output, args: ["panelClose"] }], panelBack: [{ type: i0.Output, args: ["panelBack"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], dropdownToggle: [{ type: i0.Output, args: ["dropdownToggle"] }], panelOpen: [{ type: i0.Output, args: ["panelOpen"] }] } });
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* Root navigation component. Renders a horizontal or vertical navigation bar
|
|
1617
|
+
* with support for nested dropdowns, responsive collapsing, brand slot, panel
|
|
1618
|
+
* drill-down navigation, and custom item templates.
|
|
1619
|
+
*
|
|
1620
|
+
* @example
|
|
1621
|
+
* ```html
|
|
1622
|
+
* <hub-nav [items]="menuItems" [config]="{ orientation: 'horizontal' }" />
|
|
1623
|
+
* ```
|
|
1624
|
+
*/
|
|
1625
|
+
class HubNavComponent {
|
|
1626
|
+
/**
|
|
1627
|
+
* Last route path (without query/fragment) used to synchronize panel state.
|
|
1628
|
+
* Prevents unnecessary panel rebuilds on fragment-only navigations, e.g.
|
|
1629
|
+
* scroll-spy updates in documentation pages.
|
|
1630
|
+
*/
|
|
1631
|
+
_lastSyncedPath = '';
|
|
1632
|
+
/** Navigation items to render. */
|
|
1633
|
+
items = input.required(...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1634
|
+
/** Partial configuration overrides (merged with global defaults). */
|
|
1635
|
+
config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
1636
|
+
/** Additional CSS class for the nav container. */
|
|
1637
|
+
navClass = input('', ...(ngDevMode ? [{ debugName: "navClass" }] : []));
|
|
1638
|
+
/** Optional custom template for rendering nav items (via input binding). */
|
|
1639
|
+
itemTemplate = input(null, ...(ngDevMode ? [{ debugName: "itemTemplate" }] : []));
|
|
1640
|
+
/**
|
|
1641
|
+
* When `true`, the component automatically opens the panels matching the
|
|
1642
|
+
* active router URL on initialization and on every subsequent navigation.
|
|
1643
|
+
* Useful for sidebar navigations where the URL should drive the open state.
|
|
1644
|
+
*/
|
|
1645
|
+
autoOpenFromRoute = input(false, ...(ngDevMode ? [{ debugName: "autoOpenFromRoute" }] : []));
|
|
1646
|
+
/** Start slot template projected via `hubNavStart` directive. */
|
|
1647
|
+
startDirective = contentChild(HubNavStartDirective, ...(ngDevMode ? [{ debugName: "startDirective" }] : []));
|
|
1648
|
+
/** End slot template projected via `hubNavEnd` directive. */
|
|
1649
|
+
endDirective = contentChild(HubNavEndDirective, ...(ngDevMode ? [{ debugName: "endDirective" }] : []));
|
|
1650
|
+
/** Item template projected via `hubNavItemTemplate` directive. */
|
|
1651
|
+
itemTemplateDirective = contentChild(HubNavItemTemplateDirective, ...(ngDevMode ? [{ debugName: "itemTemplateDirective" }] : []));
|
|
1652
|
+
/** Resolved start slot template (from directive content projection). */
|
|
1653
|
+
startTemplate = computed(() => this.startDirective()?.template ?? null, ...(ngDevMode ? [{ debugName: "startTemplate" }] : []));
|
|
1654
|
+
/** Resolved end slot template (from directive content projection). */
|
|
1655
|
+
endTemplate = computed(() => this.endDirective()?.template ?? null, ...(ngDevMode ? [{ debugName: "endTemplate" }] : []));
|
|
1656
|
+
/** Resolved item template: directive takes priority over input. */
|
|
1657
|
+
resolvedItemTemplate = computed(() => this.itemTemplateDirective()?.template ?? this.itemTemplate(), ...(ngDevMode ? [{ debugName: "resolvedItemTemplate" }] : []));
|
|
1658
|
+
/** Start slot context. */
|
|
1659
|
+
startContext = computed(() => ({
|
|
1660
|
+
collapsed: this.state.collapsed()
|
|
1661
|
+
}), ...(ngDevMode ? [{ debugName: "startContext" }] : []));
|
|
1662
|
+
/** End slot context. */
|
|
1663
|
+
endContext = computed(() => ({
|
|
1664
|
+
collapsed: this.state.collapsed()
|
|
1665
|
+
}), ...(ngDevMode ? [{ debugName: "endContext" }] : []));
|
|
1666
|
+
/** Emitted when a link item is clicked. */
|
|
1667
|
+
itemClick = output();
|
|
1668
|
+
/** Emitted when a dropdown opens. */
|
|
1669
|
+
dropdownOpen = output();
|
|
1670
|
+
/** Emitted when a dropdown closes. */
|
|
1671
|
+
dropdownClose = output();
|
|
1672
|
+
/** Emitted when the mobile panel toggles. */
|
|
1673
|
+
mobileToggle = output();
|
|
1674
|
+
/** Emitted when a panel is opened, drilled-down, or navigated back. */
|
|
1675
|
+
panelChange = output();
|
|
1676
|
+
/** Global config service. */
|
|
1677
|
+
configService = inject(HubNavConfigService);
|
|
1678
|
+
/** Scoped state service (provided per component). */
|
|
1679
|
+
state = inject(HubNavStateService);
|
|
1680
|
+
/** Host element reference for click-outside detection. */
|
|
1681
|
+
elementRef = inject(ElementRef);
|
|
1682
|
+
/** NgZone for running outside-click handler inside Angular. */
|
|
1683
|
+
zone = inject(NgZone);
|
|
1684
|
+
/** Angular router for URL-based panel initialization. */
|
|
1685
|
+
router = inject(Router);
|
|
1686
|
+
/** Whether component lifecycle initialization has completed. */
|
|
1687
|
+
initialized = false;
|
|
1688
|
+
/** Last applied config signature used to prevent reactive no-op loops. */
|
|
1689
|
+
lastAppliedConfigSignature = '';
|
|
1690
|
+
/** Last breakpoint-related signature used to avoid redundant listener rebinds. */
|
|
1691
|
+
lastBreakpointSignature = '';
|
|
1692
|
+
/** Subscription for router NavigationEnd events. */
|
|
1693
|
+
routerSub;
|
|
1694
|
+
/** Media query list for responsive breakpoint detection. */
|
|
1695
|
+
mediaQuery = null;
|
|
1696
|
+
/** Bound handler for media query changes. */
|
|
1697
|
+
mediaQueryHandler = () => this.refreshCollapsedState();
|
|
1698
|
+
/** Bound handler for window resize fallback. */
|
|
1699
|
+
resizeHandler = () => this.refreshCollapsedState();
|
|
1700
|
+
/** Bound handler for document click events (click-outside). */
|
|
1701
|
+
documentClickHandler = null;
|
|
1702
|
+
/** Resolved config = global defaults + component overrides. */
|
|
1703
|
+
resolvedConfig = computed(() => this.configService.resolve(this.config()), ...(ngDevMode ? [{ debugName: "resolvedConfig" }] : []));
|
|
1704
|
+
/** Shortcut to resolved orientation. */
|
|
1705
|
+
resolvedOrientation = computed(() => this.resolvedConfig().orientation, ...(ngDevMode ? [{ debugName: "resolvedOrientation" }] : []));
|
|
1706
|
+
/** Whether panels are currently active (panel stack is non-empty). */
|
|
1707
|
+
hasPanels = computed(() => this.state.panelCount() > 0, ...(ngDevMode ? [{ debugName: "hasPanels" }] : []));
|
|
1708
|
+
/** The sidebar side from resolved config. */
|
|
1709
|
+
sidebarSide = computed(() => this.resolvedConfig().sidebarSide, ...(ngDevMode ? [{ debugName: "sidebarSide" }] : []));
|
|
1710
|
+
/** The panel width from resolved config. */
|
|
1711
|
+
panelWidth = computed(() => this.resolvedConfig().panelWidth, ...(ngDevMode ? [{ debugName: "panelWidth" }] : []));
|
|
1712
|
+
/** Sync config to state service whenever it changes. */
|
|
1713
|
+
configEffect = effect(() => {
|
|
1714
|
+
const resolvedConfig = this.resolvedConfig();
|
|
1715
|
+
const nextConfigSignature = this.buildConfigSignature(resolvedConfig);
|
|
1716
|
+
const nextBreakpointSignature = this.buildBreakpointSignature(resolvedConfig);
|
|
1717
|
+
if (nextConfigSignature !== this.lastAppliedConfigSignature) {
|
|
1718
|
+
this.lastAppliedConfigSignature = nextConfigSignature;
|
|
1719
|
+
this.state.setConfig(resolvedConfig);
|
|
1720
|
+
}
|
|
1721
|
+
if (this.initialized && nextBreakpointSignature !== this.lastBreakpointSignature) {
|
|
1722
|
+
this.lastBreakpointSignature = nextBreakpointSignature;
|
|
1723
|
+
this.teardownBreakpointListener();
|
|
1724
|
+
this.setupBreakpointListener();
|
|
1725
|
+
}
|
|
1726
|
+
}, ...(ngDevMode ? [{ debugName: "configEffect" }] : []));
|
|
1727
|
+
/** ARIA label for the nav element. */
|
|
1728
|
+
ariaLabel = computed(() => this.resolvedConfig().ariaLabel, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
1729
|
+
/** Whether the viewport is below the collapse breakpoint. */
|
|
1730
|
+
isCollapsed = computed(() => this.state.collapsed(), ...(ngDevMode ? [{ debugName: "isCollapsed" }] : []));
|
|
1731
|
+
/** Whether the mobile panel is open. */
|
|
1732
|
+
isMobileOpen = computed(() => this.state.mobileOpen(), ...(ngDevMode ? [{ debugName: "isMobileOpen" }] : []));
|
|
1733
|
+
/** Collapse mode from resolved config. */
|
|
1734
|
+
collapseMode = computed(() => this.resolvedConfig().collapseMode, ...(ngDevMode ? [{ debugName: "collapseMode" }] : []));
|
|
1735
|
+
/** Offcanvas position from resolved config. */
|
|
1736
|
+
offcanvasPosition = computed(() => this.resolvedConfig().offcanvasPosition, ...(ngDevMode ? [{ debugName: "offcanvasPosition" }] : []));
|
|
1737
|
+
/**
|
|
1738
|
+
* Whether sticky positioning should be activated for the current layout.
|
|
1739
|
+
* Sticky mode is restricted to vertical navigation and also applies while
|
|
1740
|
+
* the nav is collapsed on mobile so the toggler row stays pinned.
|
|
1741
|
+
*/
|
|
1742
|
+
isStickyActive = computed(() => this.resolvedConfig().position === 'sticky' && this.resolvedOrientation() === 'vertical', ...(ngDevMode ? [{ debugName: "isStickyActive" }] : []));
|
|
1743
|
+
/** @inheritDoc */
|
|
1744
|
+
ngOnInit() {
|
|
1745
|
+
this.initialized = true;
|
|
1746
|
+
// Ensure state receives the final resolved input config before any
|
|
1747
|
+
// route-driven panel preloading runs.
|
|
1748
|
+
const resolvedConfig = this.resolvedConfig();
|
|
1749
|
+
this.lastAppliedConfigSignature = this.buildConfigSignature(resolvedConfig);
|
|
1750
|
+
this.lastBreakpointSignature = this.buildBreakpointSignature(resolvedConfig);
|
|
1751
|
+
this.state.setConfig(resolvedConfig);
|
|
1752
|
+
this.setupBreakpointListener();
|
|
1753
|
+
this.setupClickOutsideListener();
|
|
1754
|
+
if (this.autoOpenFromRoute()) {
|
|
1755
|
+
if (this.isCollapsed()) {
|
|
1756
|
+
this.state.syncDropdownsWithRoute(this.items(), this.router.url);
|
|
1757
|
+
}
|
|
1758
|
+
else {
|
|
1759
|
+
this.openPanelsFromRoute(this.router.url);
|
|
1760
|
+
}
|
|
1761
|
+
this.routerSub = this.router.events
|
|
1762
|
+
.pipe(filter((e) => e instanceof NavigationEnd))
|
|
1763
|
+
.subscribe((e) => {
|
|
1764
|
+
const nextUrl = e.urlAfterRedirects;
|
|
1765
|
+
const nextPath = this.extractPath(nextUrl);
|
|
1766
|
+
if (this.isCollapsed()) {
|
|
1767
|
+
this.state.syncDropdownsWithRoute(this.items(), nextUrl);
|
|
1768
|
+
this._lastSyncedPath = nextPath;
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
// Fragment-only transitions should not rebuild panel stacks.
|
|
1772
|
+
if (nextPath === this._lastSyncedPath) {
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
this.openPanelsFromRoute(nextUrl);
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
/** @inheritDoc */
|
|
1780
|
+
ngOnDestroy() {
|
|
1781
|
+
this.initialized = false;
|
|
1782
|
+
this.teardownBreakpointListener();
|
|
1783
|
+
this.teardownClickOutsideListener();
|
|
1784
|
+
this.routerSub?.unsubscribe();
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Handles item click events bubbled up from the item list.
|
|
1788
|
+
*
|
|
1789
|
+
* @param payload - The clicked item and original DOM event.
|
|
1790
|
+
*/
|
|
1791
|
+
onItemClick(payload) {
|
|
1792
|
+
if (payload.item.route) {
|
|
1793
|
+
this.itemClick.emit(payload.item);
|
|
1794
|
+
// Keep parent sections open when the clicked item also owns children.
|
|
1795
|
+
// This is required for responsive/mobile accordion behavior and for
|
|
1796
|
+
// route-aware items that intentionally perform both actions.
|
|
1797
|
+
if (!payload.item.children?.length) {
|
|
1798
|
+
this.state.closeAllDropdowns();
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Toggles the mobile panel and emits the state change.
|
|
1804
|
+
*/
|
|
1805
|
+
onMobileToggle() {
|
|
1806
|
+
this.state.toggleMobile();
|
|
1807
|
+
if (this.state.mobileOpen() && this.autoOpenFromRoute()) {
|
|
1808
|
+
this.state.syncDropdownsWithRoute(this.items(), this.router.url);
|
|
1809
|
+
}
|
|
1810
|
+
this.mobileToggle.emit(this.state.mobileOpen());
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Closes the mobile panel.
|
|
1814
|
+
*/
|
|
1815
|
+
onMobilePanelClose() {
|
|
1816
|
+
this.state.setMobileOpen(false);
|
|
1817
|
+
this.mobileToggle.emit(false);
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Handles dropdown toggle events bubbled up from the item list.
|
|
1821
|
+
* The toggle has already been performed in the item-list; this just
|
|
1822
|
+
* emits the appropriate output event.
|
|
1823
|
+
*
|
|
1824
|
+
* @param item - The dropdown item that was toggled.
|
|
1825
|
+
*/
|
|
1826
|
+
onDropdownToggle(item) {
|
|
1827
|
+
if (this.state.isDropdownOpen(item.id)) {
|
|
1828
|
+
this.dropdownOpen.emit(item);
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
this.dropdownClose.emit(item);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
// ──────────────────────────────────────────────
|
|
1835
|
+
// Panel event handlers
|
|
1836
|
+
// ──────────────────────────────────────────────
|
|
1837
|
+
/**
|
|
1838
|
+
* Opens a new panel for the given item's children, or drills down if max panels reached.
|
|
1839
|
+
* Focuses the first item in the newly opened/drilled panel.
|
|
1840
|
+
*
|
|
1841
|
+
* @param item - The item whose children should be displayed in a panel.
|
|
1842
|
+
*/
|
|
1843
|
+
onPanelOpen(item) {
|
|
1844
|
+
const stackBefore = this.state.panelStack().length;
|
|
1845
|
+
this.state.openPanel(item, this.items());
|
|
1846
|
+
const stackAfter = this.state.panelStack().length;
|
|
1847
|
+
const panelIndex = stackAfter - 1;
|
|
1848
|
+
const action = stackAfter > stackBefore ? 'open' : 'drill-down';
|
|
1849
|
+
this.panelChange.emit({ item, panelIndex, action });
|
|
1850
|
+
// Focus the first item in the new/updated panel
|
|
1851
|
+
this.focusPanelFirstItem(panelIndex);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Closes a panel and all panels opened after it.
|
|
1855
|
+
* Returns focus to the previous panel or the main nav trigger.
|
|
1856
|
+
*
|
|
1857
|
+
* @param panelId - ID of the panel to close.
|
|
1858
|
+
*/
|
|
1859
|
+
onPanelClose(panelId) {
|
|
1860
|
+
const panel = this.state.getPanelById(panelId);
|
|
1861
|
+
if (panel) {
|
|
1862
|
+
const panelIndex = this.state.panelStack().indexOf(panel);
|
|
1863
|
+
this.state.closePanel(panelId);
|
|
1864
|
+
this.panelChange.emit({ item: panel.parentItem, panelIndex, action: 'close' });
|
|
1865
|
+
// Return focus to previous panel or the main nav
|
|
1866
|
+
this.returnFocusAfterPanelClose(panelIndex, panel.parentItem.id);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Navigates back within a panel's drill-down history.
|
|
1871
|
+
* Focuses the first item after navigating back.
|
|
1872
|
+
*
|
|
1873
|
+
* @param panelId - ID of the panel to navigate back in.
|
|
1874
|
+
*/
|
|
1875
|
+
onPanelBack(panelId) {
|
|
1876
|
+
const panel = this.state.getPanelById(panelId);
|
|
1877
|
+
if (panel) {
|
|
1878
|
+
const panelIndex = this.state.panelStack().indexOf(panel);
|
|
1879
|
+
this.state.navigateBackInPanel(panelId);
|
|
1880
|
+
this.panelChange.emit({ item: panel.parentItem, panelIndex, action: 'drill-back' });
|
|
1881
|
+
// Focus the first item in the panel after navigating back
|
|
1882
|
+
this.focusPanelFirstItem(panelIndex);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Focuses the first focusable item inside a panel at the given index.
|
|
1887
|
+
*
|
|
1888
|
+
* @param panelIndex - Zero-based index of the panel in the DOM.
|
|
1889
|
+
*/
|
|
1890
|
+
focusPanelFirstItem(panelIndex) {
|
|
1891
|
+
requestAnimationFrame(() => {
|
|
1892
|
+
const panels = this.elementRef.nativeElement.querySelectorAll('hub-nav-panel');
|
|
1893
|
+
const targetPanel = panels[panelIndex];
|
|
1894
|
+
if (targetPanel) {
|
|
1895
|
+
const firstItem = targetPanel.querySelector('.hub-nav-item__link, .hub-nav-item__dropdown-toggle');
|
|
1896
|
+
firstItem?.focus();
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Returns focus to the appropriate element after a panel is closed.
|
|
1902
|
+
* If a previous panel exists, focuses its trigger for the closed item.
|
|
1903
|
+
* Otherwise, returns focus to the main nav item-list.
|
|
1904
|
+
*
|
|
1905
|
+
* @param closedIndex - The index of the panel that was closed.
|
|
1906
|
+
* @param parentItemId - The ID of the parent item that triggered the closed panel.
|
|
1907
|
+
*/
|
|
1908
|
+
returnFocusAfterPanelClose(closedIndex, parentItemId) {
|
|
1909
|
+
requestAnimationFrame(() => {
|
|
1910
|
+
if (closedIndex > 0) {
|
|
1911
|
+
// Focus the trigger in the previous panel
|
|
1912
|
+
const panels = this.elementRef.nativeElement.querySelectorAll('hub-nav-panel');
|
|
1913
|
+
const prevPanel = panels[closedIndex - 1];
|
|
1914
|
+
if (prevPanel) {
|
|
1915
|
+
const trigger = prevPanel.querySelector(`hub-nav-item[data-item-id="${parentItemId}"] .hub-nav-item__link,` +
|
|
1916
|
+
`hub-nav-item[data-item-id="${parentItemId}"] .hub-nav-item__dropdown-toggle`);
|
|
1917
|
+
trigger?.focus();
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
// Fallback: focus the trigger in the main nav
|
|
1922
|
+
const mainTrigger = this.elementRef.nativeElement.querySelector(`hub-nav-item[data-item-id="${parentItemId}"] .hub-nav-item__link,` +
|
|
1923
|
+
`hub-nav-item[data-item-id="${parentItemId}"] .hub-nav-item__dropdown-toggle`);
|
|
1924
|
+
mainTrigger?.focus();
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Sets up a `matchMedia` listener for the configured collapse breakpoint.
|
|
1929
|
+
*/
|
|
1930
|
+
setupBreakpointListener() {
|
|
1931
|
+
const bp = this.resolvedConfig().collapseBreakpoint;
|
|
1932
|
+
if (bp <= 0 || typeof window === 'undefined') {
|
|
1933
|
+
this.state.setCollapsed(false);
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
this.mediaQuery = window.matchMedia(`(max-width: ${bp - 1}px)`);
|
|
1937
|
+
this.refreshCollapsedState();
|
|
1938
|
+
this.mediaQuery.addEventListener('change', this.mediaQueryHandler);
|
|
1939
|
+
window.addEventListener('resize', this.resizeHandler, { passive: true });
|
|
1940
|
+
}
|
|
1941
|
+
/** Tears down the `matchMedia` listener. */
|
|
1942
|
+
teardownBreakpointListener() {
|
|
1943
|
+
if (this.mediaQuery) {
|
|
1944
|
+
this.mediaQuery.removeEventListener('change', this.mediaQueryHandler);
|
|
1945
|
+
this.mediaQuery = null;
|
|
1946
|
+
}
|
|
1947
|
+
if (typeof window !== 'undefined') {
|
|
1948
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Registers a document click listener that closes dropdowns on outside clicks.
|
|
1953
|
+
* Panel drill-down mode panels are NOT closed by outside clicks.
|
|
1954
|
+
*/
|
|
1955
|
+
setupClickOutsideListener() {
|
|
1956
|
+
if (typeof document === 'undefined') {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
this.documentClickHandler = (event) => {
|
|
1960
|
+
const target = event.target;
|
|
1961
|
+
if (!this.elementRef.nativeElement.contains(target)) {
|
|
1962
|
+
this.zone.run(() => {
|
|
1963
|
+
// Close dropdowns/flyouts on outside click
|
|
1964
|
+
this.state.closeAllDropdowns();
|
|
1965
|
+
// DO NOT close panels — they should only close via explicit user action (back/close buttons)
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
document.addEventListener('click', this.documentClickHandler);
|
|
1970
|
+
}
|
|
1971
|
+
/** Removes the document click listener. */
|
|
1972
|
+
teardownClickOutsideListener() {
|
|
1973
|
+
if (this.documentClickHandler) {
|
|
1974
|
+
document.removeEventListener('click', this.documentClickHandler);
|
|
1975
|
+
this.documentClickHandler = null;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Callback invoked when the viewport crosses the collapse breakpoint.
|
|
1980
|
+
*
|
|
1981
|
+
* @param belowBreakpoint - `true` if the viewport is now below the breakpoint.
|
|
1982
|
+
*/
|
|
1983
|
+
onBreakpointChange(belowBreakpoint) {
|
|
1984
|
+
if (belowBreakpoint === this.isCollapsed()) {
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
this.state.setCollapsed(belowBreakpoint);
|
|
1988
|
+
if (!belowBreakpoint) {
|
|
1989
|
+
this.state.closeAllDropdowns();
|
|
1990
|
+
this.state.closeAllPanels();
|
|
1991
|
+
this.mobileToggle.emit(false);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Recomputes the collapsed state from the current viewport width.
|
|
1996
|
+
* This complements `matchMedia` events, which can become unreliable in
|
|
1997
|
+
* some browser/devtools combinations when the viewport is resized or
|
|
1998
|
+
* docked without emitting the expected media-query change sequence.
|
|
1999
|
+
*/
|
|
2000
|
+
refreshCollapsedState() {
|
|
2001
|
+
if (typeof window === 'undefined') {
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
const bp = this.resolvedConfig().collapseBreakpoint;
|
|
2005
|
+
if (bp <= 0) {
|
|
2006
|
+
this.onBreakpointChange(false);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
this.onBreakpointChange(window.innerWidth < bp);
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Parses the given URL and opens the panels that correspond to the active route.
|
|
2013
|
+
*
|
|
2014
|
+
* The algorithm:
|
|
2015
|
+
* 1. Extract the path without fragment/query: `/calendar/examples` → `['calendar', 'examples']`
|
|
2016
|
+
* 2. Find the root item whose id matches the first segment.
|
|
2017
|
+
* 3. Open a panel for that root item (closes any existing panels first).
|
|
2018
|
+
* 4. If a second segment exists and matches a child item that has its own
|
|
2019
|
+
* children (e.g. the "Examples" dropdown), open a second panel for it.
|
|
2020
|
+
*
|
|
2021
|
+
* @param url - The full URL including optional fragment, e.g. `/calendar/examples#cal-basic`.
|
|
2022
|
+
*/
|
|
2023
|
+
openPanelsFromRoute(url) {
|
|
2024
|
+
// Strip query string and fragment
|
|
2025
|
+
const path = url.split('?')[0].split('#')[0];
|
|
2026
|
+
this._lastSyncedPath = path;
|
|
2027
|
+
const segments = path.split('/').filter(Boolean);
|
|
2028
|
+
if (segments.length === 0) {
|
|
2029
|
+
this.state.closeAllPanels();
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
const rootItems = this.items();
|
|
2033
|
+
const libraryId = segments[0];
|
|
2034
|
+
// Find the matching root nav item
|
|
2035
|
+
const rootItem = rootItems.find((item) => item.id === libraryId);
|
|
2036
|
+
if (!rootItem?.children?.length) {
|
|
2037
|
+
this.state.closeAllPanels();
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
const currentStack = this.state.panelStack();
|
|
2041
|
+
const hasMatchingRootPanel = this.isRootContextVisible(rootItem, currentStack);
|
|
2042
|
+
// Keep the existing root panel when it already matches the route context.
|
|
2043
|
+
// This avoids unnecessary teardown/recreate flicker when switching between
|
|
2044
|
+
// second-level links (Overview, API, Styles, etc.) in the same library.
|
|
2045
|
+
if (!hasMatchingRootPanel) {
|
|
2046
|
+
this.state.closeAllPanels();
|
|
2047
|
+
this.state.openPanel(rootItem, rootItems);
|
|
2048
|
+
}
|
|
2049
|
+
else {
|
|
2050
|
+
this.ensureRootPanelContext(rootItem);
|
|
2051
|
+
}
|
|
2052
|
+
// If a second path segment exists, find and open the matching child panel
|
|
2053
|
+
if (segments.length > 1) {
|
|
2054
|
+
const sectionSegment = segments[1];
|
|
2055
|
+
const sectionItem = rootItem.children.find((child) => child.children?.length && child.route?.toString().split('/').pop() === sectionSegment);
|
|
2056
|
+
if (sectionItem) {
|
|
2057
|
+
// Keep root-level replacement logic scoped to true root items.
|
|
2058
|
+
// Passing rootItems here preserves drill-down behavior when
|
|
2059
|
+
// `panelMaxVisible` forces nested levels to replace the last panel.
|
|
2060
|
+
if (!this.state.isPanelOpenForItem(sectionItem.id)) {
|
|
2061
|
+
this.state.openPanel(sectionItem, rootItems);
|
|
2062
|
+
}
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
// No nested section panel should be visible for plain second-level links.
|
|
2067
|
+
if (this.state.panelStack().length > 1) {
|
|
2068
|
+
this.state.closePanelsFromIndex(1);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Checks whether the first panel in the current stack belongs to the same
|
|
2073
|
+
* root route context.
|
|
2074
|
+
*
|
|
2075
|
+
* This remains true even when panel mode drill-down has replaced the first
|
|
2076
|
+
* panel content (the original root items are still present in panel history).
|
|
2077
|
+
*
|
|
2078
|
+
* @param rootItem - Root nav item resolved from the current route.
|
|
2079
|
+
* @param stack - Current panel stack snapshot.
|
|
2080
|
+
* @returns `true` when the stack already belongs to the same root context.
|
|
2081
|
+
*/
|
|
2082
|
+
isRootContextVisible(rootItem, stack) {
|
|
2083
|
+
const firstPanel = stack[0];
|
|
2084
|
+
if (!firstPanel) {
|
|
2085
|
+
return false;
|
|
2086
|
+
}
|
|
2087
|
+
if (firstPanel.parentItem.id === rootItem.id) {
|
|
2088
|
+
return true;
|
|
2089
|
+
}
|
|
2090
|
+
// Compare by item identity instead of array reference because host apps
|
|
2091
|
+
// often rebuild nav item arrays reactively (new references, same content).
|
|
2092
|
+
return firstPanel.history.some((entry) => this.isSameItemCollection(entry.items, rootItem.children ?? []));
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Compares two item collections by stable item IDs.
|
|
2096
|
+
*
|
|
2097
|
+
* @param source - First collection.
|
|
2098
|
+
* @param target - Second collection.
|
|
2099
|
+
* @returns `true` when both collections contain the same IDs in the same order.
|
|
2100
|
+
*/
|
|
2101
|
+
isSameItemCollection(source, target) {
|
|
2102
|
+
if (source.length !== target.length) {
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
2105
|
+
for (let index = 0; index < source.length; index++) {
|
|
2106
|
+
if (source[index].id !== target[index].id) {
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Restores the first panel to the root context when it is currently showing
|
|
2114
|
+
* drill-down content for a child section (e.g. the "Examples" third level).
|
|
2115
|
+
*
|
|
2116
|
+
* @param rootItem - Root nav item resolved from the current route.
|
|
2117
|
+
*/
|
|
2118
|
+
ensureRootPanelContext(rootItem) {
|
|
2119
|
+
const firstPanel = this.state.panelStack()[0];
|
|
2120
|
+
if (!firstPanel || firstPanel.parentItem.id === rootItem.id) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
// Walk back through drill-down history until the root section is visible.
|
|
2124
|
+
while (true) {
|
|
2125
|
+
const panel = this.state.getPanelById(firstPanel.id);
|
|
2126
|
+
if (!panel || panel.parentItem.id === rootItem.id || panel.history.length === 0) {
|
|
2127
|
+
break;
|
|
2128
|
+
}
|
|
2129
|
+
this.state.navigateBackInPanel(firstPanel.id);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Extracts the path part from a URL-like string, removing query and fragment.
|
|
2134
|
+
*
|
|
2135
|
+
* @param url - Router URL.
|
|
2136
|
+
* @returns Normalized path for route-state comparisons.
|
|
2137
|
+
*/
|
|
2138
|
+
extractPath(url) {
|
|
2139
|
+
return url.split('?')[0].split('#')[0];
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Builds a stable signature for the effective nav config so repeated
|
|
2143
|
+
* reactivity passes with identical values do not trigger work.
|
|
2144
|
+
*
|
|
2145
|
+
* @param config - Effective nav config.
|
|
2146
|
+
* @returns Stable string signature.
|
|
2147
|
+
*/
|
|
2148
|
+
buildConfigSignature(config) {
|
|
2149
|
+
return JSON.stringify({
|
|
2150
|
+
orientation: config.orientation,
|
|
2151
|
+
verticalExpandMode: config.verticalExpandMode,
|
|
2152
|
+
dropdownTrigger: config.dropdownTrigger,
|
|
2153
|
+
position: config.position,
|
|
2154
|
+
stickyTop: config.stickyTop,
|
|
2155
|
+
collapseMode: config.collapseMode,
|
|
2156
|
+
collapseBreakpoint: config.collapseBreakpoint,
|
|
2157
|
+
offcanvasPosition: config.offcanvasPosition,
|
|
2158
|
+
ariaLabel: config.ariaLabel,
|
|
2159
|
+
panelMaxVisible: config.panelMaxVisible,
|
|
2160
|
+
sidebarSide: config.sidebarSide,
|
|
2161
|
+
panelWidth: config.panelWidth
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Builds a stable signature for breakpoint listener inputs.
|
|
2166
|
+
*
|
|
2167
|
+
* @param config - Effective nav config.
|
|
2168
|
+
* @returns Stable breakpoint signature.
|
|
2169
|
+
*/
|
|
2170
|
+
buildBreakpointSignature(config) {
|
|
2171
|
+
return JSON.stringify({
|
|
2172
|
+
collapseBreakpoint: config.collapseBreakpoint,
|
|
2173
|
+
orientation: config.orientation
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2177
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: HubNavComponent, isStandalone: true, selector: "hub-nav", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, navClass: { classPropertyName: "navClass", publicName: "navClass", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, autoOpenFromRoute: { classPropertyName: "autoOpenFromRoute", publicName: "autoOpenFromRoute", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", dropdownOpen: "dropdownOpen", dropdownClose: "dropdownClose", mobileToggle: "mobileToggle", panelChange: "panelChange" }, host: { properties: { "class.hub-nav--horizontal": "resolvedOrientation() === \"horizontal\"", "class.hub-nav--vertical": "resolvedOrientation() === \"vertical\"", "class.hub-nav--collapsed": "isCollapsed()", "class.hub-nav--sticky": "isStickyActive()", "class.hub-nav--fixed": "resolvedConfig().position === \"fixed\"", "class.hub-nav--sidebar-left": "resolvedConfig().sidebarSide === \"left\"", "class.hub-nav--sidebar-right": "resolvedConfig().sidebarSide === \"right\"", "style.width": "resolvedOrientation() === \"vertical\" ? \"100%\" : null", "style.align-self": "resolvedOrientation() === \"vertical\" ? \"stretch\" : null", "style.--hub-nav-sticky-top": "resolvedConfig().stickyTop", "style.display": "resolvedOrientation() === \"vertical\" ? \"flex\" : null", "style.flex-direction": "resolvedOrientation() === \"vertical\" ? \"column\" : null" }, classAttribute: "hub-nav" }, providers: [HubNavStateService], queries: [{ propertyName: "startDirective", first: true, predicate: HubNavStartDirective, descendants: true, isSignal: true }, { propertyName: "endDirective", first: true, predicate: HubNavEndDirective, descendants: true, isSignal: true }, { propertyName: "itemTemplateDirective", first: true, predicate: HubNavItemTemplateDirective, descendants: true, isSignal: true }], ngImport: i0, template: "<nav [attr.aria-label]=\"ariaLabel()\" [class]=\"navClass()\">\n\t<!-- Toggler: visible when collapsed (below breakpoint) -->\n\t@if (isCollapsed()) {\n\t\t@if (startTemplate()) {\n\t\t\t<div class=\"hub-nav-slot hub-nav-slot--start\">\n\t\t\t\t<ng-container\n\t\t\t\t\t[ngTemplateOutlet]=\"startTemplate()!\"\n\t\t\t\t\t[ngTemplateOutletContext]=\"startContext()\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t}\n\n\t\t@if (endTemplate()) {\n\t\t\t<div class=\"hub-nav-slot hub-nav-slot--end\">\n\t\t\t\t<ng-container\n\t\t\t\t\t[ngTemplateOutlet]=\"endTemplate()!\"\n\t\t\t\t\t[ngTemplateOutletContext]=\"endContext()\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t}\n\n\t\t<hub-nav-toggler\n\t\t\t[isOpen]=\"isMobileOpen()\"\n\t\t\t(toggle)=\"onMobileToggle()\"\n\t\t/>\n\t}\n\n\t<!-- Desktop horizontal navigation: split into two explicit rows -->\n\t@if (!isCollapsed() && resolvedOrientation() === 'horizontal') {\n\t\t<div class=\"hub-nav__horizontal-top-row\">\n\t\t\t@if (startTemplate()) {\n\t\t\t\t<div class=\"hub-nav-slot hub-nav-slot--start\">\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"startTemplate()!\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"startContext()\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t}\n\n\t\t\t<div class=\"hub-nav__horizontal-root-scroll\">\n\t\t\t\t<hub-nav-item-list\n\t\t\t\t\tclass=\"hub-nav__horizontal-root-list\"\n\t\t\t\t\t[items]=\"items()\"\n\t\t\t\t\t[depth]=\"0\"\n\t\t\t\t\t[itemTemplate]=\"resolvedItemTemplate()\"\n\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t\t\t\t/>\n\t\t\t</div>\n\n\t\t\t@if (endTemplate()) {\n\t\t\t\t<div class=\"hub-nav-slot hub-nav-slot--end\">\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"endTemplate()!\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"endContext()\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\n\t\t@if (hasPanels()) {\n\t\t\t<div class=\"hub-nav__horizontal-panel-row\">\n\t\t\t\t<hub-nav-panel-container\n\t\t\t\t\t[panels]=\"state.panelStack()\"\n\t\t\t\t\t[sidebarSide]=\"sidebarSide()\"\n\t\t\t\t\t[panelWidth]=\"panelWidth()\"\n\t\t\t\t\t[hideFirstPanelHeader]=\"true\"\n\t\t\t\t\t[itemTemplate]=\"resolvedItemTemplate()\"\n\t\t\t\t\t(panelClose)=\"onPanelClose($event)\"\n\t\t\t\t\t(panelBack)=\"onPanelBack($event)\"\n\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t}\n\t}\n\n\t<!-- Desktop vertical navigation -->\n\t@if (!isCollapsed() && resolvedOrientation() === 'vertical') {\n\t\t<div class=\"hub-nav__main-and-panels\">\n\t\t\t<div class=\"hub-nav__primary-column\">\n\t\t\t\t<!-- Start slot in vertical desktop mode: constrained to the primary column only -->\n\t\t\t\t@if (startTemplate()) {\n\t\t\t\t\t<div class=\"hub-nav-slot hub-nav-slot--start\">\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"startTemplate()!\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"startContext()\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t\t<hub-nav-item-list\n\t\t\t\t\t[items]=\"items()\"\n\t\t\t\t\t[depth]=\"0\"\n\t\t\t\t\t[itemTemplate]=\"resolvedItemTemplate()\"\n\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t\t\t\t/>\n\n\t\t\t\t<!-- End slot in vertical desktop mode: anchored to the bottom of the primary column -->\n\t\t\t\t@if (endTemplate()) {\n\t\t\t\t\t<div class=\"hub-nav-slot hub-nav-slot--end\">\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"endTemplate()!\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"endContext()\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t</div>\n\n\t\t\t@if (hasPanels()) {\n\t\t\t\t<hub-nav-panel-container\n\t\t\t\t\t[panels]=\"state.panelStack()\"\n\t\t\t\t\t[sidebarSide]=\"sidebarSide()\"\n\t\t\t\t\t[panelWidth]=\"panelWidth()\"\n\t\t\t\t\t[hideFirstPanelHeader]=\"true\"\n\t\t\t\t\t[itemTemplate]=\"resolvedItemTemplate()\"\n\t\t\t\t\t(panelClose)=\"onPanelClose($event)\"\n\t\t\t\t\t(panelBack)=\"onPanelBack($event)\"\n\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t\t\t\t/>\n\t\t\t}\n\t\t</div>\n\t}\n\n\t<!-- Mobile panel: shown when collapsed and open -->\n\t@if (isCollapsed()) {\n\t\t<hub-nav-mobile-panel\n\t\t\t[items]=\"items()\"\n\t\t\t[isOpen]=\"isMobileOpen()\"\n\t\t\t[mode]=\"collapseMode()\"\n\t\t\t[offcanvasPosition]=\"offcanvasPosition()\"\n\t\t\t(closePanel)=\"onMobilePanelClose()\"\n\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t/>\n\t}\n</nav>\n", styles: [":root,:host{--hub-nav-height: 3.5rem;--hub-nav-padding-x: var(--hub-ref-space-3, 1rem);--hub-nav-padding-y: var(--hub-ref-space-2, .5rem);--hub-nav-gap: var(--hub-ref-space-1, .25rem);--hub-nav-horizontal-padding-y: 0;--hub-nav-horizontal-padding-x: 0;--hub-nav-horizontal-row-gap: 0;--hub-nav-horizontal-items-justify: center;--hub-nav-horizontal-items-overflow-x: auto;--hub-nav-horizontal-items-overflow-y: hidden;--hub-nav-vertical-items-overflow-y: auto;--hub-nav-vertical-items-overflow-x: hidden;--hub-nav-vertical-panel-padding-inline: var(--hub-ref-space-2, .5rem);--hub-nav-vertical-panel-padding-block: 0;--hub-nav-horizontal-panel-padding-y: var(--hub-ref-space-2, .5rem);--hub-nav-horizontal-panel-padding-x: 0;--hub-nav-scrollbar-width: none;--hub-nav-scrollbar-color: transparent transparent;--hub-nav-scrollbar-webkit-size: 0;--hub-nav-scrollbar-thumb-color: transparent;--hub-nav-scrollbar-track-color: transparent;--hub-nav-scrollbar-thumb-radius: var(--hub-ref-radius-pill, 999px);--hub-nav-bg: var(--hub-sys-surface-page, #fff);--hub-nav-color: var(--hub-sys-text-primary, #212529);--hub-nav-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-border-width: 1px;--hub-nav-border-style: solid;--hub-nav-brand-font-size: var(--hub-ref-font-size-lg, 1.25rem);--hub-nav-brand-font-weight: var(--hub-ref-font-weight-semibold, 600);--hub-nav-brand-color: var(--hub-sys-text-primary, #212529);--hub-nav-brand-padding-x: var(--hub-ref-space-2, .5rem);--hub-nav-item-padding-x: var(--hub-ref-space-3, 1rem);--hub-nav-item-padding-y: var(--hub-ref-space-2, .5rem);--hub-nav-item-color: var(--hub-sys-text-primary, #212529);--hub-nav-item-font-size: var(--hub-ref-font-size-base, 1rem);--hub-nav-item-font-weight: var(--hub-ref-font-weight-normal, 400);--hub-nav-item-border-radius: var(--hub-ref-border-radius-sm, .25rem);--hub-nav-item-transition: var(--hub-sys-transition-fast, .15s ease);--hub-nav-item-hover-bg: var(--hub-sys-state-hover-overlay, rgba(0, 0, 0, .04));--hub-nav-item-hover-color: var(--hub-sys-color-primary, #0d6efd);--hub-nav-item-active-bg: var(--hub-sys-color-primary, #0d6efd);--hub-nav-item-active-color: #fff;--hub-nav-item-active-font-weight: var(--hub-ref-font-weight-semibold, 600);--hub-nav-item-disabled-color: var(--hub-sys-text-muted, #6c757d);--hub-nav-item-disabled-opacity: var(--hub-sys-opacity-disabled, .65);--hub-nav-dropdown-bg: var(--hub-sys-surface-elevated, #fff);--hub-nav-dropdown-color: var(--hub-sys-text-primary, #212529);--hub-nav-dropdown-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-dropdown-border-radius: var(--hub-nav-item-border-radius, var(--hub-ref-border-radius-sm, .25rem));--hub-nav-dropdown-shadow: var(--hub-sys-shadow-md, 0 .5rem 1rem rgba(0, 0, 0, .15));--hub-nav-dropdown-padding-y: var(--hub-ref-space-2, .5rem);--hub-nav-dropdown-min-width: 12rem;--hub-nav-dropdown-z-index: var(--hub-sys-z-index-dropdown, 1000);--hub-nav-caret-size: .3rem;--hub-nav-caret-color: currentColor;--hub-nav-header-font-size: var(--hub-ref-font-size-sm, .875rem);--hub-nav-header-font-weight: var(--hub-ref-font-weight-semibold, 600);--hub-nav-header-color: var(--hub-sys-text-muted, #6c757d);--hub-nav-header-padding-x: var(--hub-ref-space-3, 1rem);--hub-nav-header-padding-y: var(--hub-ref-space-1, .25rem);--hub-nav-header-margin-top: var(--hub-ref-space-3, .75rem);--hub-nav-header-margin-inline-start: var(--hub-ref-space-3, .75rem);--hub-nav-header-horizontal-padding-inline-start: 2rem;--hub-nav-separator-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-separator-margin-y: var(--hub-ref-space-2, .5rem);--hub-nav-badge-font-size: var(--hub-ref-font-size-xs, .75rem);--hub-nav-badge-padding-x: .5em;--hub-nav-badge-padding-y: .25em;--hub-nav-badge-bg: var(--hub-sys-color-danger, #dc3545);--hub-nav-badge-color: #fff;--hub-nav-badge-border-radius: var(--hub-ref-border-radius-pill, 50rem);--hub-nav-toggler-padding-x: var(--hub-ref-space-2, .5rem);--hub-nav-toggler-padding-y: var(--hub-ref-space-1, .25rem);--hub-nav-toggler-font-size: var(--hub-ref-font-size-lg, 1.25rem);--hub-nav-toggler-color: var(--hub-sys-text-primary, #212529);--hub-nav-toggler-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-toggler-border-radius: var(--hub-ref-border-radius-sm, .25rem);--hub-nav-mobile-bg: var(--hub-sys-surface-page, #fff);--hub-nav-mobile-color: var(--hub-sys-text-primary, #212529);--hub-nav-mobile-width: 18rem;--hub-nav-mobile-z-index: var(--hub-sys-z-index-fixed, 1030);--hub-nav-mobile-backdrop-bg: rgba(0, 0, 0, .5);--hub-nav-mobile-transition: var(--hub-sys-transition-base, .3s ease);--hub-nav-accordion-indent: var(--hub-ref-space-3, 1rem);--hub-nav-accordion-transition: var(--hub-sys-transition-collapse, .35s ease);--hub-nav-icon-size: var(--hub-ref-icon-size, 1.25rem);--hub-nav-icon-gap: var(--hub-ref-space-2, .5rem);--hub-nav-panel-width: 16rem;--hub-nav-panel-bg: var(--hub-sys-surface-elevated, #fff);--hub-nav-panel-color: var(--hub-sys-text-primary, #212529);--hub-nav-panel-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-panel-border-width: 1px;--hub-nav-panel-shadow: var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075));--hub-nav-panel-z-index: var(--hub-sys-z-index-dropdown, 1000);--hub-nav-panel-transition: transform var(--hub-sys-transition-base, .3s ease);--hub-nav-panel-header-height: 3rem;--hub-nav-panel-header-bg: var(--hub-sys-surface-page, #fff);--hub-nav-panel-header-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-panel-header-font-size: var(--hub-ref-font-size-sm, .875rem);--hub-nav-panel-header-font-weight: var(--hub-ref-font-weight-semibold, 600);--hub-nav-panel-header-padding-x: var(--hub-ref-space-3, 1rem);--hub-nav-panel-back-color: var(--hub-sys-text-primary, #212529);--hub-nav-panel-back-hover-bg: var(--hub-sys-state-hover-overlay, rgba(0, 0, 0, .04));--hub-nav-panel-back-size: 2rem}:host{display:block}nav{display:flex;background-color:var(--hub-nav-bg, #fff);color:var(--hub-nav-color, #212529);border:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-border-color, #dee2e6);font-family:inherit}:host(.hub-nav--horizontal) nav{width:100%}:host(.hub-nav--horizontal) nav{display:flex;flex-direction:column;align-items:stretch;padding:var(--hub-nav-horizontal-padding-y, 0) var(--hub-nav-horizontal-padding-x, 0);gap:var(--hub-nav-horizontal-row-gap, 0)}:host(.hub-nav--vertical) nav{flex:1;min-height:0;flex-direction:column;padding:var(--hub-nav-padding-y, .5rem) var(--hub-nav-padding-x, 1rem);gap:var(--hub-nav-gap, .25rem)}:host(.hub-nav--vertical) nav>hub-nav-item-list{display:flex;flex-direction:column;gap:var(--hub-nav-gap, .25rem)}:host(.hub-nav--collapsed) nav,:host(.hub-nav--collapsed.hub-nav--vertical) nav,:host(.hub-nav--collapsed.hub-nav--horizontal) nav{display:flex;flex:0 0 auto;width:100%;min-height:auto;flex-direction:row;align-items:center;flex-wrap:nowrap;gap:var(--hub-nav-gap, .25rem);padding:var(--hub-nav-padding-y, .5rem) var(--hub-nav-padding-x, 1rem);overflow:hidden}.hub-nav__main-and-panels{display:flex;flex:1;min-height:0;align-items:stretch}.hub-nav__main-and-panels>hub-nav-panel-container{flex:1;min-height:0}.hub-nav__primary-column{display:flex;flex-direction:column;min-height:0}:host(.hub-nav--sidebar-left) .hub-nav__main-and-panels{flex-direction:row}:host(.hub-nav--sidebar-right) .hub-nav__main-and-panels{flex-direction:row-reverse}:host(.hub-nav--vertical) .hub-nav__main-and-panels>.hub-nav__primary-column{display:flex;flex-direction:column;flex:0 0 var(--hub-nav-panel-width, 16rem);width:var(--hub-nav-panel-width, 16rem);min-width:var(--hub-nav-panel-width, 16rem)}:host(.hub-nav--vertical) .hub-nav__main-and-panels>.hub-nav__primary-column>hub-nav-item-list{display:flex;flex-direction:column;gap:var(--hub-nav-gap, .25rem);flex:1 1 auto;min-height:0;padding-inline:var(--hub-nav-vertical-panel-padding-inline, .5rem);padding-block:var(--hub-nav-vertical-panel-padding-block, 0);overflow-y:var(--hub-nav-vertical-items-overflow-y, auto);overflow-x:var(--hub-nav-vertical-items-overflow-x, hidden)}:host(.hub-nav--vertical) .hub-nav__main-and-panels>hub-nav-panel-container{flex:0 0 auto;width:auto;min-width:0}:host(.hub-nav--horizontal) .hub-nav__horizontal-top-row{display:flex;align-items:center;gap:var(--hub-nav-gap, .25rem)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-list{display:flex;flex-direction:row;align-items:center;justify-content:var(--hub-nav-horizontal-items-justify, center);gap:var(--hub-nav-gap, .25rem);flex:0 0 auto;width:max-content;min-width:100%;margin-inline:auto;overflow:visible}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll{display:flex;align-items:center;justify-content:flex-start;flex:1 1 auto;min-width:0;padding:var(--hub-nav-horizontal-panel-padding-y, .5rem) var(--hub-nav-horizontal-panel-padding-x, 0);overflow-x:var(--hub-nav-horizontal-items-overflow-x, auto);overflow-y:var(--hub-nav-horizontal-items-overflow-y, hidden)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll,:host(.hub-nav--vertical) .hub-nav__primary-column>hub-nav-item-list{-ms-overflow-style:none;scrollbar-width:var(--hub-nav-scrollbar-width, none);scrollbar-color:var(--hub-nav-scrollbar-color, transparent transparent)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll::-webkit-scrollbar,:host(.hub-nav--vertical) .hub-nav__primary-column>hub-nav-item-list::-webkit-scrollbar{width:var(--hub-nav-scrollbar-webkit-size, 0);height:var(--hub-nav-scrollbar-webkit-size, 0)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll::-webkit-scrollbar-thumb,:host(.hub-nav--vertical) .hub-nav__primary-column>hub-nav-item-list::-webkit-scrollbar-thumb{background-color:var(--hub-nav-scrollbar-thumb-color, transparent);border-radius:var(--hub-nav-scrollbar-thumb-radius, 999px)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll::-webkit-scrollbar-track,:host(.hub-nav--vertical) .hub-nav__primary-column>hub-nav-item-list::-webkit-scrollbar-track{background-color:var(--hub-nav-scrollbar-track-color, transparent)}:host(.hub-nav--horizontal) .hub-nav__horizontal-panel-row{width:100%;min-width:0}:host(.hub-nav--horizontal) .hub-nav__horizontal-panel-row>hub-nav-panel-container{width:100%;min-width:0}.hub-nav-slot{display:flex;align-items:center;justify-content:flex-start;padding:0 var(--hub-nav-brand-padding-x, .5rem);font-size:var(--hub-nav-brand-font-size, 1.25rem);font-weight:var(--hub-nav-brand-font-weight, 600);color:var(--hub-nav-brand-color, #212529);text-decoration:none;white-space:nowrap;flex-shrink:0}:host(.hub-nav--vertical) .hub-nav-slot--start{width:100%;align-self:stretch;box-sizing:border-box;padding-bottom:var(--hub-nav-padding-y, .5rem);border-bottom:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-border-color, #dee2e6);margin-bottom:var(--hub-nav-gap, .25rem)}:host(.hub-nav--vertical) .hub-nav-slot--end{width:100%;align-self:stretch;box-sizing:border-box;margin-top:auto;padding-top:var(--hub-nav-padding-y, .5rem);border-top:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-border-color, #dee2e6)}:host(.hub-nav--horizontal) .hub-nav-slot--end{margin-inline-start:auto;justify-content:flex-end}:host(.hub-nav--horizontal) .hub-nav-slot--start,:host(.hub-nav--horizontal) .hub-nav-slot--end{align-self:center;height:auto;min-height:0;max-height:none}:host(.hub-nav--collapsed.hub-nav--vertical) .hub-nav-slot--start,:host(.hub-nav--collapsed.hub-nav--vertical) .hub-nav-slot--end,:host(.hub-nav--collapsed.hub-nav--horizontal) .hub-nav-slot--start,:host(.hub-nav--collapsed.hub-nav--horizontal) .hub-nav-slot--end{width:0;align-self:stretch;display:flex;align-items:center;flex:1 1 0;min-width:0;border:0;margin:0;padding:0 var(--hub-nav-brand-padding-x, .5rem)}:host(.hub-nav--collapsed) .hub-nav-slot--start{justify-content:flex-start}:host(.hub-nav--collapsed) .hub-nav-slot--end{margin-inline-start:0;justify-content:flex-end}:host(.hub-nav--collapsed) hub-nav-toggler{flex:0 0 auto;display:flex;align-items:center;padding-inline:var(--hub-nav-brand-padding-x, .5rem)}:host(.hub-nav--sticky){position:sticky;top:var(--hub-nav-sticky-top, 0px);align-self:flex-start;max-height:calc(100vh - var(--hub-nav-sticky-top, 0px));z-index:var(--hub-sys-z-index-sticky, 1020)}:host(.hub-nav--fixed){position:fixed;top:0;left:0;right:0;z-index:var(--hub-sys-z-index-fixed, 1030)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: HubNavItemListComponent, selector: "hub-nav-item-list", inputs: ["items", "depth", "itemTemplate", "forceAccordionMode"], outputs: ["itemClick", "dropdownToggle", "panelOpen"] }, { kind: "component", type: HubNavTogglerComponent, selector: "hub-nav-toggler", inputs: ["isOpen"], outputs: ["toggle"] }, { kind: "component", type: HubNavMobilePanelComponent, selector: "hub-nav-mobile-panel", inputs: ["items", "isOpen", "mode", "offcanvasPosition"], outputs: ["closePanel", "itemClick", "dropdownToggle"] }, { kind: "component", type: HubNavPanelContainerComponent, selector: "hub-nav-panel-container", inputs: ["panels", "sidebarSide", "panelWidth", "itemTemplate", "hideFirstPanelHeader"], outputs: ["panelClose", "panelBack", "itemClick", "dropdownToggle", "panelOpen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2178
|
+
}
|
|
2179
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: HubNavComponent, decorators: [{
|
|
2180
|
+
type: Component,
|
|
2181
|
+
args: [{ selector: 'hub-nav', standalone: true, imports: [
|
|
2182
|
+
NgTemplateOutlet,
|
|
2183
|
+
HubNavItemListComponent,
|
|
2184
|
+
HubNavTogglerComponent,
|
|
2185
|
+
HubNavMobilePanelComponent,
|
|
2186
|
+
HubNavPanelContainerComponent
|
|
2187
|
+
], providers: [HubNavStateService], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2188
|
+
class: 'hub-nav',
|
|
2189
|
+
'[class.hub-nav--horizontal]': 'resolvedOrientation() === "horizontal"',
|
|
2190
|
+
'[class.hub-nav--vertical]': 'resolvedOrientation() === "vertical"',
|
|
2191
|
+
'[class.hub-nav--collapsed]': 'isCollapsed()',
|
|
2192
|
+
'[class.hub-nav--sticky]': 'isStickyActive()',
|
|
2193
|
+
'[class.hub-nav--fixed]': 'resolvedConfig().position === "fixed"',
|
|
2194
|
+
'[class.hub-nav--sidebar-left]': 'resolvedConfig().sidebarSide === "left"',
|
|
2195
|
+
'[class.hub-nav--sidebar-right]': 'resolvedConfig().sidebarSide === "right"',
|
|
2196
|
+
'[style.width]': 'resolvedOrientation() === "vertical" ? "100%" : null',
|
|
2197
|
+
'[style.align-self]': 'resolvedOrientation() === "vertical" ? "stretch" : null',
|
|
2198
|
+
'[style.--hub-nav-sticky-top]': 'resolvedConfig().stickyTop',
|
|
2199
|
+
'[style.display]': 'resolvedOrientation() === "vertical" ? "flex" : null',
|
|
2200
|
+
'[style.flex-direction]': 'resolvedOrientation() === "vertical" ? "column" : null'
|
|
2201
|
+
}, template: "<nav [attr.aria-label]=\"ariaLabel()\" [class]=\"navClass()\">\n\t<!-- Toggler: visible when collapsed (below breakpoint) -->\n\t@if (isCollapsed()) {\n\t\t@if (startTemplate()) {\n\t\t\t<div class=\"hub-nav-slot hub-nav-slot--start\">\n\t\t\t\t<ng-container\n\t\t\t\t\t[ngTemplateOutlet]=\"startTemplate()!\"\n\t\t\t\t\t[ngTemplateOutletContext]=\"startContext()\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t}\n\n\t\t@if (endTemplate()) {\n\t\t\t<div class=\"hub-nav-slot hub-nav-slot--end\">\n\t\t\t\t<ng-container\n\t\t\t\t\t[ngTemplateOutlet]=\"endTemplate()!\"\n\t\t\t\t\t[ngTemplateOutletContext]=\"endContext()\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t}\n\n\t\t<hub-nav-toggler\n\t\t\t[isOpen]=\"isMobileOpen()\"\n\t\t\t(toggle)=\"onMobileToggle()\"\n\t\t/>\n\t}\n\n\t<!-- Desktop horizontal navigation: split into two explicit rows -->\n\t@if (!isCollapsed() && resolvedOrientation() === 'horizontal') {\n\t\t<div class=\"hub-nav__horizontal-top-row\">\n\t\t\t@if (startTemplate()) {\n\t\t\t\t<div class=\"hub-nav-slot hub-nav-slot--start\">\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"startTemplate()!\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"startContext()\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t}\n\n\t\t\t<div class=\"hub-nav__horizontal-root-scroll\">\n\t\t\t\t<hub-nav-item-list\n\t\t\t\t\tclass=\"hub-nav__horizontal-root-list\"\n\t\t\t\t\t[items]=\"items()\"\n\t\t\t\t\t[depth]=\"0\"\n\t\t\t\t\t[itemTemplate]=\"resolvedItemTemplate()\"\n\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t\t\t\t/>\n\t\t\t</div>\n\n\t\t\t@if (endTemplate()) {\n\t\t\t\t<div class=\"hub-nav-slot hub-nav-slot--end\">\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"endTemplate()!\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"endContext()\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\n\t\t@if (hasPanels()) {\n\t\t\t<div class=\"hub-nav__horizontal-panel-row\">\n\t\t\t\t<hub-nav-panel-container\n\t\t\t\t\t[panels]=\"state.panelStack()\"\n\t\t\t\t\t[sidebarSide]=\"sidebarSide()\"\n\t\t\t\t\t[panelWidth]=\"panelWidth()\"\n\t\t\t\t\t[hideFirstPanelHeader]=\"true\"\n\t\t\t\t\t[itemTemplate]=\"resolvedItemTemplate()\"\n\t\t\t\t\t(panelClose)=\"onPanelClose($event)\"\n\t\t\t\t\t(panelBack)=\"onPanelBack($event)\"\n\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t}\n\t}\n\n\t<!-- Desktop vertical navigation -->\n\t@if (!isCollapsed() && resolvedOrientation() === 'vertical') {\n\t\t<div class=\"hub-nav__main-and-panels\">\n\t\t\t<div class=\"hub-nav__primary-column\">\n\t\t\t\t<!-- Start slot in vertical desktop mode: constrained to the primary column only -->\n\t\t\t\t@if (startTemplate()) {\n\t\t\t\t\t<div class=\"hub-nav-slot hub-nav-slot--start\">\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"startTemplate()!\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"startContext()\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t\t<hub-nav-item-list\n\t\t\t\t\t[items]=\"items()\"\n\t\t\t\t\t[depth]=\"0\"\n\t\t\t\t\t[itemTemplate]=\"resolvedItemTemplate()\"\n\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t\t\t\t/>\n\n\t\t\t\t<!-- End slot in vertical desktop mode: anchored to the bottom of the primary column -->\n\t\t\t\t@if (endTemplate()) {\n\t\t\t\t\t<div class=\"hub-nav-slot hub-nav-slot--end\">\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"endTemplate()!\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"endContext()\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t</div>\n\n\t\t\t@if (hasPanels()) {\n\t\t\t\t<hub-nav-panel-container\n\t\t\t\t\t[panels]=\"state.panelStack()\"\n\t\t\t\t\t[sidebarSide]=\"sidebarSide()\"\n\t\t\t\t\t[panelWidth]=\"panelWidth()\"\n\t\t\t\t\t[hideFirstPanelHeader]=\"true\"\n\t\t\t\t\t[itemTemplate]=\"resolvedItemTemplate()\"\n\t\t\t\t\t(panelClose)=\"onPanelClose($event)\"\n\t\t\t\t\t(panelBack)=\"onPanelBack($event)\"\n\t\t\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t\t\t\t(panelOpen)=\"onPanelOpen($event)\"\n\t\t\t\t/>\n\t\t\t}\n\t\t</div>\n\t}\n\n\t<!-- Mobile panel: shown when collapsed and open -->\n\t@if (isCollapsed()) {\n\t\t<hub-nav-mobile-panel\n\t\t\t[items]=\"items()\"\n\t\t\t[isOpen]=\"isMobileOpen()\"\n\t\t\t[mode]=\"collapseMode()\"\n\t\t\t[offcanvasPosition]=\"offcanvasPosition()\"\n\t\t\t(closePanel)=\"onMobilePanelClose()\"\n\t\t\t(itemClick)=\"onItemClick($event)\"\n\t\t\t(dropdownToggle)=\"onDropdownToggle($event)\"\n\t\t/>\n\t}\n</nav>\n", styles: [":root,:host{--hub-nav-height: 3.5rem;--hub-nav-padding-x: var(--hub-ref-space-3, 1rem);--hub-nav-padding-y: var(--hub-ref-space-2, .5rem);--hub-nav-gap: var(--hub-ref-space-1, .25rem);--hub-nav-horizontal-padding-y: 0;--hub-nav-horizontal-padding-x: 0;--hub-nav-horizontal-row-gap: 0;--hub-nav-horizontal-items-justify: center;--hub-nav-horizontal-items-overflow-x: auto;--hub-nav-horizontal-items-overflow-y: hidden;--hub-nav-vertical-items-overflow-y: auto;--hub-nav-vertical-items-overflow-x: hidden;--hub-nav-vertical-panel-padding-inline: var(--hub-ref-space-2, .5rem);--hub-nav-vertical-panel-padding-block: 0;--hub-nav-horizontal-panel-padding-y: var(--hub-ref-space-2, .5rem);--hub-nav-horizontal-panel-padding-x: 0;--hub-nav-scrollbar-width: none;--hub-nav-scrollbar-color: transparent transparent;--hub-nav-scrollbar-webkit-size: 0;--hub-nav-scrollbar-thumb-color: transparent;--hub-nav-scrollbar-track-color: transparent;--hub-nav-scrollbar-thumb-radius: var(--hub-ref-radius-pill, 999px);--hub-nav-bg: var(--hub-sys-surface-page, #fff);--hub-nav-color: var(--hub-sys-text-primary, #212529);--hub-nav-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-border-width: 1px;--hub-nav-border-style: solid;--hub-nav-brand-font-size: var(--hub-ref-font-size-lg, 1.25rem);--hub-nav-brand-font-weight: var(--hub-ref-font-weight-semibold, 600);--hub-nav-brand-color: var(--hub-sys-text-primary, #212529);--hub-nav-brand-padding-x: var(--hub-ref-space-2, .5rem);--hub-nav-item-padding-x: var(--hub-ref-space-3, 1rem);--hub-nav-item-padding-y: var(--hub-ref-space-2, .5rem);--hub-nav-item-color: var(--hub-sys-text-primary, #212529);--hub-nav-item-font-size: var(--hub-ref-font-size-base, 1rem);--hub-nav-item-font-weight: var(--hub-ref-font-weight-normal, 400);--hub-nav-item-border-radius: var(--hub-ref-border-radius-sm, .25rem);--hub-nav-item-transition: var(--hub-sys-transition-fast, .15s ease);--hub-nav-item-hover-bg: var(--hub-sys-state-hover-overlay, rgba(0, 0, 0, .04));--hub-nav-item-hover-color: var(--hub-sys-color-primary, #0d6efd);--hub-nav-item-active-bg: var(--hub-sys-color-primary, #0d6efd);--hub-nav-item-active-color: #fff;--hub-nav-item-active-font-weight: var(--hub-ref-font-weight-semibold, 600);--hub-nav-item-disabled-color: var(--hub-sys-text-muted, #6c757d);--hub-nav-item-disabled-opacity: var(--hub-sys-opacity-disabled, .65);--hub-nav-dropdown-bg: var(--hub-sys-surface-elevated, #fff);--hub-nav-dropdown-color: var(--hub-sys-text-primary, #212529);--hub-nav-dropdown-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-dropdown-border-radius: var(--hub-nav-item-border-radius, var(--hub-ref-border-radius-sm, .25rem));--hub-nav-dropdown-shadow: var(--hub-sys-shadow-md, 0 .5rem 1rem rgba(0, 0, 0, .15));--hub-nav-dropdown-padding-y: var(--hub-ref-space-2, .5rem);--hub-nav-dropdown-min-width: 12rem;--hub-nav-dropdown-z-index: var(--hub-sys-z-index-dropdown, 1000);--hub-nav-caret-size: .3rem;--hub-nav-caret-color: currentColor;--hub-nav-header-font-size: var(--hub-ref-font-size-sm, .875rem);--hub-nav-header-font-weight: var(--hub-ref-font-weight-semibold, 600);--hub-nav-header-color: var(--hub-sys-text-muted, #6c757d);--hub-nav-header-padding-x: var(--hub-ref-space-3, 1rem);--hub-nav-header-padding-y: var(--hub-ref-space-1, .25rem);--hub-nav-header-margin-top: var(--hub-ref-space-3, .75rem);--hub-nav-header-margin-inline-start: var(--hub-ref-space-3, .75rem);--hub-nav-header-horizontal-padding-inline-start: 2rem;--hub-nav-separator-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-separator-margin-y: var(--hub-ref-space-2, .5rem);--hub-nav-badge-font-size: var(--hub-ref-font-size-xs, .75rem);--hub-nav-badge-padding-x: .5em;--hub-nav-badge-padding-y: .25em;--hub-nav-badge-bg: var(--hub-sys-color-danger, #dc3545);--hub-nav-badge-color: #fff;--hub-nav-badge-border-radius: var(--hub-ref-border-radius-pill, 50rem);--hub-nav-toggler-padding-x: var(--hub-ref-space-2, .5rem);--hub-nav-toggler-padding-y: var(--hub-ref-space-1, .25rem);--hub-nav-toggler-font-size: var(--hub-ref-font-size-lg, 1.25rem);--hub-nav-toggler-color: var(--hub-sys-text-primary, #212529);--hub-nav-toggler-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-toggler-border-radius: var(--hub-ref-border-radius-sm, .25rem);--hub-nav-mobile-bg: var(--hub-sys-surface-page, #fff);--hub-nav-mobile-color: var(--hub-sys-text-primary, #212529);--hub-nav-mobile-width: 18rem;--hub-nav-mobile-z-index: var(--hub-sys-z-index-fixed, 1030);--hub-nav-mobile-backdrop-bg: rgba(0, 0, 0, .5);--hub-nav-mobile-transition: var(--hub-sys-transition-base, .3s ease);--hub-nav-accordion-indent: var(--hub-ref-space-3, 1rem);--hub-nav-accordion-transition: var(--hub-sys-transition-collapse, .35s ease);--hub-nav-icon-size: var(--hub-ref-icon-size, 1.25rem);--hub-nav-icon-gap: var(--hub-ref-space-2, .5rem);--hub-nav-panel-width: 16rem;--hub-nav-panel-bg: var(--hub-sys-surface-elevated, #fff);--hub-nav-panel-color: var(--hub-sys-text-primary, #212529);--hub-nav-panel-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-panel-border-width: 1px;--hub-nav-panel-shadow: var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075));--hub-nav-panel-z-index: var(--hub-sys-z-index-dropdown, 1000);--hub-nav-panel-transition: transform var(--hub-sys-transition-base, .3s ease);--hub-nav-panel-header-height: 3rem;--hub-nav-panel-header-bg: var(--hub-sys-surface-page, #fff);--hub-nav-panel-header-border-color: var(--hub-sys-border-color-default, #dee2e6);--hub-nav-panel-header-font-size: var(--hub-ref-font-size-sm, .875rem);--hub-nav-panel-header-font-weight: var(--hub-ref-font-weight-semibold, 600);--hub-nav-panel-header-padding-x: var(--hub-ref-space-3, 1rem);--hub-nav-panel-back-color: var(--hub-sys-text-primary, #212529);--hub-nav-panel-back-hover-bg: var(--hub-sys-state-hover-overlay, rgba(0, 0, 0, .04));--hub-nav-panel-back-size: 2rem}:host{display:block}nav{display:flex;background-color:var(--hub-nav-bg, #fff);color:var(--hub-nav-color, #212529);border:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-border-color, #dee2e6);font-family:inherit}:host(.hub-nav--horizontal) nav{width:100%}:host(.hub-nav--horizontal) nav{display:flex;flex-direction:column;align-items:stretch;padding:var(--hub-nav-horizontal-padding-y, 0) var(--hub-nav-horizontal-padding-x, 0);gap:var(--hub-nav-horizontal-row-gap, 0)}:host(.hub-nav--vertical) nav{flex:1;min-height:0;flex-direction:column;padding:var(--hub-nav-padding-y, .5rem) var(--hub-nav-padding-x, 1rem);gap:var(--hub-nav-gap, .25rem)}:host(.hub-nav--vertical) nav>hub-nav-item-list{display:flex;flex-direction:column;gap:var(--hub-nav-gap, .25rem)}:host(.hub-nav--collapsed) nav,:host(.hub-nav--collapsed.hub-nav--vertical) nav,:host(.hub-nav--collapsed.hub-nav--horizontal) nav{display:flex;flex:0 0 auto;width:100%;min-height:auto;flex-direction:row;align-items:center;flex-wrap:nowrap;gap:var(--hub-nav-gap, .25rem);padding:var(--hub-nav-padding-y, .5rem) var(--hub-nav-padding-x, 1rem);overflow:hidden}.hub-nav__main-and-panels{display:flex;flex:1;min-height:0;align-items:stretch}.hub-nav__main-and-panels>hub-nav-panel-container{flex:1;min-height:0}.hub-nav__primary-column{display:flex;flex-direction:column;min-height:0}:host(.hub-nav--sidebar-left) .hub-nav__main-and-panels{flex-direction:row}:host(.hub-nav--sidebar-right) .hub-nav__main-and-panels{flex-direction:row-reverse}:host(.hub-nav--vertical) .hub-nav__main-and-panels>.hub-nav__primary-column{display:flex;flex-direction:column;flex:0 0 var(--hub-nav-panel-width, 16rem);width:var(--hub-nav-panel-width, 16rem);min-width:var(--hub-nav-panel-width, 16rem)}:host(.hub-nav--vertical) .hub-nav__main-and-panels>.hub-nav__primary-column>hub-nav-item-list{display:flex;flex-direction:column;gap:var(--hub-nav-gap, .25rem);flex:1 1 auto;min-height:0;padding-inline:var(--hub-nav-vertical-panel-padding-inline, .5rem);padding-block:var(--hub-nav-vertical-panel-padding-block, 0);overflow-y:var(--hub-nav-vertical-items-overflow-y, auto);overflow-x:var(--hub-nav-vertical-items-overflow-x, hidden)}:host(.hub-nav--vertical) .hub-nav__main-and-panels>hub-nav-panel-container{flex:0 0 auto;width:auto;min-width:0}:host(.hub-nav--horizontal) .hub-nav__horizontal-top-row{display:flex;align-items:center;gap:var(--hub-nav-gap, .25rem)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-list{display:flex;flex-direction:row;align-items:center;justify-content:var(--hub-nav-horizontal-items-justify, center);gap:var(--hub-nav-gap, .25rem);flex:0 0 auto;width:max-content;min-width:100%;margin-inline:auto;overflow:visible}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll{display:flex;align-items:center;justify-content:flex-start;flex:1 1 auto;min-width:0;padding:var(--hub-nav-horizontal-panel-padding-y, .5rem) var(--hub-nav-horizontal-panel-padding-x, 0);overflow-x:var(--hub-nav-horizontal-items-overflow-x, auto);overflow-y:var(--hub-nav-horizontal-items-overflow-y, hidden)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll,:host(.hub-nav--vertical) .hub-nav__primary-column>hub-nav-item-list{-ms-overflow-style:none;scrollbar-width:var(--hub-nav-scrollbar-width, none);scrollbar-color:var(--hub-nav-scrollbar-color, transparent transparent)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll::-webkit-scrollbar,:host(.hub-nav--vertical) .hub-nav__primary-column>hub-nav-item-list::-webkit-scrollbar{width:var(--hub-nav-scrollbar-webkit-size, 0);height:var(--hub-nav-scrollbar-webkit-size, 0)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll::-webkit-scrollbar-thumb,:host(.hub-nav--vertical) .hub-nav__primary-column>hub-nav-item-list::-webkit-scrollbar-thumb{background-color:var(--hub-nav-scrollbar-thumb-color, transparent);border-radius:var(--hub-nav-scrollbar-thumb-radius, 999px)}:host(.hub-nav--horizontal) .hub-nav__horizontal-root-scroll::-webkit-scrollbar-track,:host(.hub-nav--vertical) .hub-nav__primary-column>hub-nav-item-list::-webkit-scrollbar-track{background-color:var(--hub-nav-scrollbar-track-color, transparent)}:host(.hub-nav--horizontal) .hub-nav__horizontal-panel-row{width:100%;min-width:0}:host(.hub-nav--horizontal) .hub-nav__horizontal-panel-row>hub-nav-panel-container{width:100%;min-width:0}.hub-nav-slot{display:flex;align-items:center;justify-content:flex-start;padding:0 var(--hub-nav-brand-padding-x, .5rem);font-size:var(--hub-nav-brand-font-size, 1.25rem);font-weight:var(--hub-nav-brand-font-weight, 600);color:var(--hub-nav-brand-color, #212529);text-decoration:none;white-space:nowrap;flex-shrink:0}:host(.hub-nav--vertical) .hub-nav-slot--start{width:100%;align-self:stretch;box-sizing:border-box;padding-bottom:var(--hub-nav-padding-y, .5rem);border-bottom:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-border-color, #dee2e6);margin-bottom:var(--hub-nav-gap, .25rem)}:host(.hub-nav--vertical) .hub-nav-slot--end{width:100%;align-self:stretch;box-sizing:border-box;margin-top:auto;padding-top:var(--hub-nav-padding-y, .5rem);border-top:var(--hub-nav-border-width, 1px) var(--hub-nav-border-style, solid) var(--hub-nav-border-color, #dee2e6)}:host(.hub-nav--horizontal) .hub-nav-slot--end{margin-inline-start:auto;justify-content:flex-end}:host(.hub-nav--horizontal) .hub-nav-slot--start,:host(.hub-nav--horizontal) .hub-nav-slot--end{align-self:center;height:auto;min-height:0;max-height:none}:host(.hub-nav--collapsed.hub-nav--vertical) .hub-nav-slot--start,:host(.hub-nav--collapsed.hub-nav--vertical) .hub-nav-slot--end,:host(.hub-nav--collapsed.hub-nav--horizontal) .hub-nav-slot--start,:host(.hub-nav--collapsed.hub-nav--horizontal) .hub-nav-slot--end{width:0;align-self:stretch;display:flex;align-items:center;flex:1 1 0;min-width:0;border:0;margin:0;padding:0 var(--hub-nav-brand-padding-x, .5rem)}:host(.hub-nav--collapsed) .hub-nav-slot--start{justify-content:flex-start}:host(.hub-nav--collapsed) .hub-nav-slot--end{margin-inline-start:0;justify-content:flex-end}:host(.hub-nav--collapsed) hub-nav-toggler{flex:0 0 auto;display:flex;align-items:center;padding-inline:var(--hub-nav-brand-padding-x, .5rem)}:host(.hub-nav--sticky){position:sticky;top:var(--hub-nav-sticky-top, 0px);align-self:flex-start;max-height:calc(100vh - var(--hub-nav-sticky-top, 0px));z-index:var(--hub-sys-z-index-sticky, 1020)}:host(.hub-nav--fixed){position:fixed;top:0;left:0;right:0;z-index:var(--hub-sys-z-index-fixed, 1030)}\n"] }]
|
|
2202
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], navClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "navClass", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], autoOpenFromRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoOpenFromRoute", required: false }] }], startDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => HubNavStartDirective), { isSignal: true }] }], endDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => HubNavEndDirective), { isSignal: true }] }], itemTemplateDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => HubNavItemTemplateDirective), { isSignal: true }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], dropdownOpen: [{ type: i0.Output, args: ["dropdownOpen"] }], dropdownClose: [{ type: i0.Output, args: ["dropdownClose"] }], mobileToggle: [{ type: i0.Output, args: ["mobileToggle"] }], panelChange: [{ type: i0.Output, args: ["panelChange"] }] } });
|
|
2203
|
+
|
|
2204
|
+
/*
|
|
2205
|
+
* Public API Surface of nav
|
|
2206
|
+
*/
|
|
2207
|
+
// Models
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* Generated bundle index. Do not edit.
|
|
2211
|
+
*/
|
|
2212
|
+
|
|
2213
|
+
export { HUB_NAV_CONFIG, HubNavComponent, HubNavConfigService, HubNavEndDirective, HubNavItemComponent, HubNavItemListComponent, HubNavItemTemplateDirective, HubNavPanelComponent, HubNavPanelContainerComponent, HubNavScrollSpyDirective, HubNavScrollSpySectionDirective, HubNavSeparatorComponent, HubNavStartDirective, HubNavStateService };
|
|
2214
|
+
//# sourceMappingURL=ng-hub-ui-nav.mjs.map
|