@danjelp/ngx-app-shell 1.0.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.
@@ -0,0 +1,1944 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, PLATFORM_ID, signal, computed, linkedSignal, Injectable, NgZone, effect, input, ElementRef, Directive, numberAttribute, TemplateRef, DestroyRef, isDevMode, untracked, ChangeDetectionStrategy, Component, isSignal, viewChild, booleanAttribute, provideEnvironmentInitializer } from '@angular/core';
3
+ import { isPlatformBrowser, NgTemplateOutlet, DOCUMENT } from '@angular/common';
4
+ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
5
+ import { Router, NavigationEnd, ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router';
6
+ import { filter, map } from 'rxjs';
7
+
8
+ /** Ordered size buckets. Order matters: `sizeRank()` relies on the index. */
9
+ const SHELL_SIZES = ['xs', 'sm', 'md', 'lg', 'xl'];
10
+
11
+ /**
12
+ * Container-first breakpoints. Deliberately *not* device widths: they describe
13
+ * the space a region has, which is what layout decisions should react to.
14
+ */
15
+ const DEFAULT_BREAKPOINTS = {
16
+ xs: 0,
17
+ sm: 520,
18
+ md: 768,
19
+ lg: 1080,
20
+ xl: 1440,
21
+ };
22
+ function sizeRank(size) {
23
+ return SHELL_SIZES.indexOf(size);
24
+ }
25
+ /** Largest bucket whose minimum width is still <= `width`. */
26
+ function resolveSize(width, breakpoints) {
27
+ let match = SHELL_SIZES[0];
28
+ for (const size of SHELL_SIZES) {
29
+ if (width >= breakpoints[size]) {
30
+ match = size;
31
+ }
32
+ }
33
+ return match;
34
+ }
35
+ /** `atLeast('md', 'sm')` → true */
36
+ function atLeast(size, min) {
37
+ return sizeRank(size) >= sizeRank(min);
38
+ }
39
+ /** `atMost('md', 'lg')` → true */
40
+ function atMost(size, max) {
41
+ return sizeRank(size) <= sizeRank(max);
42
+ }
43
+ /** Strictly narrower than `bound` — used for "collapse below md" style rules. */
44
+ function isBelow(size, bound) {
45
+ return sizeRank(size) < sizeRank(bound);
46
+ }
47
+
48
+ /**
49
+ * Building blocks for `SidebarBehavior.resolve`. Each returns a complete,
50
+ * internally consistent state, so a custom behaviour only decides *which* mode
51
+ * applies — never how wide the track is or whether a scrim is needed.
52
+ */
53
+ /** Off-canvas modal drawer. Reserves no grid track. */
54
+ function drawerState(open, sidebar) {
55
+ return {
56
+ mode: 'overlay',
57
+ open,
58
+ modal: open,
59
+ labels: true,
60
+ peeked: false,
61
+ trackWidth: '0px',
62
+ panelWidth: sidebar.overlayWidth,
63
+ };
64
+ }
65
+ /** Icon rail. While peeking the panel floats wider but the track stays put. */
66
+ function railState(peeking, sidebar) {
67
+ const peeked = peeking && sidebar.peek;
68
+ return {
69
+ mode: 'collapsed',
70
+ open: true,
71
+ modal: false,
72
+ labels: peeked,
73
+ peeked,
74
+ trackWidth: sidebar.collapsedWidth,
75
+ panelWidth: peeked ? sidebar.width : sidebar.collapsedWidth,
76
+ };
77
+ }
78
+ /** Full in-flow panel with labels. */
79
+ function panelState(sidebar) {
80
+ return {
81
+ mode: 'expanded',
82
+ open: true,
83
+ modal: false,
84
+ labels: true,
85
+ peeked: false,
86
+ trackWidth: sidebar.width,
87
+ panelWidth: sidebar.width,
88
+ };
89
+ }
90
+ const HIDDEN_STATE = {
91
+ mode: 'hidden',
92
+ open: false,
93
+ modal: false,
94
+ labels: false,
95
+ peeked: false,
96
+ trackWidth: '0px',
97
+ panelWidth: '0px',
98
+ };
99
+ /** Not rendered, zero-width track. */
100
+ function hiddenState() {
101
+ return HIDDEN_STATE;
102
+ }
103
+
104
+ /**
105
+ * Default policy.
106
+ *
107
+ * size < overlayBelow → modal drawer, closed until opened
108
+ * size < collapseBelow → icon rail, widened while peeking
109
+ * otherwise → full panel
110
+ *
111
+ * An explicit intent (`expanded`, `collapsed`, `hidden`) wins over the
112
+ * automatic choice, except in drawer territory where there is no room for a
113
+ * persistent panel. Set the intent back to `auto` to follow breakpoints again.
114
+ */
115
+ const responsiveSidebarBehavior = {
116
+ resolve({ size, intent, drawerOpen, peeking, config }) {
117
+ const sidebar = config.sidebar;
118
+ if (isBelow(size, sidebar.overlayBelow)) {
119
+ return drawerState(drawerOpen, sidebar);
120
+ }
121
+ if (intent === 'hidden') {
122
+ return hiddenState();
123
+ }
124
+ const automatic = isBelow(size, sidebar.collapseBelow) ? 'collapsed' : 'expanded';
125
+ const resolved = intent === 'auto' ? automatic : intent;
126
+ return resolved === 'collapsed' ? railState(peeking, sidebar) : panelState(sidebar);
127
+ },
128
+ };
129
+
130
+ const SIDEBAR_BEHAVIOR = new InjectionToken('SIDEBAR_BEHAVIOR', {
131
+ providedIn: 'root',
132
+ factory: () => responsiveSidebarBehavior,
133
+ });
134
+
135
+ const DEFAULT_SHELL_CONFIG = {
136
+ layout: 'topbar-full',
137
+ scroll: 'main',
138
+ scrollTopOnNavigate: true,
139
+ initialSize: 'lg',
140
+ breakpoints: DEFAULT_BREAKPOINTS,
141
+ sidebar: {
142
+ width: 'var(--shell-sidebar-width, 264px)',
143
+ collapsedWidth: 'var(--shell-sidebar-collapsed-width, 68px)',
144
+ overlayWidth: 'var(--shell-sidebar-overlay-width, min(88vw, 320px))',
145
+ intent: 'auto',
146
+ collapseBelow: 'lg',
147
+ overlayBelow: 'md',
148
+ peek: true,
149
+ peekDelay: 180,
150
+ closeOnNavigate: true,
151
+ resizable: false,
152
+ minWidth: 200,
153
+ maxWidth: 420,
154
+ toggleShortcut: null,
155
+ persist: true,
156
+ storageKey: 'app-shell.sidebar',
157
+ },
158
+ topbar: { enabled: true, sidebarToggle: 'always' },
159
+ footer: { enabled: false },
160
+ appearance: {
161
+ topbar: 'solid',
162
+ sidebar: 'solid',
163
+ footer: 'solid',
164
+ navIndicator: 'pill',
165
+ subheader: 'solid',
166
+ },
167
+ motion: {
168
+ enabled: true,
169
+ speed: 1,
170
+ drawer: 'slide',
171
+ overflowMenu: 'fade',
172
+ scrim: 'fade',
173
+ collapse: true,
174
+ navExpand: true,
175
+ },
176
+ subheader: {
177
+ enabled: 'auto',
178
+ sticky: true,
179
+ overflowSlots: ['subheader-end'],
180
+ },
181
+ breadcrumbs: {
182
+ includeHome: false,
183
+ homeLabel: 'Home',
184
+ homeUrl: '/',
185
+ includeCurrent: true,
186
+ maxItems: 4,
187
+ useRouteTitle: true,
188
+ compactBelow: 480,
189
+ backBelow: 280,
190
+ skeletonDelay: 400,
191
+ separator: 'chevron',
192
+ },
193
+ overflowSlots: ['primary-nav', 'search', 'actions'],
194
+ labels: {
195
+ skipToContent: 'Skip to main content',
196
+ openNavigation: 'Open navigation',
197
+ closeNavigation: 'Close navigation',
198
+ expandSidebar: 'Expand sidebar',
199
+ collapseSidebar: 'Collapse sidebar',
200
+ resizeSidebar: 'Resize sidebar',
201
+ moreActions: 'More',
202
+ primaryNavigation: 'Primary',
203
+ breadcrumb: 'Breadcrumb',
204
+ showPath: 'Show full path',
205
+ back: 'Back to',
206
+ loading: 'Loading',
207
+ },
208
+ };
209
+ /** Configuration as provided by the app (`provideAppShell`). */
210
+ const SHELL_CONFIG = new InjectionToken('SHELL_CONFIG', {
211
+ providedIn: 'root',
212
+ factory: () => DEFAULT_SHELL_CONFIG,
213
+ });
214
+ /** Merge `input` over `base` (the defaults unless given). Never mutates. */
215
+ function mergeShellConfig(input = {}, base = DEFAULT_SHELL_CONFIG) {
216
+ return {
217
+ ...base,
218
+ ...input,
219
+ breakpoints: { ...base.breakpoints, ...input.breakpoints },
220
+ sidebar: { ...base.sidebar, ...input.sidebar },
221
+ topbar: { ...base.topbar, ...input.topbar },
222
+ footer: { ...base.footer, ...input.footer },
223
+ appearance: { ...base.appearance, ...input.appearance },
224
+ motion: { ...base.motion, ...input.motion },
225
+ subheader: { ...base.subheader, ...input.subheader },
226
+ breadcrumbs: { ...base.breadcrumbs, ...input.breadcrumbs },
227
+ labels: { ...base.labels, ...input.labels },
228
+ };
229
+ }
230
+ /**
231
+ * Register the shell once, in `app.config.ts`:
232
+ *
233
+ * ```ts
234
+ * provideAppShell({ layout: 'sidebar-full', sidebar: { collapseBelow: 'xl' } })
235
+ * ```
236
+ *
237
+ * Pass `behavior` to replace the collapse policy wholesale. Both can also be
238
+ * changed at runtime through `ShellStore.configure()` / `setBehavior()`.
239
+ */
240
+ function provideAppShell(config = {}, options = {}) {
241
+ const providers = [
242
+ { provide: SHELL_CONFIG, useValue: mergeShellConfig(config) },
243
+ ];
244
+ if (options.behavior) {
245
+ providers.push({ provide: SIDEBAR_BEHAVIOR, useValue: options.behavior });
246
+ }
247
+ return makeEnvironmentProviders(providers);
248
+ }
249
+
250
+ const NOOP_STORAGE = {
251
+ read: () => null,
252
+ write: () => undefined,
253
+ };
254
+ function localShellStateStorage() {
255
+ if (!isPlatformBrowser(inject(PLATFORM_ID))) {
256
+ return NOOP_STORAGE;
257
+ }
258
+ return {
259
+ read(key) {
260
+ try {
261
+ return localStorage.getItem(key);
262
+ }
263
+ catch {
264
+ // Private mode / disabled storage: degrade to "no preference".
265
+ return null;
266
+ }
267
+ },
268
+ write(key, value) {
269
+ try {
270
+ localStorage.setItem(key, value);
271
+ }
272
+ catch {
273
+ /* ignore */
274
+ }
275
+ },
276
+ };
277
+ }
278
+ const SHELL_STATE_STORAGE = new InjectionToken('SHELL_STATE_STORAGE', {
279
+ providedIn: 'root',
280
+ factory: localShellStateStorage,
281
+ });
282
+
283
+ /**
284
+ * Stable element ids. The shell assumes one instance per application, which
285
+ * lets `aria-controls` work from a toggle rendered anywhere in the tree.
286
+ */
287
+ const SHELL_SIDEBAR_ID = 'shell-sidebar';
288
+ const SHELL_MAIN_ID = 'shell-main';
289
+
290
+ const INTENTS = ['auto', 'expanded', 'collapsed', 'hidden'];
291
+ /**
292
+ * Single source of truth for the shell.
293
+ *
294
+ * Root-provided on purpose: any component in the app — including lazily loaded
295
+ * pages and content projected into `<app-shell>` — can inject it and drive the
296
+ * sidebar without input/output plumbing. One shell per application.
297
+ *
298
+ * Everything is derived from four inputs: the measured shell width, the user's
299
+ * intent, the transient drawer/peek flags, and the (runtime-changeable)
300
+ * config + behaviour.
301
+ */
302
+ class ShellStore {
303
+ storage = inject(SHELL_STATE_STORAGE);
304
+ baseConfig = signal(inject(SHELL_CONFIG), ...(ngDevMode ? [{ debugName: "baseConfig" }] : /* istanbul ignore next */ []));
305
+ behaviorState = signal(inject(SIDEBAR_BEHAVIOR), ...(ngDevMode ? [{ debugName: "behaviorState" }] : /* istanbul ignore next */ []));
306
+ /** Measured shell width; `null` until the first ResizeObserver callback. */
307
+ width = signal(null, ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
308
+ intent = signal(this.restoreIntent(), ...(ngDevMode ? [{ debugName: "intent" }] : /* istanbul ignore next */ []));
309
+ /** User-dragged panel width in px; `null` = use the configured width. */
310
+ customWidth = signal(this.restoreWidth(), ...(ngDevMode ? [{ debugName: "customWidth" }] : /* istanbul ignore next */ []));
311
+ peekRequested = signal(false, ...(ngDevMode ? [{ debugName: "peekRequested" }] : /* istanbul ignore next */ []));
312
+ resizingState = signal(false, ...(ngDevMode ? [{ debugName: "resizingState" }] : /* istanbul ignore next */ []));
313
+ peekTimer;
314
+ /** Size bucket of the shell container — not of the viewport. */
315
+ size = computed(() => {
316
+ const width = this.width();
317
+ const config = this.baseConfig();
318
+ return width === null ? config.initialSize : resolveSize(width, config.breakpoints);
319
+ }, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
320
+ /**
321
+ * Drawer open state. Transient by design: it is never persisted, and any
322
+ * change of size bucket closes it, so rotating a phone or resizing a window
323
+ * never leaves a modal drawer stranded open.
324
+ */
325
+ drawerOpen = linkedSignal({ ...(ngDevMode ? { debugName: "drawerOpen" } : /* istanbul ignore next */ {}), source: this.size,
326
+ computation: () => false });
327
+ /** Effective configuration: app config, runtime patches and the user's width. */
328
+ config = computed(() => {
329
+ const config = this.baseConfig();
330
+ const width = this.customWidth();
331
+ if (width === null || !config.sidebar.resizable) {
332
+ return config;
333
+ }
334
+ return { ...config, sidebar: { ...config.sidebar, width: `${width}px` } };
335
+ }, ...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
336
+ behavior = this.behaviorState.asReadonly();
337
+ shellWidth = this.width.asReadonly();
338
+ sidebarIntent = this.intent.asReadonly();
339
+ sidebarWidth = this.customWidth.asReadonly();
340
+ resizing = this.resizingState.asReadonly();
341
+ sidebar = computed(() => this.behaviorState().resolve(this.context()), ...(ngDevMode ? [{ debugName: "sidebar" }] : /* istanbul ignore next */ []));
342
+ // ── Configuration ─────────────────────────────────────────────────────────
343
+ /** Patch the configuration at runtime, e.g. from a settings screen. */
344
+ configure(patch) {
345
+ this.baseConfig.update((current) => mergeShellConfig(patch, current));
346
+ }
347
+ /** Swap the collapse policy at runtime. */
348
+ setBehavior(behavior) {
349
+ this.behaviorState.set(behavior);
350
+ }
351
+ /** Called by `<app-shell>` with its measured width. */
352
+ setWidth(width) {
353
+ this.width.set(width);
354
+ }
355
+ // ── Sidebar commands ──────────────────────────────────────────────────────
356
+ setSidebarIntent(intent) {
357
+ this.intent.set(intent);
358
+ this.persistIntent(intent);
359
+ }
360
+ /**
361
+ * Overlay drawers toggle open/closed; persistent panels ask the behaviour
362
+ * for the next intent (by default: expanded ⇄ collapsed).
363
+ */
364
+ toggleSidebar() {
365
+ const state = this.sidebar();
366
+ if (state.mode === 'overlay') {
367
+ this.drawerOpen.set(!state.open);
368
+ return;
369
+ }
370
+ const context = this.context();
371
+ const next = this.behaviorState().nextIntent?.(context) ??
372
+ (state.mode === 'expanded' ? 'collapsed' : 'expanded');
373
+ this.setSidebarIntent(next);
374
+ }
375
+ openSidebar() {
376
+ if (this.sidebar().mode === 'overlay') {
377
+ this.drawerOpen.set(true);
378
+ }
379
+ else {
380
+ this.setSidebarIntent('expanded');
381
+ }
382
+ }
383
+ closeSidebar() {
384
+ if (this.sidebar().mode === 'overlay') {
385
+ this.drawerOpen.set(false);
386
+ }
387
+ else {
388
+ this.setSidebarIntent('collapsed');
389
+ }
390
+ }
391
+ /**
392
+ * Set the expanded panel width in px (clamped to `minWidth`/`maxWidth`), or
393
+ * `null` to return to the configured width. Pass `{ persist: false }` for
394
+ * intermediate values, e.g. while a drag is still in progress.
395
+ */
396
+ setSidebarWidth(width, options = {}) {
397
+ const { minWidth, maxWidth } = this.baseConfig().sidebar;
398
+ const next = width === null ? null : Math.round(Math.min(maxWidth, Math.max(minWidth, width)));
399
+ this.customWidth.set(next);
400
+ if (options.persist ?? true) {
401
+ this.write('width', next === null ? '' : String(next));
402
+ }
403
+ }
404
+ /** Suspends layout transitions while a resize drag is in progress. */
405
+ setResizing(resizing) {
406
+ this.resizingState.set(resizing);
407
+ }
408
+ /** Hover/focus peek on the collapsed rail, debounced by `sidebar.peekDelay`. */
409
+ requestPeek(peeking) {
410
+ clearTimeout(this.peekTimer);
411
+ if (!peeking) {
412
+ this.peekRequested.set(false);
413
+ return;
414
+ }
415
+ const sidebar = this.baseConfig().sidebar;
416
+ if (!sidebar.peek) {
417
+ return;
418
+ }
419
+ this.peekTimer = setTimeout(() => this.peekRequested.set(true), sidebar.peekDelay);
420
+ }
421
+ // ── Internals ─────────────────────────────────────────────────────────────
422
+ context() {
423
+ return {
424
+ width: this.width(),
425
+ size: this.size(),
426
+ intent: this.intent(),
427
+ drawerOpen: this.drawerOpen(),
428
+ peeking: this.peekRequested(),
429
+ config: this.config(),
430
+ };
431
+ }
432
+ persistIntent(intent) {
433
+ const allowed = this.behaviorState().persistIntent?.(intent, this.context()) ?? true;
434
+ if (allowed) {
435
+ this.write('intent', intent);
436
+ }
437
+ }
438
+ write(key, value) {
439
+ const sidebar = this.baseConfig().sidebar;
440
+ if (sidebar.persist) {
441
+ this.storage.write(`${sidebar.storageKey}.${key}`, value);
442
+ }
443
+ }
444
+ read(key) {
445
+ const sidebar = this.baseConfig().sidebar;
446
+ return sidebar.persist ? this.storage.read(`${sidebar.storageKey}.${key}`) : null;
447
+ }
448
+ restoreIntent() {
449
+ const stored = this.read('intent');
450
+ return INTENTS.includes(stored)
451
+ ? stored
452
+ : this.baseConfig().sidebar.intent;
453
+ }
454
+ restoreWidth() {
455
+ const stored = Number.parseInt(this.read('width') ?? '', 10);
456
+ return Number.isFinite(stored) ? stored : null;
457
+ }
458
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
459
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellStore, providedIn: 'root' });
460
+ }
461
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellStore, decorators: [{
462
+ type: Injectable,
463
+ args: [{ providedIn: 'root' }]
464
+ }] });
465
+
466
+ /**
467
+ * The sidebar never collapses on its own. Whatever the user last chose is what
468
+ * they get at every desktop size (`auto` means expanded); only drawer
469
+ * territory overrides it.
470
+ *
471
+ * ```ts
472
+ * provideAppShell({}, { behavior: manualSidebarBehavior });
473
+ * ```
474
+ */
475
+ const manualSidebarBehavior = {
476
+ resolve({ size, intent, drawerOpen, peeking, config }) {
477
+ const sidebar = config.sidebar;
478
+ if (isBelow(size, sidebar.overlayBelow)) {
479
+ return drawerState(drawerOpen, sidebar);
480
+ }
481
+ switch (intent) {
482
+ case 'hidden':
483
+ return hiddenState();
484
+ case 'collapsed':
485
+ return railState(peeking, sidebar);
486
+ default:
487
+ return panelState(sidebar);
488
+ }
489
+ },
490
+ };
491
+
492
+ /**
493
+ * Content-first policy: the sidebar is always an off-canvas drawer, at every
494
+ * size. Suits editors, media players and dashboards that want the full width.
495
+ */
496
+ const drawerSidebarBehavior = {
497
+ resolve({ drawerOpen, config }) {
498
+ return drawerState(drawerOpen, config.sidebar);
499
+ },
500
+ };
501
+
502
+ /**
503
+ * Tracks the inline size of an element as a signal.
504
+ *
505
+ * Accepts a plain `ElementRef` (host element) or a `viewChild` signal, in which
506
+ * case the observer follows the element as it appears, changes or is removed.
507
+ *
508
+ * Must be called from an injection context. SSR safe: with no `ResizeObserver`
509
+ * the signal simply stays `null`, which callers read as "not measured yet" and
510
+ * fall back to `config.initialSize` — so the server renders the desktop layout
511
+ * instead of flashing the mobile one.
512
+ */
513
+ function observeWidth(target) {
514
+ const width = signal(null, ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
515
+ const zone = inject(NgZone);
516
+ const read = typeof target === 'function' ? target : () => target;
517
+ effect((onCleanup) => {
518
+ const element = read()?.nativeElement;
519
+ if (!element || typeof ResizeObserver === 'undefined') {
520
+ return;
521
+ }
522
+ const observer = new ResizeObserver((entries) => {
523
+ const entry = entries[entries.length - 1];
524
+ const box = entry.borderBoxSize?.[0];
525
+ const next = Math.round(box ? box.inlineSize : entry.contentRect.width);
526
+ // Signal equality dedupes; NgZone.run keeps zone-based apps ticking.
527
+ zone.run(() => width.set(next));
528
+ });
529
+ observer.observe(element);
530
+ onCleanup(() => observer.disconnect());
531
+ });
532
+ return width.asReadonly();
533
+ }
534
+
535
+ /**
536
+ * Measure an element and bucket its width with the shell's breakpoints.
537
+ * Before the first measurement (and on the server) the bucket is
538
+ * `config.initialSize`. Call from an injection context.
539
+ */
540
+ function measureRegion(target) {
541
+ const store = inject(ShellStore);
542
+ const width = observeWidth(target);
543
+ const size = computed(() => {
544
+ const measured = width();
545
+ const config = store.config();
546
+ return measured === null ? config.initialSize : resolveSize(measured, config.breakpoints);
547
+ }, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
548
+ return { width, size };
549
+ }
550
+
551
+ const SHELL_REGION = new InjectionToken('SHELL_REGION');
552
+
553
+ /**
554
+ * Turns any element into a measured container.
555
+ *
556
+ * ```html
557
+ * <div shellContainerSize #box="shellContainerSize">
558
+ * @if (box.atLeast('md')) { <app-filters /> }
559
+ * <span>{{ box.size() }}</span>
560
+ * </div>
561
+ * ```
562
+ *
563
+ * Also mirrors the bucket onto the host as `data-shell-size` and the raw width
564
+ * as `--shell-container-width`, and makes the host a CSS query container, so
565
+ * plain CSS can react without a media query:
566
+ *
567
+ * ```scss
568
+ * [data-shell-size='xs'] .toolbar__label { display: none; }
569
+ * ```
570
+ *
571
+ * Provides itself as the enclosing `SHELL_REGION`, so nested slot outlets pick
572
+ * the nearest measured ancestor.
573
+ */
574
+ class ContainerSizeDirective {
575
+ /** Optional name, purely for debugging: `shellContainerSize="toolbar"`. */
576
+ label = input('', { ...(ngDevMode ? { debugName: "label" } : /* istanbul ignore next */ {}), alias: 'shellContainerSize' });
577
+ region = measureRegion(inject(ElementRef));
578
+ regionWidth = this.region.width;
579
+ size = this.region.size;
580
+ regionSize = this.size;
581
+ cssWidth = computed(() => {
582
+ const width = this.regionWidth();
583
+ return width === null ? null : `${width}px`;
584
+ }, ...(ngDevMode ? [{ debugName: "cssWidth" }] : /* istanbul ignore next */ []));
585
+ get regionName() {
586
+ return this.label() || 'container';
587
+ }
588
+ atLeast(min) {
589
+ return atLeast(this.size(), min);
590
+ }
591
+ atMost(max) {
592
+ return atMost(this.size(), max);
593
+ }
594
+ below(bound) {
595
+ return isBelow(this.size(), bound);
596
+ }
597
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ContainerSizeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
598
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.23", type: ContainerSizeDirective, isStandalone: true, selector: "[shellContainerSize]", inputs: { label: { classPropertyName: "label", publicName: "shellContainerSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-shell-size": "size()", "style.container-type": "\"inline-size\"", "style.--shell-container-width": "cssWidth()" } }, providers: [{ provide: SHELL_REGION, useExisting: ContainerSizeDirective }], exportAs: ["shellContainerSize"], ngImport: i0 });
599
+ }
600
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ContainerSizeDirective, decorators: [{
601
+ type: Directive,
602
+ args: [{
603
+ selector: '[shellContainerSize]',
604
+ exportAs: 'shellContainerSize',
605
+ host: {
606
+ '[attr.data-shell-size]': 'size()',
607
+ '[style.container-type]': '"inline-size"',
608
+ '[style.--shell-container-width]': 'cssWidth()',
609
+ },
610
+ providers: [{ provide: SHELL_REGION, useExisting: ContainerSizeDirective }],
611
+ }]
612
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "shellContainerSize", required: false }] }] } });
613
+
614
+ /** Reactive index of every registered slot template, keyed by name. */
615
+ class ShellSlotRegistry {
616
+ slots = signal([], ...(ngDevMode ? [{ debugName: "slots" }] : /* istanbul ignore next */ []));
617
+ byName = new Map();
618
+ register(slot) {
619
+ this.slots.update((current) => [...current, slot]);
620
+ }
621
+ unregister(slot) {
622
+ this.slots.update((current) => current.filter((candidate) => candidate !== slot));
623
+ }
624
+ /** All templates for a slot, ordered. Memoised per name. */
625
+ all(name) {
626
+ let entry = this.byName.get(name);
627
+ if (!entry) {
628
+ entry = computed(() => this.slots()
629
+ .filter((slot) => slot.name() === name)
630
+ .sort((a, b) => a.order() - b.order()));
631
+ this.byName.set(name, entry);
632
+ }
633
+ return entry;
634
+ }
635
+ /** Templates that fit the given region size. */
636
+ visible(name, size) {
637
+ return this.all(name)().filter((slot) => fits(slot, size));
638
+ }
639
+ /** Templates that do not fit but asked to be kept via the overflow menu. */
640
+ overflowing(name, size) {
641
+ return this.all(name)().filter((slot) => !fits(slot, size) && slot.overflow());
642
+ }
643
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSlotRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
644
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSlotRegistry, providedIn: 'root' });
645
+ }
646
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSlotRegistry, decorators: [{
647
+ type: Injectable,
648
+ args: [{ providedIn: 'root' }]
649
+ }] });
650
+ function fits(slot, size) {
651
+ return atLeast(size, slot.minSize()) && atMost(size, slot.maxSize());
652
+ }
653
+
654
+ /**
655
+ * Declares a piece of shell chrome.
656
+ *
657
+ * ```html
658
+ * <ng-template shellSlot="search" slotMinSize="md" slotOverflow>
659
+ * <app-global-search />
660
+ * </ng-template>
661
+ * ```
662
+ *
663
+ * Templates register themselves with a root registry, so a slot can be
664
+ * contributed from anywhere — the app root, a routed page, a feature
665
+ * component — and it does not need to be a DOM child of `<app-shell>`.
666
+ * That is also what allows the same template to be rendered in the topbar on a
667
+ * wide screen and inside the overflow menu on a narrow one.
668
+ */
669
+ class ShellSlotDirective {
670
+ /** Target slot. Unknown names are fine; render them with `<shell-slot-outlet>`. */
671
+ name = input.required({ ...(ngDevMode ? { debugName: "name" } : /* istanbul ignore next */ {}), alias: 'shellSlot' });
672
+ /** Ascending render order within the slot. Contribute several templates freely. */
673
+ order = input(0, { ...(ngDevMode ? { debugName: "order" } : /* istanbul ignore next */ {}), alias: 'slotOrder', transform: numberAttribute });
674
+ /** Render only when the *region* has at least this much room. */
675
+ minSize = input('xs', { ...(ngDevMode ? { debugName: "minSize" } : /* istanbul ignore next */ {}), alias: 'slotMinSize' });
676
+ /** Render only up to this region size — for narrow-only affordances. */
677
+ maxSize = input('xl', { ...(ngDevMode ? { debugName: "maxSize" } : /* istanbul ignore next */ {}), alias: 'slotMaxSize' });
678
+ /**
679
+ * When the region is too narrow, move the content into the overflow menu
680
+ * instead of dropping it. Off by default: dropping is the right answer for
681
+ * decorative chrome, moving is the right answer for actions. Only slots in
682
+ * `config.overflowSlots` have an overflow menu.
683
+ */
684
+ overflow = input(false, { ...(ngDevMode ? { debugName: "overflow" } : /* istanbul ignore next */ {}), alias: 'slotOverflow',
685
+ transform: (value) => value !== false });
686
+ template = inject(TemplateRef);
687
+ registry = inject(ShellSlotRegistry);
688
+ constructor() {
689
+ this.registry.register(this);
690
+ inject(DestroyRef).onDestroy(() => this.registry.unregister(this));
691
+ if (isDevMode()) {
692
+ this.warnOnUnsupportedOverflow();
693
+ }
694
+ }
695
+ /** Type guard for `let-` context inference in the template. */
696
+ static ngTemplateContextGuard(_directive, _context) {
697
+ return true;
698
+ }
699
+ /**
700
+ * `slotOverflow` on a slot the topbar never scans is silently ignored at
701
+ * runtime; say so during development. Inputs are only readable once bound,
702
+ * hence the effect. Config is read untracked so runtime `configure()` calls
703
+ * do not repeat the warning.
704
+ */
705
+ warnOnUnsupportedOverflow() {
706
+ const store = inject(ShellStore);
707
+ effect(() => {
708
+ const name = this.name();
709
+ if (!this.overflow()) {
710
+ return;
711
+ }
712
+ const overflowSlots = untracked(() => {
713
+ const config = store.config();
714
+ return [...config.overflowSlots, ...config.subheader.overflowSlots];
715
+ });
716
+ if (!overflowSlots.includes(name)) {
717
+ console.warn(`[app-shell] slotOverflow on "${name}" has no effect: only slots listed in ` +
718
+ `config.overflowSlots or config.subheader.overflowSlots (${overflowSlots.join(', ')}) ` +
719
+ `have an overflow menu. Add "${name}" to one of them, or render it with your own ` +
720
+ `<shell-slot-outlet select="overflow">.`);
721
+ }
722
+ });
723
+ }
724
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSlotDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
725
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.23", type: ShellSlotDirective, isStandalone: true, selector: "ng-template[shellSlot]", inputs: { name: { classPropertyName: "name", publicName: "shellSlot", isSignal: true, isRequired: true, transformFunction: null }, order: { classPropertyName: "order", publicName: "slotOrder", isSignal: true, isRequired: false, transformFunction: null }, minSize: { classPropertyName: "minSize", publicName: "slotMinSize", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "slotMaxSize", isSignal: true, isRequired: false, transformFunction: null }, overflow: { classPropertyName: "overflow", publicName: "slotOverflow", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
726
+ }
727
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSlotDirective, decorators: [{
728
+ type: Directive,
729
+ args: [{
730
+ selector: 'ng-template[shellSlot]',
731
+ }]
732
+ }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "shellSlot", required: true }] }], order: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotOrder", required: false }] }], minSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotMinSize", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotMaxSize", required: false }] }], overflow: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotOverflow", required: false }] }] } });
733
+
734
+ /**
735
+ * Renders the templates registered for a slot.
736
+ *
737
+ * Size filtering uses the nearest measured region, falling back to the shell
738
+ * itself — which is why a topbar squeezed by a wide sidebar collapses its
739
+ * contents even though the window never changed.
740
+ */
741
+ class ShellSlotOutletComponent {
742
+ name = input.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
743
+ /** `visible` renders what fits; `overflow` renders what did not fit. */
744
+ select = input('visible', ...(ngDevMode ? [{ debugName: "select" }] : /* istanbul ignore next */ []));
745
+ /** Overrides the measured region size. Mostly for tests. */
746
+ size = input(null, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
747
+ registry = inject(ShellSlotRegistry);
748
+ store = inject(ShellStore);
749
+ region = inject(SHELL_REGION, { optional: true });
750
+ effectiveSize = computed(() => this.size() ?? this.region?.regionSize() ?? this.store.size(), ...(ngDevMode ? [{ debugName: "effectiveSize" }] : /* istanbul ignore next */ []));
751
+ slots = computed(() => this.select() === 'overflow'
752
+ ? this.registry.overflowing(this.name(), this.effectiveSize())
753
+ : this.registry.visible(this.name(), this.effectiveSize()), ...(ngDevMode ? [{ debugName: "slots" }] : /* istanbul ignore next */ []));
754
+ context = computed(() => {
755
+ const sidebar = this.store.sidebar();
756
+ return {
757
+ $implicit: sidebar,
758
+ sidebar,
759
+ size: this.store.size(),
760
+ region: this.effectiveSize(),
761
+ overflow: this.select() === 'overflow',
762
+ shell: this.store,
763
+ };
764
+ }, ...(ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []));
765
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSlotOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
766
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: ShellSlotOutletComponent, isStandalone: true, selector: "shell-slot-outlet", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, select: { classPropertyName: "select", publicName: "select", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-slot": "name()", "hidden": "!slots().length" } }, ngImport: i0, template: `
767
+ @for (slot of slots(); track slot) {
768
+ <ng-container *ngTemplateOutlet="slot.template; context: context()" />
769
+ }
770
+ `, isInline: true, styles: [":host{display:flex;align-items:center;min-inline-size:0}:host([hidden]){display:none}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
771
+ }
772
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSlotOutletComponent, decorators: [{
773
+ type: Component,
774
+ args: [{ selector: 'shell-slot-outlet', imports: [NgTemplateOutlet], template: `
775
+ @for (slot of slots(); track slot) {
776
+ <ng-container *ngTemplateOutlet="slot.template; context: context()" />
777
+ }
778
+ `, host: {
779
+ '[attr.data-slot]': 'name()',
780
+ // Collapse to nothing when unfilled, so a region's gaps never show as
781
+ // phantom spacing for a slot the app never provided.
782
+ '[hidden]': '!slots().length',
783
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:flex;align-items:center;min-inline-size:0}:host([hidden]){display:none}\n"] }]
784
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], select: [{ type: i0.Input, args: [{ isSignal: true, alias: "select", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
785
+
786
+ /**
787
+ * `true` when at least one template is registered for a slot — lets a region
788
+ * skip its wrapper markup (and its padding) instead of rendering an empty box.
789
+ * Call from an injection context.
790
+ */
791
+ function hasSlot(...names) {
792
+ const registry = inject(ShellSlotRegistry);
793
+ return computed(() => names.some((name) => registry.all(name)().length > 0));
794
+ }
795
+
796
+ /**
797
+ * Route `data` key for a level's label: a string (static, or produced by a
798
+ * resolver under `resolve: { breadcrumb: … }`), or `false` to skip the level.
799
+ */
800
+ const BREADCRUMB_DATA_KEY = 'breadcrumb';
801
+ /** Route `data` key that forces the breadcrumb row on (`true`) or off (`false`) for a page. */
802
+ const SUBHEADER_DATA_KEY = 'subheader';
803
+
804
+ const NO_OVERRIDE = Symbol('no-override');
805
+ function urlOf(snapshot) {
806
+ const segments = snapshot.pathFromRoot.flatMap((route) => route.url.map((part) => part.path));
807
+ return `/${segments.join('/')}`;
808
+ }
809
+ function readLabel(label) {
810
+ return isSignal(label) ? label() : label;
811
+ }
812
+ /**
813
+ * The breadcrumb trail, derived from the router.
814
+ *
815
+ * On every `NavigationEnd` it walks the active (primary) route tree. Each
816
+ * route that consumes URL segments is one level, labelled by — in order — a
817
+ * page override, the route's own `data.breadcrumb` (static or resolved), or
818
+ * its own `title`. Only what a route declares *itself* counts: Angular copies
819
+ * `data` from componentless and empty-path parents into children, and a child
820
+ * must not borrow its parent's label.
821
+ */
822
+ class ShellBreadcrumbStore {
823
+ router = inject(Router, { optional: true });
824
+ shell = inject(ShellStore);
825
+ levels = signal([], ...(ngDevMode ? [{ debugName: "levels" }] : /* istanbul ignore next */ []));
826
+ routeSubheader = signal(undefined, ...(ngDevMode ? [{ debugName: "routeSubheader" }] : /* istanbul ignore next */ []));
827
+ overrides = signal([], ...(ngDevMode ? [{ debugName: "overrides" }] : /* istanbul ignore next */ []));
828
+ /** Labelled levels of the current route, before the home/current options. */
829
+ trail = computed(() => {
830
+ const levels = this.levels();
831
+ const overrides = this.overrides();
832
+ const useTitle = this.shell.config().breadcrumbs.useRouteTitle;
833
+ const crumbs = [];
834
+ levels.forEach((level, index) => {
835
+ const override = this.overrideFor(level.url, index === levels.length - 1, overrides);
836
+ const label = override === NO_OVERRIDE
837
+ ? (level.label ?? (useTitle ? level.title : undefined))
838
+ : (override ?? null);
839
+ if (label !== undefined) {
840
+ crumbs.push({ label, url: level.url, current: false });
841
+ }
842
+ });
843
+ const last = crumbs.length - 1;
844
+ if (last >= 0) {
845
+ crumbs[last] = { ...crumbs[last], current: true };
846
+ }
847
+ return crumbs;
848
+ }, ...(ngDevMode ? [{ debugName: "trail" }] : /* istanbul ignore next */ []));
849
+ /** The trail to render, with `breadcrumbs.includeHome` / `includeCurrent` applied. */
850
+ crumbs = computed(() => {
851
+ const options = this.shell.config().breadcrumbs;
852
+ let crumbs = [...this.trail()];
853
+ if (options.includeHome && crumbs[0]?.url !== options.homeUrl) {
854
+ crumbs.unshift({ label: options.homeLabel, url: options.homeUrl, current: crumbs.length === 0 });
855
+ }
856
+ if (!options.includeCurrent) {
857
+ crumbs = crumbs.filter((crumb) => !crumb.current);
858
+ }
859
+ return crumbs;
860
+ }, ...(ngDevMode ? [{ debugName: "crumbs" }] : /* istanbul ignore next */ []));
861
+ /** Labelled levels in the current route; the home crumb is not counted. */
862
+ depth = computed(() => this.trail().length, ...(ngDevMode ? [{ debugName: "depth" }] : /* istanbul ignore next */ []));
863
+ /** `data.subheader` of the deepest route that declares it, if any. */
864
+ subheaderOverride = this.routeSubheader.asReadonly();
865
+ constructor() {
866
+ this.router?.events
867
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed())
868
+ .subscribe(() => this.refresh());
869
+ this.refresh();
870
+ }
871
+ /**
872
+ * Label one level from code — for names only the page knows, e.g. a record
873
+ * loaded without a resolver. `route` picks the level ending at that route's
874
+ * URL; `null` labels the current level. While a signal label is `undefined`
875
+ * the crumb renders as a placeholder. Returns a function that removes the
876
+ * label again; prefer `contributeBreadcrumbLabel()`, which does so for you.
877
+ */
878
+ setLabel(label, route = null) {
879
+ const entry = { route, label };
880
+ this.overrides.update((current) => [...current, entry]);
881
+ return () => this.overrides.update((current) => current.filter((candidate) => candidate !== entry));
882
+ }
883
+ /** The latest override for a level, `undefined` while pending, or `NO_OVERRIDE`. */
884
+ overrideFor(url, isCurrent, overrides) {
885
+ for (let index = overrides.length - 1; index >= 0; index--) {
886
+ const { route, label } = overrides[index];
887
+ const matches = route ? urlOf(route.snapshot) === url : isCurrent;
888
+ if (matches) {
889
+ return readLabel(label);
890
+ }
891
+ }
892
+ return NO_OVERRIDE;
893
+ }
894
+ refresh() {
895
+ const router = this.router;
896
+ if (!router) {
897
+ return;
898
+ }
899
+ const levels = [];
900
+ const segments = [];
901
+ let subheader;
902
+ for (let route = router.routerState.snapshot.root; route; route = route.firstChild) {
903
+ const config = route.routeConfig;
904
+ const declaresLabel = config?.data?.[BREADCRUMB_DATA_KEY] !== undefined ||
905
+ config?.resolve?.[BREADCRUMB_DATA_KEY] !== undefined;
906
+ const label = declaresLabel ? route.data[BREADCRUMB_DATA_KEY] : undefined;
907
+ const flag = config?.data?.[SUBHEADER_DATA_KEY];
908
+ if (typeof flag === 'boolean') {
909
+ subheader = flag;
910
+ }
911
+ segments.push(...route.url.map((part) => part.path));
912
+ // Empty-path routes add no level; `breadcrumb: false` removes one.
913
+ if (route.url.length > 0 && label !== false) {
914
+ levels.push({
915
+ url: `/${segments.join('/')}`,
916
+ label: typeof label === 'string' ? label : undefined,
917
+ title: config?.title !== undefined ? route.title : undefined,
918
+ });
919
+ }
920
+ }
921
+ this.levels.set(levels);
922
+ this.routeSubheader.set(subheader);
923
+ }
924
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellBreadcrumbStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
925
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellBreadcrumbStore, providedIn: 'root' });
926
+ }
927
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellBreadcrumbStore, decorators: [{
928
+ type: Injectable,
929
+ args: [{ providedIn: 'root' }]
930
+ }], ctorParameters: () => [] });
931
+ /**
932
+ * Label the calling page's own level of the trail, for as long as the page
933
+ * lives. Call from a routed component's injection context:
934
+ *
935
+ * ```ts
936
+ * export class ProjectPage {
937
+ * readonly name = signal<string | undefined>(undefined); // set when loaded
938
+ * constructor() { contributeBreadcrumbLabel(this.name); }
939
+ * }
940
+ * ```
941
+ *
942
+ * While the signal is `undefined` the crumb is a placeholder; the breadcrumb
943
+ * row has reserved its height, so nothing shifts when the name arrives.
944
+ */
945
+ function contributeBreadcrumbLabel(label) {
946
+ const route = inject(ActivatedRoute, { optional: true });
947
+ const remove = inject(ShellBreadcrumbStore).setLabel(label, route);
948
+ inject(DestroyRef).onDestroy(remove);
949
+ }
950
+
951
+ /**
952
+ * Footer strip. Three slots so the common "legal start, meta end" pattern
953
+ * needs no markup of its own, and so a status bar can occupy the middle.
954
+ * Measures itself, because its width depends on the layout and the sidebar.
955
+ */
956
+ class ShellFooterComponent {
957
+ region = measureRegion(inject(ElementRef));
958
+ store = inject(ShellStore);
959
+ regionName = 'footer';
960
+ regionWidth = this.region.width;
961
+ regionSize = this.region.size;
962
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
963
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.23", type: ShellFooterComponent, isStandalone: true, selector: "shell-footer", host: { attributes: { "role": "contentinfo" }, properties: { "attr.data-shell-size": "regionSize()", "attr.data-variant": "store.config().appearance.footer" } }, providers: [{ provide: SHELL_REGION, useExisting: ShellFooterComponent }], ngImport: i0, template: "<shell-slot-outlet name=\"footer-start\" class=\"footer__slot\" />\n<shell-slot-outlet name=\"footer\" class=\"footer__slot footer__slot--main\" />\n<shell-slot-outlet name=\"footer-end\" class=\"footer__slot\" />\n", styles: ["@charset \"UTF-8\";:host{container:shell-footer/inline-size;display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;min-block-size:var(--shell-footer-min-height, 48px);padding:8px var(--shell-topbar-padding-inline, 12px);box-sizing:border-box;border-block-start:1px solid var(--shell-footer-border-color, var(--shell-border, #e6e6e9));background:var(--shell-footer-bg, var(--shell-surface-raised, #f7f7f8));color:var(--shell-footer-fg, var(--shell-fg-muted, #6b6f76));box-shadow:var(--shell-footer-shadow, none);font-size:var(--shell-footer-font-size, var(--shell-font-size-sm, 12.5px));transition-property:background-color,border-color,box-shadow;transition-duration:calc(var(--shell-footer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-footer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){:host{transition-duration:1ms}}:host([data-variant=minimal]){border-block-start-color:var(--shell-footer-border-color, transparent);background:var(--shell-footer-bg, transparent)}.footer__slot{gap:8px 16px;flex-wrap:wrap}.footer__slot--main{flex:1 1 auto;min-inline-size:0}\n"], dependencies: [{ kind: "component", type: ShellSlotOutletComponent, selector: "shell-slot-outlet", inputs: ["name", "select", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
964
+ }
965
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellFooterComponent, decorators: [{
966
+ type: Component,
967
+ args: [{ selector: 'shell-footer', imports: [ShellSlotOutletComponent], host: {
968
+ role: 'contentinfo',
969
+ '[attr.data-shell-size]': 'regionSize()',
970
+ '[attr.data-variant]': 'store.config().appearance.footer',
971
+ }, providers: [{ provide: SHELL_REGION, useExisting: ShellFooterComponent }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<shell-slot-outlet name=\"footer-start\" class=\"footer__slot\" />\n<shell-slot-outlet name=\"footer\" class=\"footer__slot footer__slot--main\" />\n<shell-slot-outlet name=\"footer-end\" class=\"footer__slot\" />\n", styles: ["@charset \"UTF-8\";:host{container:shell-footer/inline-size;display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;min-block-size:var(--shell-footer-min-height, 48px);padding:8px var(--shell-topbar-padding-inline, 12px);box-sizing:border-box;border-block-start:1px solid var(--shell-footer-border-color, var(--shell-border, #e6e6e9));background:var(--shell-footer-bg, var(--shell-surface-raised, #f7f7f8));color:var(--shell-footer-fg, var(--shell-fg-muted, #6b6f76));box-shadow:var(--shell-footer-shadow, none);font-size:var(--shell-footer-font-size, var(--shell-font-size-sm, 12.5px));transition-property:background-color,border-color,box-shadow;transition-duration:calc(var(--shell-footer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-footer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){:host{transition-duration:1ms}}:host([data-variant=minimal]){border-block-start-color:var(--shell-footer-border-color, transparent);background:var(--shell-footer-bg, transparent)}.footer__slot{gap:8px 16px;flex-wrap:wrap}.footer__slot--main{flex:1 1 auto;min-inline-size:0}\n"] }]
972
+ }] });
973
+
974
+ const FOCUSABLE = [
975
+ 'a[href]',
976
+ 'button:not([disabled])',
977
+ 'input:not([disabled]):not([type="hidden"])',
978
+ 'select:not([disabled])',
979
+ 'textarea:not([disabled])',
980
+ '[tabindex]:not([tabindex="-1"])',
981
+ '[contenteditable="true"]',
982
+ ].join(',');
983
+ function focusable(root) {
984
+ return Array.from(root.querySelectorAll(FOCUSABLE)).filter((element) => !element.hasAttribute('inert') &&
985
+ element.offsetWidth + element.offsetHeight > 0 &&
986
+ getComputedStyle(element).visibility !== 'hidden');
987
+ }
988
+ /**
989
+ * Keeps Tab inside `root` while a modal drawer is open and restores focus to
990
+ * whatever was focused before. Small on purpose: no dependency on the CDK, and
991
+ * the only behaviours a navigation drawer needs.
992
+ */
993
+ function trapFocus(root, options = {}) {
994
+ const previous = options.restoreTo ?? document.activeElement;
995
+ const onKeydown = (event) => {
996
+ if (event.key !== 'Tab') {
997
+ return;
998
+ }
999
+ const items = focusable(root);
1000
+ if (!items.length) {
1001
+ event.preventDefault();
1002
+ root.focus();
1003
+ return;
1004
+ }
1005
+ const first = items[0];
1006
+ const last = items[items.length - 1];
1007
+ const active = document.activeElement;
1008
+ if (event.shiftKey && (active === first || !root.contains(active))) {
1009
+ event.preventDefault();
1010
+ last.focus();
1011
+ }
1012
+ else if (!event.shiftKey && active === last) {
1013
+ event.preventDefault();
1014
+ first.focus();
1015
+ }
1016
+ };
1017
+ root.addEventListener('keydown', onKeydown);
1018
+ // Focus the first control, or the panel itself when it has none yet.
1019
+ (focusable(root)[0] ?? root).focus({ preventScroll: true });
1020
+ return {
1021
+ release() {
1022
+ root.removeEventListener('keydown', onKeydown);
1023
+ previous?.focus?.({ preventScroll: true });
1024
+ },
1025
+ };
1026
+ }
1027
+
1028
+ /**
1029
+ * Drop-in toggle. Works anywhere in the app — header, page, command palette —
1030
+ * because it talks to the root store rather than to a parent component.
1031
+ *
1032
+ * ```html
1033
+ * <button shellSidebarToggle class="icon-button">☰</button>
1034
+ * ```
1035
+ *
1036
+ * Sets `aria-expanded`, `aria-controls` and an accessible label that tracks the
1037
+ * current mode, so the same button reads correctly as a hamburger on mobile and
1038
+ * as a collapse control on desktop.
1039
+ */
1040
+ class SidebarToggleDirective {
1041
+ store = inject(ShellStore);
1042
+ sidebarId = SHELL_SIDEBAR_ID;
1043
+ /** True when the sidebar is showing its full, labelled panel. */
1044
+ expanded = computed(() => {
1045
+ const state = this.store.sidebar();
1046
+ return state.mode === 'expanded' || (state.mode === 'overlay' && state.open);
1047
+ }, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
1048
+ label = computed(() => {
1049
+ const labels = this.store.config().labels;
1050
+ const state = this.store.sidebar();
1051
+ if (state.mode === 'overlay') {
1052
+ return state.open ? labels.closeNavigation : labels.openNavigation;
1053
+ }
1054
+ return this.expanded() ? labels.collapseSidebar : labels.expandSidebar;
1055
+ }, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1056
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: SidebarToggleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1057
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.23", type: SidebarToggleDirective, isStandalone: true, selector: "button[shellSidebarToggle]", host: { attributes: { "type": "button" }, listeners: { "click": "store.toggleSidebar()" }, properties: { "attr.aria-controls": "sidebarId", "attr.aria-expanded": "expanded()", "attr.aria-label": "label()", "attr.title": "label()", "attr.data-sidebar-mode": "store.sidebar().mode" } }, exportAs: ["shellSidebarToggle"], ngImport: i0 });
1058
+ }
1059
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: SidebarToggleDirective, decorators: [{
1060
+ type: Directive,
1061
+ args: [{
1062
+ selector: 'button[shellSidebarToggle]',
1063
+ exportAs: 'shellSidebarToggle',
1064
+ host: {
1065
+ type: 'button',
1066
+ '[attr.aria-controls]': 'sidebarId',
1067
+ '[attr.aria-expanded]': 'expanded()',
1068
+ '[attr.aria-label]': 'label()',
1069
+ '[attr.title]': 'label()',
1070
+ '[attr.data-sidebar-mode]': 'store.sidebar().mode',
1071
+ '(click)': 'store.toggleSidebar()',
1072
+ },
1073
+ }]
1074
+ }] });
1075
+
1076
+ const RESIZE_STEP = 16;
1077
+ const RESIZE_STEP_LARGE = 48;
1078
+ /**
1079
+ * Sidebar with four modes — `expanded`, `collapsed` (icon rail), `overlay`
1080
+ * (modal drawer) and `hidden` — chosen by the injected `SidebarBehavior`
1081
+ * rather than by this component. All this class does is render the resolved
1082
+ * state and handle the mechanics each mode needs: peek timing, focus trapping,
1083
+ * Escape, and drag-to-resize.
1084
+ *
1085
+ * Slot order, top to bottom:
1086
+ *
1087
+ * sidebar-header brand, workspace or account switcher
1088
+ * sidebar-action one primary action ("New", "Compose")
1089
+ * sidebar-search quick find / command palette launcher
1090
+ * sidebar-nav the scrolling navigation (grows)
1091
+ * sidebar-secondary settings, help, upgrade prompts
1092
+ * sidebar-footer user card, storage meter — shares a row with the collapse control
1093
+ */
1094
+ class ShellSidebarComponent {
1095
+ host = inject(ElementRef);
1096
+ panel = viewChild('panel', ...(ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []));
1097
+ store = inject(ShellStore);
1098
+ config = this.store.config;
1099
+ /** Render the built-in expand/collapse control in the footer row. */
1100
+ collapseControl = input(true, { ...(ngDevMode ? { debugName: "collapseControl" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1101
+ regionName = 'sidebar';
1102
+ /**
1103
+ * Measured on the *panel*, not the host: during a peek the panel is wider
1104
+ * than the grid track it lives in. Exposed as `--shell-container-width`.
1105
+ */
1106
+ regionWidth = observeWidth(this.panel);
1107
+ /**
1108
+ * The sidebar reports a *semantic* bucket rather than a measured one. Its
1109
+ * panel is a few hundred pixels wide in every mode, so raw widths would put
1110
+ * it permanently in `xs` and make `slotMinSize` useless. The contract is:
1111
+ *
1112
+ * rail (icons only) → `xs`
1113
+ * panel / drawer (labels on) → `md`
1114
+ *
1115
+ * so `slotMinSize="sm"` on a sidebar slot reads as "only when labels show".
1116
+ */
1117
+ regionSize = computed(() => (this.state().labels ? 'md' : 'xs'), ...(ngDevMode ? [{ debugName: "regionSize" }] : /* istanbul ignore next */ []));
1118
+ state = this.store.sidebar;
1119
+ hasHeader = hasSlot('sidebar-header');
1120
+ hasSecondary = hasSlot('sidebar-secondary');
1121
+ hasFooterSlot = hasSlot('sidebar-footer');
1122
+ /** The collapse control only makes sense for persistent panels. */
1123
+ showCollapse = computed(() => {
1124
+ const mode = this.state().mode;
1125
+ return this.collapseControl() && (mode === 'expanded' || mode === 'collapsed');
1126
+ }, ...(ngDevMode ? [{ debugName: "showCollapse" }] : /* istanbul ignore next */ []));
1127
+ showFooter = computed(() => this.hasFooterSlot() || this.showCollapse(), ...(ngDevMode ? [{ debugName: "showFooter" }] : /* istanbul ignore next */ []));
1128
+ containerWidth = computed(() => {
1129
+ const width = this.regionWidth();
1130
+ return width === null ? null : `${width}px`;
1131
+ }, ...(ngDevMode ? [{ debugName: "containerWidth" }] : /* istanbul ignore next */ []));
1132
+ /** Peek only makes sense on a rail, and only when configured. */
1133
+ peekable = computed(() => this.config().sidebar.peek && this.state().mode === 'collapsed', ...(ngDevMode ? [{ debugName: "peekable" }] : /* istanbul ignore next */ []));
1134
+ resizable = computed(() => this.config().sidebar.resizable && this.state().mode === 'expanded', ...(ngDevMode ? [{ debugName: "resizable" }] : /* istanbul ignore next */ []));
1135
+ /** Current panel width in px for `aria-valuenow` and keyboard steps. */
1136
+ resizeValue = computed(() => this.store.sidebarWidth() ?? this.regionWidth() ?? this.config().sidebar.minWidth, ...(ngDevMode ? [{ debugName: "resizeValue" }] : /* istanbul ignore next */ []));
1137
+ constructor() {
1138
+ // Trap focus for as long as the drawer is modal, and hand focus back on close.
1139
+ effect((onCleanup) => {
1140
+ const element = this.panel()?.nativeElement;
1141
+ if (!this.state().modal || !element) {
1142
+ return;
1143
+ }
1144
+ let handle;
1145
+ const frame = requestAnimationFrame(() => (handle = trapFocus(element)));
1146
+ onCleanup(() => {
1147
+ cancelAnimationFrame(frame);
1148
+ handle?.release();
1149
+ });
1150
+ });
1151
+ }
1152
+ // ── Peek ──────────────────────────────────────────────────────────────────
1153
+ onPointerEnter() {
1154
+ if (this.peekable()) {
1155
+ this.store.requestPeek(true);
1156
+ }
1157
+ }
1158
+ onPointerLeave() {
1159
+ if (!this.store.resizing()) {
1160
+ this.store.requestPeek(false);
1161
+ }
1162
+ }
1163
+ /** Keyboard users get the same peek: focus entering the rail widens it. */
1164
+ onFocusIn() {
1165
+ if (this.peekable()) {
1166
+ this.store.requestPeek(true);
1167
+ }
1168
+ }
1169
+ onFocusOut(event) {
1170
+ const next = event.relatedTarget;
1171
+ if (!next || !this.panel()?.nativeElement.contains(next)) {
1172
+ this.store.requestPeek(false);
1173
+ }
1174
+ }
1175
+ onEscape() {
1176
+ if (this.state().modal) {
1177
+ this.store.closeSidebar();
1178
+ }
1179
+ }
1180
+ // ── Resize ────────────────────────────────────────────────────────────────
1181
+ onResizeStart(event) {
1182
+ if (event.button !== 0) {
1183
+ return;
1184
+ }
1185
+ event.preventDefault();
1186
+ event.currentTarget.setPointerCapture(event.pointerId);
1187
+ this.store.setResizing(true);
1188
+ }
1189
+ onResizeMove(event) {
1190
+ const panel = this.panel()?.nativeElement;
1191
+ if (!this.store.resizing() || !panel) {
1192
+ return;
1193
+ }
1194
+ // Measured from the grid track (the host), not the panel: a floating panel
1195
+ // is inset from its track, and the handle must stay under the pointer.
1196
+ const track = this.host.nativeElement.getBoundingClientRect();
1197
+ const inset = Number.parseFloat(getComputedStyle(panel).marginInlineEnd) || 0;
1198
+ const offset = this.isRtl(panel) ? track.right - event.clientX : event.clientX - track.left;
1199
+ this.store.setSidebarWidth(offset + inset, { persist: false });
1200
+ }
1201
+ onResizeEnd(event) {
1202
+ const handle = event.currentTarget;
1203
+ if (handle.hasPointerCapture(event.pointerId)) {
1204
+ handle.releasePointerCapture(event.pointerId);
1205
+ }
1206
+ if (this.store.resizing()) {
1207
+ this.store.setResizing(false);
1208
+ this.store.setSidebarWidth(this.store.sidebarWidth());
1209
+ }
1210
+ }
1211
+ onResizeKey(event) {
1212
+ const panel = this.panel()?.nativeElement;
1213
+ if (!panel) {
1214
+ return;
1215
+ }
1216
+ const { minWidth, maxWidth } = this.config().sidebar;
1217
+ const step = event.shiftKey ? RESIZE_STEP_LARGE : RESIZE_STEP;
1218
+ // "Right" grows the panel in LTR and shrinks it in RTL.
1219
+ const grow = this.isRtl(panel) ? -step : step;
1220
+ const current = this.resizeValue();
1221
+ let next;
1222
+ switch (event.key) {
1223
+ case 'ArrowRight':
1224
+ next = current + grow;
1225
+ break;
1226
+ case 'ArrowLeft':
1227
+ next = current - grow;
1228
+ break;
1229
+ case 'Home':
1230
+ next = minWidth;
1231
+ break;
1232
+ case 'End':
1233
+ next = maxWidth;
1234
+ break;
1235
+ default:
1236
+ return;
1237
+ }
1238
+ event.preventDefault();
1239
+ this.store.setSidebarWidth(next);
1240
+ }
1241
+ /** Double-click the handle to return to the configured width. */
1242
+ onResizeReset() {
1243
+ this.store.setSidebarWidth(null);
1244
+ }
1245
+ isRtl(element) {
1246
+ return getComputedStyle(element).direction === 'rtl';
1247
+ }
1248
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1249
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: ShellSidebarComponent, isStandalone: true, selector: "shell-sidebar", inputs: { collapseControl: { classPropertyName: "collapseControl", publicName: "collapseControl", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-mode": "state().mode", "attr.data-open": "state().open", "attr.data-labels": "state().labels", "attr.data-peeked": "state().peeked", "attr.data-shell-size": "regionSize()", "attr.data-resizing": "store.resizing() || null", "attr.data-variant": "config().appearance.sidebar", "attr.data-drawer-motion": "config().motion.drawer", "attr.inert": "state().open ? null : \"\"" } }, providers: [{ provide: SHELL_REGION, useExisting: ShellSidebarComponent }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n `aside` wraps the panel so the drawer can slide and the rail can peek without\n moving the grid track. Everything below reads from one resolved state object.\n-->\n<aside\n #panel\n class=\"sidebar__panel\"\n [style.--shell-sidebar-panel-width]=\"state().panelWidth\"\n [style.--shell-container-width]=\"containerWidth()\"\n [attr.role]=\"state().modal ? 'dialog' : null\"\n [attr.aria-modal]=\"state().modal ? 'true' : null\"\n [attr.aria-label]=\"state().modal ? config().labels.primaryNavigation : null\"\n [attr.tabindex]=\"state().modal ? -1 : null\"\n (keydown.escape)=\"onEscape()\"\n (pointerenter)=\"onPointerEnter()\"\n (pointerleave)=\"onPointerLeave()\"\n (focusin)=\"onFocusIn()\"\n (focusout)=\"onFocusOut($event)\"\n>\n @if (hasHeader()) {\n <div class=\"sidebar__header\">\n <shell-slot-outlet name=\"sidebar-header\" class=\"sidebar__header-slot\" />\n </div>\n }\n\n <shell-slot-outlet name=\"sidebar-action\" class=\"sidebar__block\" />\n <shell-slot-outlet name=\"sidebar-search\" class=\"sidebar__block\" />\n\n <nav class=\"sidebar__nav\" [attr.aria-label]=\"config().labels.primaryNavigation\">\n <shell-slot-outlet name=\"sidebar-nav\" class=\"sidebar__block\" />\n </nav>\n\n @if (hasSecondary()) {\n <shell-slot-outlet name=\"sidebar-secondary\" class=\"sidebar__block sidebar__secondary\" />\n }\n\n @if (showFooter()) {\n <div class=\"sidebar__footer\">\n <shell-slot-outlet name=\"sidebar-footer\" class=\"sidebar__footer-slot\" />\n\n @if (showCollapse()) {\n <button shellSidebarToggle class=\"sidebar__collapse\">\n <span class=\"sidebar__collapse-glyph\" aria-hidden=\"true\"></span>\n </button>\n }\n </div>\n }\n\n @if (resizable()) {\n <div\n class=\"sidebar__resizer\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n tabindex=\"0\"\n [attr.aria-label]=\"config().labels.resizeSidebar\"\n [attr.aria-valuemin]=\"config().sidebar.minWidth\"\n [attr.aria-valuemax]=\"config().sidebar.maxWidth\"\n [attr.aria-valuenow]=\"resizeValue()\"\n (pointerdown)=\"onResizeStart($event)\"\n (pointermove)=\"onResizeMove($event)\"\n (pointerup)=\"onResizeEnd($event)\"\n (pointercancel)=\"onResizeEnd($event)\"\n (keydown)=\"onResizeKey($event)\"\n (dblclick)=\"onResizeReset()\"\n ></div>\n }\n</aside>\n", styles: ["@charset \"UTF-8\";:host{--shell-drawer-offset: -100%;--shell-drawer-origin: left;display:block;position:relative;min-inline-size:0}:host-context([dir=rtl]){--shell-drawer-offset: 100%;--shell-drawer-origin: right}.sidebar__panel{position:sticky;inset-block-start:var(--shell-sidebar-top-offset, 0px);display:flex;flex-direction:column;gap:var(--shell-sidebar-gap, 2px);box-sizing:border-box;inline-size:var(--shell-sidebar-panel-width, var(--shell-sidebar-width, 264px));max-inline-size:100vw;block-size:var(--shell-sidebar-block-size, 100%);padding:var(--shell-sidebar-padding, 12px);border-inline-end:1px solid var(--shell-sidebar-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-sidebar-radius, 0px);background:var(--shell-sidebar-bg, var(--shell-sidebar-surface, #fafafb));color:var(--shell-sidebar-fg, var(--shell-fg, #1b1c1f));box-shadow:var(--shell-sidebar-shadow, none);overflow:clip;transition:inline-size calc(var(--shell-collapse-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-collapse-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),box-shadow calc(var(--shell-sidebar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-sidebar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),background-color calc(var(--shell-sidebar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-sidebar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),transform calc(var(--shell-drawer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-drawer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),opacity calc(var(--shell-drawer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-drawer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),visibility calc(var(--shell-drawer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-drawer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.sidebar__panel{transition-duration:1ms}}:host([data-resizing]) .sidebar__panel{transition-duration:0s}:host([data-variant=borderless]) .sidebar__panel{border-inline-end-color:var(--shell-sidebar-border-color, transparent)}:host([data-variant=floating]:not([data-mode=overlay])) .sidebar__panel{--_inset: var(--shell-sidebar-inset, 8px);margin:var(--_inset);inline-size:calc(var(--shell-sidebar-panel-width, var(--shell-sidebar-width, 264px)) - 2 * var(--_inset));block-size:calc(var(--shell-sidebar-block-size, 100%) - 2 * var(--_inset));border:1px solid var(--shell-sidebar-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-sidebar-radius, var(--shell-radius, 10px));box-shadow:var(--shell-sidebar-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)))}:host([data-peeked=true]) .sidebar__panel{box-shadow:var(--shell-sidebar-peek-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)))}:host([data-layout=inset]:not([data-mode=overlay]):not([data-variant=floating])) .sidebar__panel{border-inline-end-color:var(--shell-sidebar-border-color, transparent);background:var(--shell-sidebar-bg, transparent);box-shadow:var(--shell-sidebar-shadow, none)}:host([data-layout=inset][data-peeked=true]:not([data-variant=floating])) .sidebar__panel{background:var(--shell-sidebar-bg, var(--shell-sidebar-surface, #fafafb));box-shadow:var(--shell-sidebar-peek-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)))}:host([data-mode=overlay]) .sidebar__panel{position:fixed;inset-block:0;inset-inline-start:0;block-size:100dvh;border-radius:0;border-start-end-radius:var(--shell-drawer-radius, 0px);border-end-end-radius:var(--shell-drawer-radius, 0px);background:var(--shell-drawer-bg, var(--shell-sidebar-bg, var(--shell-sidebar-surface, #fafafb)));transform-origin:var(--shell-drawer-origin) center;visibility:hidden}:host([data-mode=overlay][data-drawer-motion=slide]) .sidebar__panel{transform:translate(var(--shell-drawer-offset))}:host([data-mode=overlay][data-drawer-motion=fade]) .sidebar__panel{opacity:0}:host([data-mode=overlay][data-drawer-motion=scale]) .sidebar__panel{opacity:0;transform:scale(.94)}:host([data-mode=overlay][data-drawer-motion=none]) .sidebar__panel{transition-duration:0s}:host([data-mode=overlay][data-open=true]) .sidebar__panel{visibility:visible;opacity:1;transform:none;box-shadow:var(--shell-drawer-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)))}:host([data-mode=hidden]){display:none}.sidebar__header{display:flex;align-items:center;flex:0 0 auto;min-block-size:calc(var(--shell-nav-item-height, 36px) + 6px)}.sidebar__header-slot{flex:1 1 auto}.sidebar__block{flex:0 0 auto;flex-direction:column;align-items:stretch;gap:var(--shell-sidebar-gap, 2px)}.sidebar__nav{display:flex;flex-direction:column;flex:1 1 auto;min-block-size:0;margin-inline:calc(-1 * var(--shell-sidebar-padding, 12px));padding-inline:var(--shell-sidebar-padding, 12px);overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin}.sidebar__secondary{padding-block-start:var(--shell-sidebar-gap, 2px)}.sidebar__footer{display:flex;align-items:center;gap:8px;flex:0 0 auto;padding-block-start:8px;border-block-start:1px solid var(--shell-sidebar-border-color, var(--shell-border, #e6e6e9))}.sidebar__footer-slot{flex:1 1 auto;min-inline-size:0}.sidebar__collapse{display:grid;place-items:center;flex:0 0 auto;inline-size:32px;block-size:32px;margin-inline-start:auto;padding:0;border:0;border-radius:var(--shell-radius-sm, 6px);background:transparent;color:var(--shell-fg-muted, #6b6f76);cursor:pointer;transition-property:background-color,color;transition-duration:calc(var(--shell-sidebar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-sidebar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.sidebar__collapse{transition-duration:1ms}}.sidebar__collapse:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:2px}.sidebar__collapse:hover{background:var(--shell-hover-surface, #f0f0f3);color:var(--shell-sidebar-fg, var(--shell-fg, #1b1c1f))}.sidebar__collapse-glyph{inline-size:7px;block-size:7px;border-inline-start:2px solid currentcolor;border-block-end:2px solid currentcolor;transform:translate(1px) rotate(45deg);transition-property:transform;transition-duration:calc(var(--shell-collapse-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-collapse-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.sidebar__collapse-glyph{transition-duration:1ms}}:host([data-mode=collapsed]) .sidebar__collapse-glyph{transform:translate(-1px) rotate(-135deg)}:host-context([dir=rtl]) .sidebar__collapse-glyph{scale:-1 1}:host([data-labels=false]) .sidebar__panel{padding-inline:8px}:host([data-labels=false]) .sidebar__nav{margin-inline:-8px;padding-inline:8px}:host([data-labels=false]) .sidebar__footer{flex-direction:column}:host([data-labels=false]) .sidebar__collapse{margin-inline-start:0}.sidebar__resizer{position:absolute;inset-block:0;inset-inline-end:0;z-index:1;inline-size:6px;cursor:col-resize;touch-action:none}.sidebar__resizer:after{content:\"\";position:absolute;inset-block:0;inset-inline-end:0;inline-size:2px;background:transparent;transition-property:background-color;transition-duration:calc(var(--shell-sidebar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-sidebar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.sidebar__resizer:after{transition-duration:1ms}}.sidebar__resizer:hover:after,.sidebar__resizer:focus-visible:after{background:var(--shell-accent, #4f46e5)}.sidebar__resizer:focus-visible{outline:none}:host([data-resizing]) .sidebar__resizer:after{background:var(--shell-accent, #4f46e5)}\n"], dependencies: [{ kind: "component", type: ShellSlotOutletComponent, selector: "shell-slot-outlet", inputs: ["name", "select", "size"] }, { kind: "directive", type: SidebarToggleDirective, selector: "button[shellSidebarToggle]", exportAs: ["shellSidebarToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1250
+ }
1251
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSidebarComponent, decorators: [{
1252
+ type: Component,
1253
+ args: [{ selector: 'shell-sidebar', imports: [ShellSlotOutletComponent, SidebarToggleDirective], host: {
1254
+ '[attr.data-mode]': 'state().mode',
1255
+ '[attr.data-open]': 'state().open',
1256
+ '[attr.data-labels]': 'state().labels',
1257
+ '[attr.data-peeked]': 'state().peeked',
1258
+ '[attr.data-shell-size]': 'regionSize()',
1259
+ '[attr.data-resizing]': 'store.resizing() || null',
1260
+ '[attr.data-variant]': 'config().appearance.sidebar',
1261
+ '[attr.data-drawer-motion]': 'config().motion.drawer',
1262
+ '[attr.inert]': 'state().open ? null : ""',
1263
+ }, providers: [{ provide: SHELL_REGION, useExisting: ShellSidebarComponent }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n `aside` wraps the panel so the drawer can slide and the rail can peek without\n moving the grid track. Everything below reads from one resolved state object.\n-->\n<aside\n #panel\n class=\"sidebar__panel\"\n [style.--shell-sidebar-panel-width]=\"state().panelWidth\"\n [style.--shell-container-width]=\"containerWidth()\"\n [attr.role]=\"state().modal ? 'dialog' : null\"\n [attr.aria-modal]=\"state().modal ? 'true' : null\"\n [attr.aria-label]=\"state().modal ? config().labels.primaryNavigation : null\"\n [attr.tabindex]=\"state().modal ? -1 : null\"\n (keydown.escape)=\"onEscape()\"\n (pointerenter)=\"onPointerEnter()\"\n (pointerleave)=\"onPointerLeave()\"\n (focusin)=\"onFocusIn()\"\n (focusout)=\"onFocusOut($event)\"\n>\n @if (hasHeader()) {\n <div class=\"sidebar__header\">\n <shell-slot-outlet name=\"sidebar-header\" class=\"sidebar__header-slot\" />\n </div>\n }\n\n <shell-slot-outlet name=\"sidebar-action\" class=\"sidebar__block\" />\n <shell-slot-outlet name=\"sidebar-search\" class=\"sidebar__block\" />\n\n <nav class=\"sidebar__nav\" [attr.aria-label]=\"config().labels.primaryNavigation\">\n <shell-slot-outlet name=\"sidebar-nav\" class=\"sidebar__block\" />\n </nav>\n\n @if (hasSecondary()) {\n <shell-slot-outlet name=\"sidebar-secondary\" class=\"sidebar__block sidebar__secondary\" />\n }\n\n @if (showFooter()) {\n <div class=\"sidebar__footer\">\n <shell-slot-outlet name=\"sidebar-footer\" class=\"sidebar__footer-slot\" />\n\n @if (showCollapse()) {\n <button shellSidebarToggle class=\"sidebar__collapse\">\n <span class=\"sidebar__collapse-glyph\" aria-hidden=\"true\"></span>\n </button>\n }\n </div>\n }\n\n @if (resizable()) {\n <div\n class=\"sidebar__resizer\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n tabindex=\"0\"\n [attr.aria-label]=\"config().labels.resizeSidebar\"\n [attr.aria-valuemin]=\"config().sidebar.minWidth\"\n [attr.aria-valuemax]=\"config().sidebar.maxWidth\"\n [attr.aria-valuenow]=\"resizeValue()\"\n (pointerdown)=\"onResizeStart($event)\"\n (pointermove)=\"onResizeMove($event)\"\n (pointerup)=\"onResizeEnd($event)\"\n (pointercancel)=\"onResizeEnd($event)\"\n (keydown)=\"onResizeKey($event)\"\n (dblclick)=\"onResizeReset()\"\n ></div>\n }\n</aside>\n", styles: ["@charset \"UTF-8\";:host{--shell-drawer-offset: -100%;--shell-drawer-origin: left;display:block;position:relative;min-inline-size:0}:host-context([dir=rtl]){--shell-drawer-offset: 100%;--shell-drawer-origin: right}.sidebar__panel{position:sticky;inset-block-start:var(--shell-sidebar-top-offset, 0px);display:flex;flex-direction:column;gap:var(--shell-sidebar-gap, 2px);box-sizing:border-box;inline-size:var(--shell-sidebar-panel-width, var(--shell-sidebar-width, 264px));max-inline-size:100vw;block-size:var(--shell-sidebar-block-size, 100%);padding:var(--shell-sidebar-padding, 12px);border-inline-end:1px solid var(--shell-sidebar-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-sidebar-radius, 0px);background:var(--shell-sidebar-bg, var(--shell-sidebar-surface, #fafafb));color:var(--shell-sidebar-fg, var(--shell-fg, #1b1c1f));box-shadow:var(--shell-sidebar-shadow, none);overflow:clip;transition:inline-size calc(var(--shell-collapse-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-collapse-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),box-shadow calc(var(--shell-sidebar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-sidebar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),background-color calc(var(--shell-sidebar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-sidebar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),transform calc(var(--shell-drawer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-drawer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),opacity calc(var(--shell-drawer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-drawer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))),visibility calc(var(--shell-drawer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1)) var(--shell-drawer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.sidebar__panel{transition-duration:1ms}}:host([data-resizing]) .sidebar__panel{transition-duration:0s}:host([data-variant=borderless]) .sidebar__panel{border-inline-end-color:var(--shell-sidebar-border-color, transparent)}:host([data-variant=floating]:not([data-mode=overlay])) .sidebar__panel{--_inset: var(--shell-sidebar-inset, 8px);margin:var(--_inset);inline-size:calc(var(--shell-sidebar-panel-width, var(--shell-sidebar-width, 264px)) - 2 * var(--_inset));block-size:calc(var(--shell-sidebar-block-size, 100%) - 2 * var(--_inset));border:1px solid var(--shell-sidebar-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-sidebar-radius, var(--shell-radius, 10px));box-shadow:var(--shell-sidebar-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)))}:host([data-peeked=true]) .sidebar__panel{box-shadow:var(--shell-sidebar-peek-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)))}:host([data-layout=inset]:not([data-mode=overlay]):not([data-variant=floating])) .sidebar__panel{border-inline-end-color:var(--shell-sidebar-border-color, transparent);background:var(--shell-sidebar-bg, transparent);box-shadow:var(--shell-sidebar-shadow, none)}:host([data-layout=inset][data-peeked=true]:not([data-variant=floating])) .sidebar__panel{background:var(--shell-sidebar-bg, var(--shell-sidebar-surface, #fafafb));box-shadow:var(--shell-sidebar-peek-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)))}:host([data-mode=overlay]) .sidebar__panel{position:fixed;inset-block:0;inset-inline-start:0;block-size:100dvh;border-radius:0;border-start-end-radius:var(--shell-drawer-radius, 0px);border-end-end-radius:var(--shell-drawer-radius, 0px);background:var(--shell-drawer-bg, var(--shell-sidebar-bg, var(--shell-sidebar-surface, #fafafb)));transform-origin:var(--shell-drawer-origin) center;visibility:hidden}:host([data-mode=overlay][data-drawer-motion=slide]) .sidebar__panel{transform:translate(var(--shell-drawer-offset))}:host([data-mode=overlay][data-drawer-motion=fade]) .sidebar__panel{opacity:0}:host([data-mode=overlay][data-drawer-motion=scale]) .sidebar__panel{opacity:0;transform:scale(.94)}:host([data-mode=overlay][data-drawer-motion=none]) .sidebar__panel{transition-duration:0s}:host([data-mode=overlay][data-open=true]) .sidebar__panel{visibility:visible;opacity:1;transform:none;box-shadow:var(--shell-drawer-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)))}:host([data-mode=hidden]){display:none}.sidebar__header{display:flex;align-items:center;flex:0 0 auto;min-block-size:calc(var(--shell-nav-item-height, 36px) + 6px)}.sidebar__header-slot{flex:1 1 auto}.sidebar__block{flex:0 0 auto;flex-direction:column;align-items:stretch;gap:var(--shell-sidebar-gap, 2px)}.sidebar__nav{display:flex;flex-direction:column;flex:1 1 auto;min-block-size:0;margin-inline:calc(-1 * var(--shell-sidebar-padding, 12px));padding-inline:var(--shell-sidebar-padding, 12px);overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin}.sidebar__secondary{padding-block-start:var(--shell-sidebar-gap, 2px)}.sidebar__footer{display:flex;align-items:center;gap:8px;flex:0 0 auto;padding-block-start:8px;border-block-start:1px solid var(--shell-sidebar-border-color, var(--shell-border, #e6e6e9))}.sidebar__footer-slot{flex:1 1 auto;min-inline-size:0}.sidebar__collapse{display:grid;place-items:center;flex:0 0 auto;inline-size:32px;block-size:32px;margin-inline-start:auto;padding:0;border:0;border-radius:var(--shell-radius-sm, 6px);background:transparent;color:var(--shell-fg-muted, #6b6f76);cursor:pointer;transition-property:background-color,color;transition-duration:calc(var(--shell-sidebar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-sidebar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.sidebar__collapse{transition-duration:1ms}}.sidebar__collapse:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:2px}.sidebar__collapse:hover{background:var(--shell-hover-surface, #f0f0f3);color:var(--shell-sidebar-fg, var(--shell-fg, #1b1c1f))}.sidebar__collapse-glyph{inline-size:7px;block-size:7px;border-inline-start:2px solid currentcolor;border-block-end:2px solid currentcolor;transform:translate(1px) rotate(45deg);transition-property:transform;transition-duration:calc(var(--shell-collapse-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-collapse-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.sidebar__collapse-glyph{transition-duration:1ms}}:host([data-mode=collapsed]) .sidebar__collapse-glyph{transform:translate(-1px) rotate(-135deg)}:host-context([dir=rtl]) .sidebar__collapse-glyph{scale:-1 1}:host([data-labels=false]) .sidebar__panel{padding-inline:8px}:host([data-labels=false]) .sidebar__nav{margin-inline:-8px;padding-inline:8px}:host([data-labels=false]) .sidebar__footer{flex-direction:column}:host([data-labels=false]) .sidebar__collapse{margin-inline-start:0}.sidebar__resizer{position:absolute;inset-block:0;inset-inline-end:0;z-index:1;inline-size:6px;cursor:col-resize;touch-action:none}.sidebar__resizer:after{content:\"\";position:absolute;inset-block:0;inset-inline-end:0;inline-size:2px;background:transparent;transition-property:background-color;transition-duration:calc(var(--shell-sidebar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-sidebar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.sidebar__resizer:after{transition-duration:1ms}}.sidebar__resizer:hover:after,.sidebar__resizer:focus-visible:after{background:var(--shell-accent, #4f46e5)}.sidebar__resizer:focus-visible{outline:none}:host([data-resizing]) .sidebar__resizer:after{background:var(--shell-accent, #4f46e5)}\n"] }]
1264
+ }], ctorParameters: () => [], propDecorators: { panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], collapseControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseControl", required: false }] }] } });
1265
+
1266
+ let nextId = 0;
1267
+ /**
1268
+ * The shell's ⋯ menu: a disclosure button plus a panel for projected content.
1269
+ * Shared by the topbar, the breadcrumb row and the breadcrumb's folded levels,
1270
+ * so behaviour, motion and accessibility live in one place.
1271
+ *
1272
+ * The panel stays mounted so it can animate out as well as in; while closed it
1273
+ * is invisible and `inert`. It closes on Escape (returning focus to the
1274
+ * trigger), on a press outside, and after a link or button inside is used.
1275
+ */
1276
+ class ShellOverflowMenuComponent {
1277
+ /** Accessible name and tooltip of the trigger. */
1278
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1279
+ /** Which edge of the trigger the panel lines up with. */
1280
+ align = input('end', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
1281
+ host = inject(ElementRef);
1282
+ trigger = viewChild.required('trigger');
1283
+ store = inject(ShellStore);
1284
+ motion = computed(() => this.store.config().motion.overflowMenu, ...(ngDevMode ? [{ debugName: "motion" }] : /* istanbul ignore next */ []));
1285
+ panelId = `shell-overflow-menu-${nextId++}`;
1286
+ openState = signal(false, ...(ngDevMode ? [{ debugName: "openState" }] : /* istanbul ignore next */ []));
1287
+ open = this.openState.asReadonly();
1288
+ toggle() {
1289
+ this.openState.update((open) => !open);
1290
+ }
1291
+ close(restoreFocus = false) {
1292
+ if (!this.openState()) {
1293
+ return;
1294
+ }
1295
+ this.openState.set(false);
1296
+ if (restoreFocus) {
1297
+ this.trigger().nativeElement.focus();
1298
+ }
1299
+ }
1300
+ onDocumentPointerDown(event) {
1301
+ if (!this.host.nativeElement.contains(event.target)) {
1302
+ this.close();
1303
+ }
1304
+ }
1305
+ /** Activating anything inside the menu is a choice; close behind it. */
1306
+ onPanelClick(event) {
1307
+ if (event.target.closest('a[href], button:not([aria-haspopup])')) {
1308
+ this.close();
1309
+ }
1310
+ }
1311
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellOverflowMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1312
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.23", type: ShellOverflowMenuComponent, isStandalone: true, selector: "shell-overflow-menu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown.escape": "close(true)", "document:pointerdown": "onDocumentPointerDown($event)" }, properties: { "attr.data-align": "align()", "attr.data-menu-motion": "motion()" } }, viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<button\n #trigger\n type=\"button\"\n class=\"shell-menu__trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-label]=\"label()\"\n [attr.title]=\"label()\"\n (click)=\"toggle()\"\n>\n <span class=\"shell-menu__glyph\" aria-hidden=\"true\"></span>\n</button>\n\n<div\n class=\"shell-menu__panel\"\n [id]=\"panelId\"\n [attr.data-open]=\"open()\"\n [attr.inert]=\"open() ? null : ''\"\n (click)=\"onPanelClick($event)\"\n>\n <ng-content />\n</div>\n", styles: ["@charset \"UTF-8\";:host{position:relative;display:inline-flex;flex:0 0 auto}.shell-menu__trigger{display:grid;place-items:center;inline-size:var(--shell-overflow-trigger-size, 38px);block-size:var(--shell-overflow-trigger-size, 38px);padding:0;border:0;border-radius:var(--shell-radius-sm, 6px);background:transparent;color:inherit;cursor:pointer;transition-property:background-color;transition-duration:calc(var(--shell-menu-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-menu-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.shell-menu__trigger{transition-duration:1ms}}.shell-menu__trigger:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:2px}.shell-menu__trigger:hover{background:var(--shell-hover-surface, #f0f0f3)}.shell-menu__trigger[aria-expanded=true]{background:var(--shell-active-surface, #e9e9ee)}.shell-menu__glyph{inline-size:4px;block-size:4px;border-radius:50%;background:currentcolor;box-shadow:-7px 0 0 currentcolor,7px 0 0 currentcolor}.shell-menu__panel{position:absolute;inset-inline-end:0;inset-block-start:calc(100% + 6px);z-index:var(--shell-z-flyout, 50);display:flex;flex-direction:column;gap:4px;min-inline-size:220px;max-inline-size:min(90vw,320px);padding:8px;border:1px solid var(--shell-menu-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-menu-radius, var(--shell-radius, 10px));background:var(--shell-menu-bg, var(--shell-surface, #ffffff));color:var(--shell-fg, #1b1c1f);box-shadow:var(--shell-menu-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)));transform-origin:top right;transition-property:opacity,transform,visibility;transition-duration:calc(var(--shell-menu-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-menu-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.shell-menu__panel{transition-duration:1ms}}.shell-menu__panel[data-open=false]{opacity:0;visibility:hidden;pointer-events:none}:host([data-align=start]) .shell-menu__panel{inset-inline-start:0;inset-inline-end:auto;transform-origin:top left}:host-context([dir=rtl]) .shell-menu__panel{transform-origin:top left}:host([data-menu-motion=scale]) .shell-menu__panel[data-open=false]{transform:scale(.95)}:host([data-menu-motion=slide]) .shell-menu__panel[data-open=false]{transform:translateY(-8px)}:host([data-menu-motion=none]) .shell-menu__panel{transition-duration:0s}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1313
+ }
1314
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellOverflowMenuComponent, decorators: [{
1315
+ type: Component,
1316
+ args: [{ selector: 'shell-overflow-menu', host: {
1317
+ '[attr.data-align]': 'align()',
1318
+ '[attr.data-menu-motion]': 'motion()',
1319
+ '(keydown.escape)': 'close(true)',
1320
+ '(document:pointerdown)': 'onDocumentPointerDown($event)',
1321
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n #trigger\n type=\"button\"\n class=\"shell-menu__trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-label]=\"label()\"\n [attr.title]=\"label()\"\n (click)=\"toggle()\"\n>\n <span class=\"shell-menu__glyph\" aria-hidden=\"true\"></span>\n</button>\n\n<div\n class=\"shell-menu__panel\"\n [id]=\"panelId\"\n [attr.data-open]=\"open()\"\n [attr.inert]=\"open() ? null : ''\"\n (click)=\"onPanelClick($event)\"\n>\n <ng-content />\n</div>\n", styles: ["@charset \"UTF-8\";:host{position:relative;display:inline-flex;flex:0 0 auto}.shell-menu__trigger{display:grid;place-items:center;inline-size:var(--shell-overflow-trigger-size, 38px);block-size:var(--shell-overflow-trigger-size, 38px);padding:0;border:0;border-radius:var(--shell-radius-sm, 6px);background:transparent;color:inherit;cursor:pointer;transition-property:background-color;transition-duration:calc(var(--shell-menu-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-menu-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.shell-menu__trigger{transition-duration:1ms}}.shell-menu__trigger:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:2px}.shell-menu__trigger:hover{background:var(--shell-hover-surface, #f0f0f3)}.shell-menu__trigger[aria-expanded=true]{background:var(--shell-active-surface, #e9e9ee)}.shell-menu__glyph{inline-size:4px;block-size:4px;border-radius:50%;background:currentcolor;box-shadow:-7px 0 0 currentcolor,7px 0 0 currentcolor}.shell-menu__panel{position:absolute;inset-inline-end:0;inset-block-start:calc(100% + 6px);z-index:var(--shell-z-flyout, 50);display:flex;flex-direction:column;gap:4px;min-inline-size:220px;max-inline-size:min(90vw,320px);padding:8px;border:1px solid var(--shell-menu-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-menu-radius, var(--shell-radius, 10px));background:var(--shell-menu-bg, var(--shell-surface, #ffffff));color:var(--shell-fg, #1b1c1f);box-shadow:var(--shell-menu-shadow, var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28)));transform-origin:top right;transition-property:opacity,transform,visibility;transition-duration:calc(var(--shell-menu-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-menu-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.shell-menu__panel{transition-duration:1ms}}.shell-menu__panel[data-open=false]{opacity:0;visibility:hidden;pointer-events:none}:host([data-align=start]) .shell-menu__panel{inset-inline-start:0;inset-inline-end:auto;transform-origin:top left}:host-context([dir=rtl]) .shell-menu__panel{transform-origin:top left}:host([data-menu-motion=scale]) .shell-menu__panel[data-open=false]{transform:scale(.95)}:host([data-menu-motion=slide]) .shell-menu__panel[data-open=false]{transform:translateY(-8px)}:host([data-menu-motion=none]) .shell-menu__panel{transition-duration:0s}\n"] }]
1322
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
1323
+
1324
+ /**
1325
+ * Breadcrumb trail, following the WAI-ARIA breadcrumb pattern:
1326
+ * `<nav aria-label>` › `<ol>` › links, the current page as text with
1327
+ * `aria-current="page"`, and CSS separators that screen readers skip.
1328
+ *
1329
+ * Renders the router-derived trail from `ShellBreadcrumbStore`, or `[items]`.
1330
+ * It measures its own width and shortens itself instead of wrapping:
1331
+ * full → middle levels folded into "…" → first › … › parent › current →
1332
+ * a single "‹ Parent" back link.
1333
+ */
1334
+ class ShellBreadcrumbComponent {
1335
+ /** A manual trail. Leave unset to follow the router. */
1336
+ items = input(null, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1337
+ /** Overrides `breadcrumbs.maxItems` for this instance. */
1338
+ maxItems = input(null, ...(ngDevMode ? [{ debugName: "maxItems" }] : /* istanbul ignore next */ []));
1339
+ /** Force a layout instead of choosing one from the available width. */
1340
+ display = input('auto', ...(ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []));
1341
+ /** Custom crumb content; the link and `aria-current` stay the library's. */
1342
+ itemTemplate = input(null, ...(ngDevMode ? [{ debugName: "itemTemplate" }] : /* istanbul ignore next */ []));
1343
+ store = inject(ShellBreadcrumbStore);
1344
+ shell = inject(ShellStore);
1345
+ width = observeWidth(inject(ElementRef));
1346
+ options = computed(() => this.shell.config().breadcrumbs, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
1347
+ separator = computed(() => this.options().separator, ...(ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []));
1348
+ labels = computed(() => this.shell.config().labels, ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
1349
+ crumbs = computed(() => this.items() ?? this.store.crumbs(), ...(ngDevMode ? [{ debugName: "crumbs" }] : /* istanbul ignore next */ []));
1350
+ mode = computed(() => {
1351
+ const forced = this.display();
1352
+ if (forced !== 'auto') {
1353
+ return forced;
1354
+ }
1355
+ const width = this.width();
1356
+ if (width === null) {
1357
+ return 'full';
1358
+ }
1359
+ const { backBelow, compactBelow } = this.options();
1360
+ return width < backBelow ? 'back' : width < compactBelow ? 'compact' : 'full';
1361
+ }, ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
1362
+ view = computed(() => {
1363
+ const crumbs = this.crumbs();
1364
+ // compact = first › … › parent › current
1365
+ const limit = this.mode() === 'compact' ? 3 : Math.max(2, this.maxItems() ?? this.options().maxItems);
1366
+ if (crumbs.length <= limit) {
1367
+ return { head: crumbs, hidden: [], tail: [] };
1368
+ }
1369
+ const tailCount = limit - 1;
1370
+ return {
1371
+ head: crumbs.slice(0, 1),
1372
+ hidden: crumbs.slice(1, crumbs.length - tailCount),
1373
+ tail: crumbs.slice(crumbs.length - tailCount),
1374
+ };
1375
+ }, ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
1376
+ /** Target of the "‹ Parent" link: the level just above the current page. */
1377
+ parent = computed(() => {
1378
+ const crumbs = this.crumbs();
1379
+ const current = crumbs.findIndex((crumb) => crumb.current);
1380
+ if (current === -1) {
1381
+ return crumbs.at(-1) ?? null;
1382
+ }
1383
+ return current > 0 ? crumbs[current - 1] : null;
1384
+ }, ...(ngDevMode ? [{ debugName: "parent" }] : /* istanbul ignore next */ []));
1385
+ pending = computed(() => this.crumbs().some((crumb) => crumb.label === null), ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
1386
+ /** Fast labels never flash a skeleton: it only shows after `skeletonDelay`. */
1387
+ delayElapsed = signal(false, ...(ngDevMode ? [{ debugName: "delayElapsed" }] : /* istanbul ignore next */ []));
1388
+ skeletonShown = computed(() => this.pending() && (this.options().skeletonDelay <= 0 || this.delayElapsed()), ...(ngDevMode ? [{ debugName: "skeletonShown" }] : /* istanbul ignore next */ []));
1389
+ constructor() {
1390
+ effect((onCleanup) => {
1391
+ if (!this.pending()) {
1392
+ this.delayElapsed.set(false);
1393
+ return;
1394
+ }
1395
+ const timer = setTimeout(() => this.delayElapsed.set(true), this.options().skeletonDelay);
1396
+ onCleanup(() => clearTimeout(timer));
1397
+ });
1398
+ }
1399
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellBreadcrumbComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1400
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: ShellBreadcrumbComponent, isStandalone: true, selector: "shell-breadcrumb", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, maxItems: { classPropertyName: "maxItems", publicName: "maxItems", isSignal: true, isRequired: false, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-display": "mode()", "attr.data-separator": "separator()" } }, ngImport: i0, template: "<nav class=\"crumbs\" [attr.aria-label]=\"labels().breadcrumb\">\n @if (mode() === 'back') {\n @if (parent(); as target) {\n <a\n class=\"crumbs__back\"\n [routerLink]=\"target.url\"\n [attr.aria-label]=\"labels().back + ' ' + (target.label ?? '')\"\n >\n <span class=\"crumbs__back-glyph\" aria-hidden=\"true\"></span>\n <ng-container *ngTemplateOutlet=\"labelTemplate; context: { $implicit: target }\" />\n </a>\n } @else if (crumbs()[0]; as only) {\n <span class=\"crumbs__current\" aria-current=\"page\">\n <ng-container *ngTemplateOutlet=\"labelTemplate; context: { $implicit: only }\" />\n </span>\n }\n } @else {\n @let trail = view();\n <ol class=\"crumbs__list\" [attr.aria-busy]=\"pending() ? 'true' : null\">\n @for (crumb of trail.head; track crumb.url) {\n <li class=\"crumbs__item\">\n <ng-container *ngTemplateOutlet=\"crumbTemplate; context: { $implicit: crumb }\" />\n </li>\n }\n\n @if (trail.hidden.length) {\n <li class=\"crumbs__item crumbs__item--more\">\n <shell-overflow-menu class=\"crumbs__more\" align=\"start\" [label]=\"labels().showPath\">\n @for (crumb of trail.hidden; track crumb.url) {\n <a class=\"crumbs__menu-link\" [routerLink]=\"crumb.url\">{{ crumb.label }}</a>\n }\n </shell-overflow-menu>\n </li>\n }\n\n @for (crumb of trail.tail; track crumb.url) {\n <li class=\"crumbs__item\">\n <ng-container *ngTemplateOutlet=\"crumbTemplate; context: { $implicit: crumb }\" />\n </li>\n }\n </ol>\n }\n\n @if (pending()) {\n <span class=\"crumbs__sr\">{{ labels().loading }}</span>\n }\n</nav>\n\n<ng-template #crumbTemplate let-crumb>\n @if (crumb.current) {\n <span class=\"crumbs__current\" aria-current=\"page\">\n <ng-container *ngTemplateOutlet=\"labelTemplate; context: { $implicit: crumb }\" />\n </span>\n } @else {\n <a class=\"crumbs__link\" [routerLink]=\"crumb.url\">\n <ng-container *ngTemplateOutlet=\"labelTemplate; context: { $implicit: crumb }\" />\n </a>\n }\n</ng-template>\n\n<!--\n A pending label is a placeholder: it reserves its width from the start, and\n only becomes a visible skeleton once `skeletonDelay` has passed.\n-->\n<ng-template #labelTemplate let-crumb>\n @if (crumb.label === null) {\n <span class=\"crumbs__skeleton\" [class.crumbs__skeleton--shown]=\"skeletonShown()\" aria-hidden=\"true\"></span>\n } @else if (itemTemplate(); as custom) {\n <ng-container *ngTemplateOutlet=\"custom; context: { $implicit: crumb, crumb: crumb }\" />\n } @else {\n <span class=\"crumbs__label\">{{ crumb.label }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;min-inline-size:0;font-size:var(--shell-breadcrumb-font-size, 1em)}.crumbs{display:flex;align-items:center;min-inline-size:0}.crumbs__list{display:flex;align-items:center;gap:var(--shell-breadcrumb-gap, 4px);min-inline-size:0;margin:0;padding:0;list-style:none;white-space:nowrap}.crumbs__item{display:flex;align-items:center;gap:var(--shell-breadcrumb-gap, 4px);flex:0 3 auto;min-inline-size:0}.crumbs__item:last-child{flex-shrink:1}.crumbs__item+.crumbs__item:before{content:\"\";flex:0 0 auto;inline-size:5px;block-size:5px;margin-inline:2px 3px;border-block-start:1.5px solid var(--shell-breadcrumb-separator-color, var(--shell-fg-muted, #6b6f76));border-right:1.5px solid var(--shell-breadcrumb-separator-color, var(--shell-fg-muted, #6b6f76));transform:rotate(45deg)}:host-context([dir=rtl]) .crumbs__item+.crumbs__item:before{transform:rotate(-135deg)}:host([data-separator=slash]) .crumbs__item+.crumbs__item:before,:host([data-separator=dot]) .crumbs__item+.crumbs__item:before{inline-size:auto;block-size:auto;margin-inline:1px 2px;border:0;color:var(--shell-breadcrumb-separator-color, var(--shell-fg-muted, #6b6f76));line-height:1;transform:none}:host([data-separator=slash]) .crumbs__item+.crumbs__item:before{content:\"/\" / \"\"}:host([data-separator=dot]) .crumbs__item+.crumbs__item:before{content:\"\\b7\" / \"\";font-weight:700}.crumbs__link,.crumbs__current,.crumbs__back{display:inline-flex;align-items:center;gap:6px;min-inline-size:0;max-inline-size:var(--shell-breadcrumb-label-max, 28ch);padding:2px 4px;border-radius:var(--shell-radius-sm, 6px)}.crumbs__link,.crumbs__back{color:var(--shell-breadcrumb-fg, var(--shell-fg-muted, #6b6f76));text-decoration:none;transition-property:color,background-color;transition-duration:calc(var(--shell-subheader-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-subheader-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.crumbs__link,.crumbs__back{transition-duration:1ms}}.crumbs__link:focus-visible,.crumbs__back:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:0}.crumbs__link:hover,.crumbs__back:hover{color:var(--shell-breadcrumb-hover-fg, var(--shell-fg, #1b1c1f));background:var(--shell-breadcrumb-hover-bg, var(--shell-hover-surface, #f0f0f3))}.crumbs__current{color:var(--shell-breadcrumb-current-fg, var(--shell-fg, #1b1c1f));font-weight:var(--shell-breadcrumb-current-weight, 600)}.crumbs__label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.crumbs__back-glyph{flex:0 0 auto;inline-size:6px;block-size:6px;border-block-end:1.5px solid currentcolor;border-left:1.5px solid currentcolor;transform:rotate(45deg)}:host-context([dir=rtl]) .crumbs__back-glyph{transform:rotate(-135deg)}.crumbs__more{--shell-overflow-trigger-size: 26px}.crumbs__menu-link{display:block;padding:7px 10px;border-radius:var(--shell-radius-sm, 6px);color:var(--shell-fg, #1b1c1f);text-decoration:none;white-space:nowrap}.crumbs__menu-link:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:-2px}.crumbs__menu-link:hover{background:var(--shell-hover-surface, #f0f0f3)}.crumbs__skeleton{display:inline-block;inline-size:9ch;block-size:.85em;border-radius:999px;background:var(--shell-breadcrumb-skeleton-bg, var(--shell-active-surface, #e9e9ee));visibility:hidden}.crumbs__skeleton--shown{visibility:visible;animation:crumbs-pulse calc(1.6s * var(--shell-motion-scale, 1)) ease-in-out infinite}@media(prefers-reduced-motion:reduce){.crumbs__skeleton--shown{animation:none}}@keyframes crumbs-pulse{50%{opacity:.45}}.crumbs__sr{position:absolute;inline-size:1px;block-size:1px;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\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"] }, { kind: "component", type: ShellOverflowMenuComponent, selector: "shell-overflow-menu", inputs: ["label", "align"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1401
+ }
1402
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellBreadcrumbComponent, decorators: [{
1403
+ type: Component,
1404
+ args: [{ selector: 'shell-breadcrumb', imports: [NgTemplateOutlet, RouterLink, ShellOverflowMenuComponent], host: {
1405
+ '[attr.data-display]': 'mode()',
1406
+ '[attr.data-separator]': 'separator()',
1407
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<nav class=\"crumbs\" [attr.aria-label]=\"labels().breadcrumb\">\n @if (mode() === 'back') {\n @if (parent(); as target) {\n <a\n class=\"crumbs__back\"\n [routerLink]=\"target.url\"\n [attr.aria-label]=\"labels().back + ' ' + (target.label ?? '')\"\n >\n <span class=\"crumbs__back-glyph\" aria-hidden=\"true\"></span>\n <ng-container *ngTemplateOutlet=\"labelTemplate; context: { $implicit: target }\" />\n </a>\n } @else if (crumbs()[0]; as only) {\n <span class=\"crumbs__current\" aria-current=\"page\">\n <ng-container *ngTemplateOutlet=\"labelTemplate; context: { $implicit: only }\" />\n </span>\n }\n } @else {\n @let trail = view();\n <ol class=\"crumbs__list\" [attr.aria-busy]=\"pending() ? 'true' : null\">\n @for (crumb of trail.head; track crumb.url) {\n <li class=\"crumbs__item\">\n <ng-container *ngTemplateOutlet=\"crumbTemplate; context: { $implicit: crumb }\" />\n </li>\n }\n\n @if (trail.hidden.length) {\n <li class=\"crumbs__item crumbs__item--more\">\n <shell-overflow-menu class=\"crumbs__more\" align=\"start\" [label]=\"labels().showPath\">\n @for (crumb of trail.hidden; track crumb.url) {\n <a class=\"crumbs__menu-link\" [routerLink]=\"crumb.url\">{{ crumb.label }}</a>\n }\n </shell-overflow-menu>\n </li>\n }\n\n @for (crumb of trail.tail; track crumb.url) {\n <li class=\"crumbs__item\">\n <ng-container *ngTemplateOutlet=\"crumbTemplate; context: { $implicit: crumb }\" />\n </li>\n }\n </ol>\n }\n\n @if (pending()) {\n <span class=\"crumbs__sr\">{{ labels().loading }}</span>\n }\n</nav>\n\n<ng-template #crumbTemplate let-crumb>\n @if (crumb.current) {\n <span class=\"crumbs__current\" aria-current=\"page\">\n <ng-container *ngTemplateOutlet=\"labelTemplate; context: { $implicit: crumb }\" />\n </span>\n } @else {\n <a class=\"crumbs__link\" [routerLink]=\"crumb.url\">\n <ng-container *ngTemplateOutlet=\"labelTemplate; context: { $implicit: crumb }\" />\n </a>\n }\n</ng-template>\n\n<!--\n A pending label is a placeholder: it reserves its width from the start, and\n only becomes a visible skeleton once `skeletonDelay` has passed.\n-->\n<ng-template #labelTemplate let-crumb>\n @if (crumb.label === null) {\n <span class=\"crumbs__skeleton\" [class.crumbs__skeleton--shown]=\"skeletonShown()\" aria-hidden=\"true\"></span>\n } @else if (itemTemplate(); as custom) {\n <ng-container *ngTemplateOutlet=\"custom; context: { $implicit: crumb, crumb: crumb }\" />\n } @else {\n <span class=\"crumbs__label\">{{ crumb.label }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;min-inline-size:0;font-size:var(--shell-breadcrumb-font-size, 1em)}.crumbs{display:flex;align-items:center;min-inline-size:0}.crumbs__list{display:flex;align-items:center;gap:var(--shell-breadcrumb-gap, 4px);min-inline-size:0;margin:0;padding:0;list-style:none;white-space:nowrap}.crumbs__item{display:flex;align-items:center;gap:var(--shell-breadcrumb-gap, 4px);flex:0 3 auto;min-inline-size:0}.crumbs__item:last-child{flex-shrink:1}.crumbs__item+.crumbs__item:before{content:\"\";flex:0 0 auto;inline-size:5px;block-size:5px;margin-inline:2px 3px;border-block-start:1.5px solid var(--shell-breadcrumb-separator-color, var(--shell-fg-muted, #6b6f76));border-right:1.5px solid var(--shell-breadcrumb-separator-color, var(--shell-fg-muted, #6b6f76));transform:rotate(45deg)}:host-context([dir=rtl]) .crumbs__item+.crumbs__item:before{transform:rotate(-135deg)}:host([data-separator=slash]) .crumbs__item+.crumbs__item:before,:host([data-separator=dot]) .crumbs__item+.crumbs__item:before{inline-size:auto;block-size:auto;margin-inline:1px 2px;border:0;color:var(--shell-breadcrumb-separator-color, var(--shell-fg-muted, #6b6f76));line-height:1;transform:none}:host([data-separator=slash]) .crumbs__item+.crumbs__item:before{content:\"/\" / \"\"}:host([data-separator=dot]) .crumbs__item+.crumbs__item:before{content:\"\\b7\" / \"\";font-weight:700}.crumbs__link,.crumbs__current,.crumbs__back{display:inline-flex;align-items:center;gap:6px;min-inline-size:0;max-inline-size:var(--shell-breadcrumb-label-max, 28ch);padding:2px 4px;border-radius:var(--shell-radius-sm, 6px)}.crumbs__link,.crumbs__back{color:var(--shell-breadcrumb-fg, var(--shell-fg-muted, #6b6f76));text-decoration:none;transition-property:color,background-color;transition-duration:calc(var(--shell-subheader-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-subheader-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.crumbs__link,.crumbs__back{transition-duration:1ms}}.crumbs__link:focus-visible,.crumbs__back:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:0}.crumbs__link:hover,.crumbs__back:hover{color:var(--shell-breadcrumb-hover-fg, var(--shell-fg, #1b1c1f));background:var(--shell-breadcrumb-hover-bg, var(--shell-hover-surface, #f0f0f3))}.crumbs__current{color:var(--shell-breadcrumb-current-fg, var(--shell-fg, #1b1c1f));font-weight:var(--shell-breadcrumb-current-weight, 600)}.crumbs__label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.crumbs__back-glyph{flex:0 0 auto;inline-size:6px;block-size:6px;border-block-end:1.5px solid currentcolor;border-left:1.5px solid currentcolor;transform:rotate(45deg)}:host-context([dir=rtl]) .crumbs__back-glyph{transform:rotate(-135deg)}.crumbs__more{--shell-overflow-trigger-size: 26px}.crumbs__menu-link{display:block;padding:7px 10px;border-radius:var(--shell-radius-sm, 6px);color:var(--shell-fg, #1b1c1f);text-decoration:none;white-space:nowrap}.crumbs__menu-link:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:-2px}.crumbs__menu-link:hover{background:var(--shell-hover-surface, #f0f0f3)}.crumbs__skeleton{display:inline-block;inline-size:9ch;block-size:.85em;border-radius:999px;background:var(--shell-breadcrumb-skeleton-bg, var(--shell-active-surface, #e9e9ee));visibility:hidden}.crumbs__skeleton--shown{visibility:visible;animation:crumbs-pulse calc(1.6s * var(--shell-motion-scale, 1)) ease-in-out infinite}@media(prefers-reduced-motion:reduce){.crumbs__skeleton--shown{animation:none}}@keyframes crumbs-pulse{50%{opacity:.45}}.crumbs__sr{position:absolute;inline-size:1px;block-size:1px;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"] }]
1408
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], maxItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxItems", required: false }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }] } });
1409
+
1410
+ /**
1411
+ * The breadcrumb row, between the topbar and `main`.
1412
+ *
1413
+ * [breadcrumb | built-in trail] [subheader-start] ········ [subheader-end] [⋯]
1414
+ *
1415
+ * A measured region like the topbar: `subheader-end` templates use
1416
+ * `slotMinSize` / `slotOverflow` against the row's own width and move into
1417
+ * the row's ⋯ menu when they don't fit. Its minimum height is reserved, so
1418
+ * labels and actions that arrive late never shift the page.
1419
+ */
1420
+ class ShellSubheaderComponent {
1421
+ region = measureRegion(inject(ElementRef));
1422
+ registry = inject(ShellSlotRegistry);
1423
+ config = inject(ShellStore).config;
1424
+ regionName = 'subheader';
1425
+ regionWidth = this.region.width;
1426
+ regionSize = this.region.size;
1427
+ /** An app-provided trail replaces the built-in one. */
1428
+ customTrail = hasSlot('breadcrumb');
1429
+ overflowSlots = computed(() => this.config().subheader.overflowSlots, ...(ngDevMode ? [{ debugName: "overflowSlots" }] : /* istanbul ignore next */ []));
1430
+ hasOverflow = computed(() => this.overflowSlots().some((name) => this.registry.overflowing(name, this.regionSize()).length > 0), ...(ngDevMode ? [{ debugName: "hasOverflow" }] : /* istanbul ignore next */ []));
1431
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSubheaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1432
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: ShellSubheaderComponent, isStandalone: true, selector: "shell-subheader", host: { properties: { "attr.data-shell-size": "regionSize()", "attr.data-variant": "config().appearance.subheader" } }, providers: [{ provide: SHELL_REGION, useExisting: ShellSubheaderComponent }], ngImport: i0, template: "<div class=\"subheader__start\">\n @if (customTrail()) {\n <shell-slot-outlet name=\"breadcrumb\" class=\"subheader__slot subheader__trail\" />\n } @else {\n <shell-breadcrumb class=\"subheader__trail\" />\n }\n <shell-slot-outlet name=\"subheader-start\" class=\"subheader__slot\" />\n</div>\n\n<div class=\"subheader__end\">\n <shell-slot-outlet name=\"subheader-end\" class=\"subheader__slot\" />\n\n @if (hasOverflow()) {\n <shell-overflow-menu [label]=\"config().labels.moreActions\">\n @for (name of overflowSlots(); track name) {\n <shell-slot-outlet [name]=\"name\" select=\"overflow\" class=\"subheader__overflow-item\" />\n }\n </shell-overflow-menu>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{container:shell-subheader/inline-size;display:flex;align-items:center;gap:8px 16px;box-sizing:border-box;min-block-size:var(--shell-subheader-height, 44px);margin:var(--shell-subheader-margin, 0);padding-inline:var(--shell-subheader-padding-inline, 24px);border-width:var(--shell-subheader-border-width, 0 0 1px);border-style:var(--shell-subheader-border-style, solid);border-color:var(--shell-subheader-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-subheader-radius, 0px);background:var(--shell-subheader-bg, var(--shell-surface, #ffffff));color:var(--shell-subheader-fg, var(--shell-fg, #1b1c1f));box-shadow:var(--shell-subheader-shadow, none);font-size:var(--shell-subheader-font-size, var(--shell-font-size, 14px));transition-property:background-color,border-color,box-shadow;transition-duration:calc(var(--shell-subheader-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-subheader-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){:host{transition-duration:1ms}}:host([data-variant=transparent]){border-color:var(--shell-subheader-border-color, transparent);background:var(--shell-subheader-bg, transparent)}:host([data-variant=elevated]){border-color:var(--shell-subheader-border-color, transparent);box-shadow:var(--shell-subheader-shadow, 0 1px 2px rgba(0, 0, 0, .04), 0 6px 14px -8px rgba(0, 0, 0, .14))}.subheader__start{display:flex;align-items:center;gap:12px;flex:1 1 auto;min-inline-size:0}.subheader__trail{flex:1 1 auto;min-inline-size:0}.subheader__slot{gap:8px}.subheader__end{display:flex;align-items:center;gap:8px;flex:0 0 auto}.subheader__overflow-item{flex-direction:column;align-items:stretch;gap:4px}\n"], dependencies: [{ kind: "component", type: ShellSlotOutletComponent, selector: "shell-slot-outlet", inputs: ["name", "select", "size"] }, { kind: "component", type: ShellBreadcrumbComponent, selector: "shell-breadcrumb", inputs: ["items", "maxItems", "display", "itemTemplate"] }, { kind: "component", type: ShellOverflowMenuComponent, selector: "shell-overflow-menu", inputs: ["label", "align"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1433
+ }
1434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellSubheaderComponent, decorators: [{
1435
+ type: Component,
1436
+ args: [{ selector: 'shell-subheader', imports: [ShellSlotOutletComponent, ShellBreadcrumbComponent, ShellOverflowMenuComponent], host: {
1437
+ '[attr.data-shell-size]': 'regionSize()',
1438
+ '[attr.data-variant]': 'config().appearance.subheader',
1439
+ }, providers: [{ provide: SHELL_REGION, useExisting: ShellSubheaderComponent }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"subheader__start\">\n @if (customTrail()) {\n <shell-slot-outlet name=\"breadcrumb\" class=\"subheader__slot subheader__trail\" />\n } @else {\n <shell-breadcrumb class=\"subheader__trail\" />\n }\n <shell-slot-outlet name=\"subheader-start\" class=\"subheader__slot\" />\n</div>\n\n<div class=\"subheader__end\">\n <shell-slot-outlet name=\"subheader-end\" class=\"subheader__slot\" />\n\n @if (hasOverflow()) {\n <shell-overflow-menu [label]=\"config().labels.moreActions\">\n @for (name of overflowSlots(); track name) {\n <shell-slot-outlet [name]=\"name\" select=\"overflow\" class=\"subheader__overflow-item\" />\n }\n </shell-overflow-menu>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{container:shell-subheader/inline-size;display:flex;align-items:center;gap:8px 16px;box-sizing:border-box;min-block-size:var(--shell-subheader-height, 44px);margin:var(--shell-subheader-margin, 0);padding-inline:var(--shell-subheader-padding-inline, 24px);border-width:var(--shell-subheader-border-width, 0 0 1px);border-style:var(--shell-subheader-border-style, solid);border-color:var(--shell-subheader-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-subheader-radius, 0px);background:var(--shell-subheader-bg, var(--shell-surface, #ffffff));color:var(--shell-subheader-fg, var(--shell-fg, #1b1c1f));box-shadow:var(--shell-subheader-shadow, none);font-size:var(--shell-subheader-font-size, var(--shell-font-size, 14px));transition-property:background-color,border-color,box-shadow;transition-duration:calc(var(--shell-subheader-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-subheader-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){:host{transition-duration:1ms}}:host([data-variant=transparent]){border-color:var(--shell-subheader-border-color, transparent);background:var(--shell-subheader-bg, transparent)}:host([data-variant=elevated]){border-color:var(--shell-subheader-border-color, transparent);box-shadow:var(--shell-subheader-shadow, 0 1px 2px rgba(0, 0, 0, .04), 0 6px 14px -8px rgba(0, 0, 0, .14))}.subheader__start{display:flex;align-items:center;gap:12px;flex:1 1 auto;min-inline-size:0}.subheader__trail{flex:1 1 auto;min-inline-size:0}.subheader__slot{gap:8px}.subheader__end{display:flex;align-items:center;gap:8px;flex:0 0 auto}.subheader__overflow-item{flex-direction:column;align-items:stretch;gap:4px}\n"] }]
1440
+ }] });
1441
+
1442
+ /**
1443
+ * Topbar with priority-based slot collapsing.
1444
+ *
1445
+ * The bar measures **itself**, not the window. That matters: expanding the
1446
+ * sidebar from a rail to a full panel takes ~200px away from the topbar without
1447
+ * the window ever resizing, and the bar has to respond to that the same way it
1448
+ * responds to a phone.
1449
+ *
1450
+ * Slot order, start to end:
1451
+ *
1452
+ * [toggle] [topbar-start] [brand] [primary-nav] [title] ····· [search] [actions] [⋯] [account] [topbar-end]
1453
+ *
1454
+ * Each slot template declares the room it needs (`slotMinSize` / `slotMaxSize`)
1455
+ * and what happens when it does not get it (`slotOverflow` → move into the ⋯
1456
+ * menu, otherwise drop).
1457
+ */
1458
+ class ShellTopbarComponent {
1459
+ registry = inject(ShellSlotRegistry);
1460
+ region = measureRegion(inject(ElementRef));
1461
+ store = inject(ShellStore);
1462
+ config = this.store.config;
1463
+ regionName = 'topbar';
1464
+ regionWidth = this.region.width;
1465
+ regionSize = this.region.size;
1466
+ showToggle = computed(() => {
1467
+ switch (this.config().topbar.sidebarToggle) {
1468
+ case 'never':
1469
+ return false;
1470
+ case 'auto': {
1471
+ const mode = this.store.sidebar().mode;
1472
+ return mode === 'overlay' || mode === 'hidden';
1473
+ }
1474
+ default:
1475
+ return true;
1476
+ }
1477
+ }, ...(ngDevMode ? [{ debugName: "showToggle" }] : /* istanbul ignore next */ []));
1478
+ overflowSlots = computed(() => this.config().overflowSlots, ...(ngDevMode ? [{ debugName: "overflowSlots" }] : /* istanbul ignore next */ []));
1479
+ /** The ⋯ menu exists only while something is in it. */
1480
+ hasOverflow = computed(() => this.overflowSlots().some((name) => this.registry.overflowing(name, this.regionSize()).length > 0), ...(ngDevMode ? [{ debugName: "hasOverflow" }] : /* istanbul ignore next */ []));
1481
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellTopbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1482
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: ShellTopbarComponent, isStandalone: true, selector: "shell-topbar", host: { attributes: { "role": "banner" }, properties: { "attr.data-shell-size": "regionSize()", "attr.data-overflow": "hasOverflow()", "attr.data-variant": "config().appearance.topbar", "attr.data-menu-motion": "config().motion.overflowMenu" } }, providers: [{ provide: SHELL_REGION, useExisting: ShellTopbarComponent }], ngImport: i0, template: "<div class=\"topbar__group topbar__group--start\">\n @if (showToggle()) {\n <button shellSidebarToggle class=\"topbar__icon-button topbar__toggle\">\n <span class=\"topbar__toggle-glyph\" aria-hidden=\"true\"></span>\n </button>\n }\n\n <shell-slot-outlet name=\"topbar-start\" class=\"topbar__slot\" />\n <shell-slot-outlet name=\"brand\" class=\"topbar__slot topbar__slot--brand\" />\n <shell-slot-outlet name=\"primary-nav\" class=\"topbar__slot topbar__slot--nav\" />\n <shell-slot-outlet name=\"title\" class=\"topbar__slot topbar__slot--title\" />\n</div>\n\n<div class=\"topbar__group topbar__group--end\">\n <shell-slot-outlet name=\"search\" class=\"topbar__slot topbar__slot--search\" />\n <shell-slot-outlet name=\"actions\" class=\"topbar__slot\" />\n\n @if (hasOverflow()) {\n <shell-overflow-menu class=\"topbar__overflow\" [label]=\"config().labels.moreActions\">\n @for (name of overflowSlots(); track name) {\n <shell-slot-outlet [name]=\"name\" select=\"overflow\" class=\"topbar__overflow-item\" />\n }\n </shell-overflow-menu>\n }\n\n <shell-slot-outlet name=\"account\" class=\"topbar__slot\" />\n <shell-slot-outlet name=\"topbar-end\" class=\"topbar__slot\" />\n</div>\n", styles: ["@charset \"UTF-8\";:host{container:shell-topbar/inline-size;display:flex;align-items:center;gap:var(--shell-topbar-gap, 8px);block-size:var(--shell-topbar-height, 64px);padding-inline:var(--shell-topbar-padding-inline, 12px);border-width:var(--shell-topbar-border-width, 0 0 1px);border-style:var(--shell-topbar-border-style, solid);border-color:var(--shell-topbar-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-topbar-radius, 0px);background:var(--shell-topbar-bg, var(--shell-topbar-surface, #ffffff));color:var(--shell-topbar-fg, var(--shell-fg, #1b1c1f));box-shadow:var(--shell-topbar-shadow, none);transition-property:background-color,border-color,box-shadow;transition-duration:calc(var(--shell-topbar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-topbar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){:host{transition-duration:1ms}}:host([data-variant=elevated]){border-color:var(--shell-topbar-border-color, transparent);box-shadow:var(--shell-topbar-shadow, 0 1px 2px rgba(0, 0, 0, .05), 0 6px 16px -8px rgba(0, 0, 0, .18))}:host([data-variant=blur]){border-color:var(--shell-topbar-border-color, color-mix(in srgb, var(--shell-border, #e6e6e9) 60%, transparent));background:var(--shell-topbar-bg, color-mix(in srgb, var(--shell-topbar-surface, #ffffff) 72%, transparent));-webkit-backdrop-filter:blur(var(--shell-topbar-blur, 12px)) saturate(1.5);backdrop-filter:blur(var(--shell-topbar-blur, 12px)) saturate(1.5)}:host([data-variant=transparent]){border-color:var(--shell-topbar-border-color, transparent);background:var(--shell-topbar-bg, transparent)}.topbar__group{display:flex;align-items:center;gap:var(--shell-topbar-gap, 8px);min-inline-size:0}.topbar__group--start{flex:1 1 auto}.topbar__group--end{flex:0 1 auto;margin-inline-start:auto}.topbar__slot{flex:0 0 auto;gap:var(--shell-topbar-gap, 8px)}.topbar__slot--nav,.topbar__slot--title{flex:0 1 auto;min-inline-size:0;overflow:hidden}.topbar__slot--title{font-weight:600;white-space:nowrap}.topbar__slot--search{flex:0 1 clamp(40px,34cqi,420px);min-inline-size:0}.topbar__icon-button{display:grid;place-items:center;flex:0 0 auto;inline-size:38px;block-size:38px;padding:0;border:0;border-radius:var(--shell-radius-sm, 6px);background:transparent;color:inherit;cursor:pointer;transition-property:background-color;transition-duration:calc(var(--shell-topbar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-topbar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.topbar__icon-button{transition-duration:1ms}}.topbar__icon-button:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:2px}.topbar__icon-button:hover{background:var(--shell-hover-surface, #f0f0f3)}.topbar__toggle-glyph{position:relative;inline-size:16px;block-size:2px;border-radius:2px;background:currentcolor}.topbar__toggle-glyph:before,.topbar__toggle-glyph:after{content:\"\";position:absolute;inset-inline-start:0;inline-size:16px;block-size:2px;border-radius:2px;background:currentcolor;transition-property:inline-size;transition-duration:calc(var(--shell-collapse-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-collapse-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.topbar__toggle-glyph:before,.topbar__toggle-glyph:after{transition-duration:1ms}}.topbar__toggle-glyph:before{inset-block-start:-5px}.topbar__toggle-glyph:after{inset-block-start:5px}:host-context(app-shell[data-sidebar-mode=expanded]) .topbar__toggle-glyph:before,:host-context(app-shell[data-sidebar-mode=expanded]) .topbar__toggle-glyph:after{inline-size:10px}.topbar__overflow-item{flex-direction:column;align-items:stretch;gap:4px}\n"], dependencies: [{ kind: "component", type: ShellSlotOutletComponent, selector: "shell-slot-outlet", inputs: ["name", "select", "size"] }, { kind: "directive", type: SidebarToggleDirective, selector: "button[shellSidebarToggle]", exportAs: ["shellSidebarToggle"] }, { kind: "component", type: ShellOverflowMenuComponent, selector: "shell-overflow-menu", inputs: ["label", "align"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1483
+ }
1484
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellTopbarComponent, decorators: [{
1485
+ type: Component,
1486
+ args: [{ selector: 'shell-topbar', imports: [ShellSlotOutletComponent, SidebarToggleDirective, ShellOverflowMenuComponent], host: {
1487
+ role: 'banner',
1488
+ '[attr.data-shell-size]': 'regionSize()',
1489
+ '[attr.data-overflow]': 'hasOverflow()',
1490
+ '[attr.data-variant]': 'config().appearance.topbar',
1491
+ '[attr.data-menu-motion]': 'config().motion.overflowMenu',
1492
+ }, providers: [{ provide: SHELL_REGION, useExisting: ShellTopbarComponent }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"topbar__group topbar__group--start\">\n @if (showToggle()) {\n <button shellSidebarToggle class=\"topbar__icon-button topbar__toggle\">\n <span class=\"topbar__toggle-glyph\" aria-hidden=\"true\"></span>\n </button>\n }\n\n <shell-slot-outlet name=\"topbar-start\" class=\"topbar__slot\" />\n <shell-slot-outlet name=\"brand\" class=\"topbar__slot topbar__slot--brand\" />\n <shell-slot-outlet name=\"primary-nav\" class=\"topbar__slot topbar__slot--nav\" />\n <shell-slot-outlet name=\"title\" class=\"topbar__slot topbar__slot--title\" />\n</div>\n\n<div class=\"topbar__group topbar__group--end\">\n <shell-slot-outlet name=\"search\" class=\"topbar__slot topbar__slot--search\" />\n <shell-slot-outlet name=\"actions\" class=\"topbar__slot\" />\n\n @if (hasOverflow()) {\n <shell-overflow-menu class=\"topbar__overflow\" [label]=\"config().labels.moreActions\">\n @for (name of overflowSlots(); track name) {\n <shell-slot-outlet [name]=\"name\" select=\"overflow\" class=\"topbar__overflow-item\" />\n }\n </shell-overflow-menu>\n }\n\n <shell-slot-outlet name=\"account\" class=\"topbar__slot\" />\n <shell-slot-outlet name=\"topbar-end\" class=\"topbar__slot\" />\n</div>\n", styles: ["@charset \"UTF-8\";:host{container:shell-topbar/inline-size;display:flex;align-items:center;gap:var(--shell-topbar-gap, 8px);block-size:var(--shell-topbar-height, 64px);padding-inline:var(--shell-topbar-padding-inline, 12px);border-width:var(--shell-topbar-border-width, 0 0 1px);border-style:var(--shell-topbar-border-style, solid);border-color:var(--shell-topbar-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-topbar-radius, 0px);background:var(--shell-topbar-bg, var(--shell-topbar-surface, #ffffff));color:var(--shell-topbar-fg, var(--shell-fg, #1b1c1f));box-shadow:var(--shell-topbar-shadow, none);transition-property:background-color,border-color,box-shadow;transition-duration:calc(var(--shell-topbar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-topbar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){:host{transition-duration:1ms}}:host([data-variant=elevated]){border-color:var(--shell-topbar-border-color, transparent);box-shadow:var(--shell-topbar-shadow, 0 1px 2px rgba(0, 0, 0, .05), 0 6px 16px -8px rgba(0, 0, 0, .18))}:host([data-variant=blur]){border-color:var(--shell-topbar-border-color, color-mix(in srgb, var(--shell-border, #e6e6e9) 60%, transparent));background:var(--shell-topbar-bg, color-mix(in srgb, var(--shell-topbar-surface, #ffffff) 72%, transparent));-webkit-backdrop-filter:blur(var(--shell-topbar-blur, 12px)) saturate(1.5);backdrop-filter:blur(var(--shell-topbar-blur, 12px)) saturate(1.5)}:host([data-variant=transparent]){border-color:var(--shell-topbar-border-color, transparent);background:var(--shell-topbar-bg, transparent)}.topbar__group{display:flex;align-items:center;gap:var(--shell-topbar-gap, 8px);min-inline-size:0}.topbar__group--start{flex:1 1 auto}.topbar__group--end{flex:0 1 auto;margin-inline-start:auto}.topbar__slot{flex:0 0 auto;gap:var(--shell-topbar-gap, 8px)}.topbar__slot--nav,.topbar__slot--title{flex:0 1 auto;min-inline-size:0;overflow:hidden}.topbar__slot--title{font-weight:600;white-space:nowrap}.topbar__slot--search{flex:0 1 clamp(40px,34cqi,420px);min-inline-size:0}.topbar__icon-button{display:grid;place-items:center;flex:0 0 auto;inline-size:38px;block-size:38px;padding:0;border:0;border-radius:var(--shell-radius-sm, 6px);background:transparent;color:inherit;cursor:pointer;transition-property:background-color;transition-duration:calc(var(--shell-topbar-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-topbar-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.topbar__icon-button{transition-duration:1ms}}.topbar__icon-button:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:2px}.topbar__icon-button:hover{background:var(--shell-hover-surface, #f0f0f3)}.topbar__toggle-glyph{position:relative;inline-size:16px;block-size:2px;border-radius:2px;background:currentcolor}.topbar__toggle-glyph:before,.topbar__toggle-glyph:after{content:\"\";position:absolute;inset-inline-start:0;inline-size:16px;block-size:2px;border-radius:2px;background:currentcolor;transition-property:inline-size;transition-duration:calc(var(--shell-collapse-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-collapse-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.topbar__toggle-glyph:before,.topbar__toggle-glyph:after{transition-duration:1ms}}.topbar__toggle-glyph:before{inset-block-start:-5px}.topbar__toggle-glyph:after{inset-block-start:5px}:host-context(app-shell[data-sidebar-mode=expanded]) .topbar__toggle-glyph:before,:host-context(app-shell[data-sidebar-mode=expanded]) .topbar__toggle-glyph:after{inline-size:10px}.topbar__overflow-item{flex-direction:column;align-items:stretch;gap:4px}\n"] }]
1493
+ }] });
1494
+
1495
+ /** `undefined` (attribute absent) means "use the config"; otherwise a boolean attribute. */
1496
+ function optionalBoolean(value) {
1497
+ return value === undefined || value === null ? undefined : booleanAttribute(value);
1498
+ }
1499
+ function isEditable(target) {
1500
+ if (!(target instanceof HTMLElement)) {
1501
+ return false;
1502
+ }
1503
+ return target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName);
1504
+ }
1505
+ /**
1506
+ * The navigational shell: sidebar + topbar + breadcrumb row + main + footer.
1507
+ *
1508
+ * Layout is a CSS grid whose tracks come from custom properties, so a host app
1509
+ * restyles it entirely from CSS. The only value TypeScript writes is the
1510
+ * current sidebar track width, which is derived state, not layout maths.
1511
+ */
1512
+ class AppShellComponent {
1513
+ host = inject(ElementRef);
1514
+ document = inject(DOCUMENT);
1515
+ router = inject(Router, { optional: true });
1516
+ breadcrumbs = inject(ShellBreadcrumbStore);
1517
+ main = viewChild.required('main');
1518
+ frame = viewChild.required('frame');
1519
+ store = inject(ShellStore);
1520
+ config = this.store.config;
1521
+ mainId = SHELL_MAIN_ID;
1522
+ sidebarId = SHELL_SIDEBAR_ID;
1523
+ /** Per-instance overrides. Leave unset to follow the (runtime) config. */
1524
+ layout = input(undefined, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
1525
+ scroll = input(undefined, ...(ngDevMode ? [{ debugName: "scroll" }] : /* istanbul ignore next */ []));
1526
+ topbar = input(undefined, { ...(ngDevMode ? { debugName: "topbar" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
1527
+ subheader = input(undefined, { ...(ngDevMode ? { debugName: "subheader" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
1528
+ footer = input(undefined, { ...(ngDevMode ? { debugName: "footer" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
1529
+ regionName = 'shell';
1530
+ regionWidth = observeWidth(this.host);
1531
+ regionSize = this.store.size;
1532
+ size = this.store.size;
1533
+ sidebar = this.store.sidebar;
1534
+ hasSubheaderSlots = hasSlot('breadcrumb', 'subheader-start', 'subheader-end');
1535
+ hasFooterSlots = hasSlot('footer', 'footer-start', 'footer-end');
1536
+ resolvedLayout = computed(() => this.layout() ?? this.config().layout, ...(ngDevMode ? [{ debugName: "resolvedLayout" }] : /* istanbul ignore next */ []));
1537
+ resolvedScroll = computed(() => this.scroll() ?? this.config().scroll, ...(ngDevMode ? [{ debugName: "resolvedScroll" }] : /* istanbul ignore next */ []));
1538
+ /** Topbar, main and footer share one rounded card; the sidebar sits on the shell. */
1539
+ inset = computed(() => this.resolvedLayout() === 'inset', ...(ngDevMode ? [{ debugName: "inset" }] : /* istanbul ignore next */ []));
1540
+ showTopbar = computed(() => this.topbar() ?? this.config().topbar.enabled, ...(ngDevMode ? [{ debugName: "showTopbar" }] : /* istanbul ignore next */ []));
1541
+ showFooter = computed(() => this.footer() ?? (this.config().footer.enabled || this.hasFooterSlots()), ...(ngDevMode ? [{ debugName: "showFooter" }] : /* istanbul ignore next */ []));
1542
+ /**
1543
+ * The breadcrumb row. In `'auto'` it follows the page: a route's
1544
+ * `data.subheader` wins; otherwise it shows when the trail is at least two
1545
+ * levels deep (a flat trail is noise) or when a page fills a subheader slot.
1546
+ * Decided per navigation, so it never appears or vanishes within a page.
1547
+ */
1548
+ showSubheader = computed(() => {
1549
+ const explicit = this.subheader();
1550
+ if (explicit !== undefined) {
1551
+ return explicit;
1552
+ }
1553
+ const mode = this.config().subheader.enabled;
1554
+ if (mode !== 'auto') {
1555
+ return mode;
1556
+ }
1557
+ return (this.breadcrumbs.subheaderOverride() ??
1558
+ (this.breadcrumbs.depth() >= 2 || this.hasSubheaderSlots()));
1559
+ }, ...(ngDevMode ? [{ debugName: "showSubheader" }] : /* istanbul ignore next */ []));
1560
+ /**
1561
+ * Factor every shell duration is multiplied by (see `duration()` in the
1562
+ * token file): 0 turns motion off, 1 / speed scales it.
1563
+ */
1564
+ motionScale = computed(() => {
1565
+ const { enabled, speed } = this.config().motion;
1566
+ return String(enabled && speed > 0 ? 1 / speed : 0);
1567
+ }, ...(ngDevMode ? [{ debugName: "motionScale" }] : /* istanbul ignore next */ []));
1568
+ constructor() {
1569
+ // Feed the measured width back into the store: everything downstream —
1570
+ // buckets, sidebar mode, slot visibility — is derived from this one value.
1571
+ effect(() => this.store.setWidth(this.regionWidth()));
1572
+ // A modal drawer must not leave the page scrolling underneath it. Done in
1573
+ // TS rather than a global class so the library needs no global stylesheet.
1574
+ effect((onCleanup) => {
1575
+ if (!this.sidebar().modal) {
1576
+ return;
1577
+ }
1578
+ const root = this.document.documentElement;
1579
+ const previous = root.style.overflow;
1580
+ root.style.overflow = 'hidden';
1581
+ onCleanup(() => {
1582
+ root.style.overflow = previous;
1583
+ });
1584
+ });
1585
+ this.router?.events
1586
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed())
1587
+ .subscribe(() => {
1588
+ const config = this.config();
1589
+ const state = this.store.sidebar();
1590
+ if (config.sidebar.closeOnNavigate && state.mode === 'overlay' && state.open) {
1591
+ this.store.closeSidebar();
1592
+ }
1593
+ if (config.scrollTopOnNavigate) {
1594
+ this.resetScroll();
1595
+ }
1596
+ });
1597
+ }
1598
+ /**
1599
+ * The window does not scroll in these modes, so the router cannot restore
1600
+ * scroll: `main` scrolls in `scroll: 'main'`, the card in `inset` + `page`.
1601
+ */
1602
+ resetScroll() {
1603
+ if (this.resolvedScroll() === 'main') {
1604
+ this.main().nativeElement.scrollTo({ top: 0 });
1605
+ }
1606
+ else if (this.inset()) {
1607
+ this.frame().nativeElement.scrollTo({ top: 0 });
1608
+ }
1609
+ }
1610
+ /**
1611
+ * A plain `href="#id"` would resolve against `<base href>` and trigger a
1612
+ * router navigation, so the jump is done by moving focus instead.
1613
+ */
1614
+ skipToMain(event) {
1615
+ event.preventDefault();
1616
+ this.document.getElementById(this.mainId)?.focus();
1617
+ }
1618
+ /** Optional Ctrl/⌘ + key toggle, ignored while typing. */
1619
+ onDocumentKeydown(event) {
1620
+ const key = this.config().sidebar.toggleShortcut;
1621
+ if (!key ||
1622
+ event.defaultPrevented ||
1623
+ event.altKey ||
1624
+ event.shiftKey ||
1625
+ !(event.ctrlKey || event.metaKey) ||
1626
+ event.key.toLowerCase() !== key.toLowerCase() ||
1627
+ isEditable(event.target)) {
1628
+ return;
1629
+ }
1630
+ event.preventDefault();
1631
+ this.store.toggleSidebar();
1632
+ }
1633
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: AppShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1634
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: AppShellComponent, isStandalone: true, selector: "app-shell", inputs: { layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, scroll: { classPropertyName: "scroll", publicName: "scroll", isSignal: true, isRequired: false, transformFunction: null }, topbar: { classPropertyName: "topbar", publicName: "topbar", isSignal: true, isRequired: false, transformFunction: null }, subheader: { classPropertyName: "subheader", publicName: "subheader", isSignal: true, isRequired: false, transformFunction: null }, footer: { classPropertyName: "footer", publicName: "footer", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:keydown": "onDocumentKeydown($event)" }, properties: { "attr.data-layout": "resolvedLayout()", "attr.data-scroll": "resolvedScroll()", "attr.data-topbar": "showTopbar()", "attr.data-subheader": "showSubheader()", "attr.data-shell-size": "size()", "attr.data-sidebar-mode": "sidebar().mode", "attr.data-sidebar-open": "sidebar().open", "attr.data-sidebar-modal": "sidebar().modal", "attr.data-sidebar-peeked": "sidebar().peeked", "attr.data-resizing": "store.resizing() || null", "attr.data-motion-collapse": "config().motion.collapse ? null : \"off\"", "style.--shell-sidebar-track": "sidebar().trackWidth", "style.--shell-motion-scale": "motionScale()" } }, providers: [{ provide: SHELL_REGION, useExisting: AppShellComponent }], viewQueries: [{ propertyName: "main", first: true, predicate: ["main"], descendants: true, isSignal: true }, { propertyName: "frame", first: true, predicate: ["frame"], descendants: true, isSignal: true }], exportAs: ["appShell"], ngImport: i0, template: "<a class=\"shell__skip-link\" [href]=\"'#' + mainId\" (click)=\"skipToMain($event)\">\n {{ config().labels.skipToContent }}\n</a>\n\n<!--\n Outside `inset` the topbar comes first in the DOM, so keyboard order follows\n the visual order (topbar \u2192 sidebar \u2192 breadcrumbs \u2192 main). In `inset` it moves\n into the card. Only one of the two ever renders.\n-->\n@if (showTopbar() && !inset()) {\n <shell-topbar class=\"shell__topbar\" />\n}\n\n<shell-sidebar class=\"shell__sidebar\" [id]=\"sidebarId\" [attr.data-layout]=\"resolvedLayout()\" />\n\n<!--\n The frame wraps topbar, breadcrumb row, main and footer. In `inset` it is the\n rounded card (and, with `scroll: 'page'`, the scroll container); in every\n other layout it is `display: contents`, so its children stay shell grid items.\n-->\n<div #frame class=\"shell__frame\">\n @if (showTopbar() && inset()) {\n <shell-topbar class=\"shell__topbar\" />\n }\n\n <!-- Before <main>, so the skip link jumps past the breadcrumbs too. -->\n @if (showSubheader()) {\n <shell-subheader class=\"shell__subheader\" [attr.data-sticky]=\"config().subheader.sticky || null\" />\n }\n\n <main #main class=\"shell__main\" [id]=\"mainId\" tabindex=\"-1\">\n <div class=\"shell__content\">\n <ng-content />\n </div>\n </main>\n\n @if (showFooter()) {\n <shell-footer class=\"shell__footer\" />\n }\n</div>\n\n<!--\n Scrim for the modal drawer. Always rendered so it can fade out as well as\n in; while closed it is invisible and ignores the pointer. It stacks above\n main content but below the sidebar, which is raised by --shell-z-sidebar.\n-->\n<div\n class=\"shell__scrim\"\n aria-hidden=\"true\"\n [attr.data-open]=\"sidebar().modal\"\n [attr.data-motion]=\"config().motion.scrim\"\n (click)=\"store.closeSidebar()\"\n></div>\n", styles: ["@charset \"UTF-8\";:host{--shell-sidebar-track: var(--shell-sidebar-width, 264px);--shell-sidebar-top-offset: var(--shell-topbar-height, 64px);--shell-subheader-top-offset: var(--shell-topbar-height, 64px);display:grid;grid-template-columns:var(--shell-sidebar-track) minmax(0,1fr);grid-template-rows:auto auto minmax(0,1fr) auto;grid-template-areas:\"topbar topbar\" \"sidebar subheader\" \"sidebar main\" \"sidebar footer\";isolation:isolate;color-scheme:var(--shell-color-scheme, light);color:var(--shell-fg, #1b1c1f);background:var(--shell-bg, var(--shell-surface, #ffffff));font-family:var(--shell-font-family, inherit);font-size:var(--shell-font-size, 14px);transition-property:grid-template-columns;transition-duration:calc(var(--shell-collapse-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-collapse-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){:host{transition-duration:1ms}}:host([data-motion-collapse=off]){--shell-collapse-duration: 0s}:host([data-layout=sidebar-full]){grid-template-areas:\"sidebar topbar\" \"sidebar subheader\" \"sidebar main\" \"sidebar footer\"}:host([data-layout=stacked]){grid-template-areas:\"topbar topbar\" \"sidebar subheader\" \"sidebar main\" \"footer footer\"}:host([data-layout=sidebar-full]),:host([data-topbar=false]){--shell-sidebar-top-offset: 0px}:host([data-topbar=false]){--shell-subheader-top-offset: 0px}:host([data-scroll=main]){--shell-sidebar-block-size: 100%;block-size:100dvh;overflow:clip}:host([data-scroll=page]){--shell-sidebar-block-size: calc(100dvh - var(--shell-sidebar-top-offset));min-block-size:100dvh}.shell__frame{display:contents}:host([data-layout=inset]){--shell-sidebar-top-offset: 0px;--shell-sidebar-block-size: 100%;grid-template-rows:minmax(0,1fr);grid-template-areas:\"sidebar frame\";block-size:100dvh;overflow:clip;background:var(--shell-bg, var(--shell-surface-raised, #f7f7f8))}:host([data-layout=inset]) .shell__frame{grid-area:frame;z-index:var(--shell-z-main, 0);display:grid;grid-template-rows:auto auto minmax(0,1fr) auto;grid-template-areas:\"topbar\" \"subheader\" \"main\" \"footer\";min-inline-size:0;margin:var(--shell-frame-inset, 8px);border-width:var(--shell-frame-border-width, 0);border-style:var(--shell-frame-border-style, solid);border-color:var(--shell-frame-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-frame-radius, 14px);background:var(--shell-frame-bg, var(--shell-surface, #ffffff));box-shadow:var(--shell-frame-shadow, 0 1px 2px rgba(0, 0, 0, .04), 0 8px 24px -8px rgba(0, 0, 0, .14));overflow:clip}:host([data-layout=inset][data-scroll=page]) .shell__frame{grid-template-rows:auto auto 1fr auto;overflow:auto;overscroll-behavior:contain}:host([data-resizing]){cursor:col-resize;-webkit-user-select:none;user-select:none;transition-duration:0s}.shell__topbar{grid-area:topbar;z-index:var(--shell-z-topbar, 20)}:host([data-scroll=page]) .shell__topbar{position:sticky;inset-block-start:0}.shell__subheader{grid-area:subheader;z-index:calc(var(--shell-z-topbar, 20) - 1)}:host([data-scroll=page]) .shell__subheader[data-sticky]{position:sticky;inset-block-start:var(--shell-subheader-top-offset)}.shell__sidebar{grid-area:sidebar;z-index:var(--shell-z-sidebar, 40)}.shell__main{position:relative;grid-area:main;min-inline-size:0;z-index:var(--shell-z-main, 0);margin:var(--shell-main-margin, 0);padding:var(--shell-main-padding, 24px);border-width:var(--shell-main-border-width, 0);border-style:var(--shell-main-border-style, solid);border-color:var(--shell-main-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-main-radius, 0px);background:var(--shell-main-bg, var(--shell-surface, #ffffff));color:var(--shell-main-fg, var(--shell-fg, #1b1c1f));box-shadow:var(--shell-main-shadow, none);container:shell-main/inline-size}.shell__main:focus{outline:none}:host([data-scroll=main]) .shell__main{overflow:auto;overscroll-behavior:contain}.shell__content{max-inline-size:var(--shell-main-max-width, none);margin-inline:auto}.shell__footer{grid-area:footer;z-index:var(--shell-z-topbar, 20)}.shell__scrim{position:fixed;inset:0;z-index:var(--shell-z-scrim, 30);background:var(--shell-drawer-scrim, var(--shell-scrim, rgba(15, 17, 21, .45)));-webkit-backdrop-filter:blur(var(--shell-drawer-scrim-blur, 0px));backdrop-filter:blur(var(--shell-drawer-scrim-blur, 0px));opacity:0;visibility:hidden;pointer-events:none;transition-property:opacity,visibility;transition-duration:calc(var(--shell-drawer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-drawer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.shell__scrim{transition-duration:1ms}}.shell__scrim[data-open=true]{opacity:1;visibility:visible;pointer-events:auto}.shell__scrim[data-motion=none]{transition-duration:0s}.shell__skip-link{position:fixed;inset-block-start:8px;inset-inline-start:8px;z-index:var(--shell-z-skip-link, 60);padding:8px 14px;border-radius:var(--shell-radius-sm, 6px);background:var(--shell-surface, #ffffff);color:var(--shell-fg, #1b1c1f);box-shadow:var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28));text-decoration:none;transform:translateY(calc(-100% - 16px));transition-property:transform;transition-duration:calc(var(--shell-transition-duration, .18s) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))}@media(prefers-reduced-motion:reduce){.shell__skip-link{transition-duration:1ms}}.shell__skip-link:focus-visible{transform:none;outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:2px}\n"], dependencies: [{ kind: "component", type: ShellTopbarComponent, selector: "shell-topbar" }, { kind: "component", type: ShellSidebarComponent, selector: "shell-sidebar", inputs: ["collapseControl"] }, { kind: "component", type: ShellSubheaderComponent, selector: "shell-subheader" }, { kind: "component", type: ShellFooterComponent, selector: "shell-footer" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1635
+ }
1636
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: AppShellComponent, decorators: [{
1637
+ type: Component,
1638
+ args: [{ selector: 'app-shell', imports: [ShellTopbarComponent, ShellSidebarComponent, ShellSubheaderComponent, ShellFooterComponent], host: {
1639
+ '[attr.data-layout]': 'resolvedLayout()',
1640
+ '[attr.data-scroll]': 'resolvedScroll()',
1641
+ '[attr.data-topbar]': 'showTopbar()',
1642
+ '[attr.data-subheader]': 'showSubheader()',
1643
+ '[attr.data-shell-size]': 'size()',
1644
+ '[attr.data-sidebar-mode]': 'sidebar().mode',
1645
+ '[attr.data-sidebar-open]': 'sidebar().open',
1646
+ '[attr.data-sidebar-modal]': 'sidebar().modal',
1647
+ '[attr.data-sidebar-peeked]': 'sidebar().peeked',
1648
+ '[attr.data-resizing]': 'store.resizing() || null',
1649
+ '[attr.data-motion-collapse]': 'config().motion.collapse ? null : "off"',
1650
+ '[style.--shell-sidebar-track]': 'sidebar().trackWidth',
1651
+ '[style.--shell-motion-scale]': 'motionScale()',
1652
+ '(document:keydown)': 'onDocumentKeydown($event)',
1653
+ }, providers: [{ provide: SHELL_REGION, useExisting: AppShellComponent }], exportAs: 'appShell', changeDetection: ChangeDetectionStrategy.OnPush, template: "<a class=\"shell__skip-link\" [href]=\"'#' + mainId\" (click)=\"skipToMain($event)\">\n {{ config().labels.skipToContent }}\n</a>\n\n<!--\n Outside `inset` the topbar comes first in the DOM, so keyboard order follows\n the visual order (topbar \u2192 sidebar \u2192 breadcrumbs \u2192 main). In `inset` it moves\n into the card. Only one of the two ever renders.\n-->\n@if (showTopbar() && !inset()) {\n <shell-topbar class=\"shell__topbar\" />\n}\n\n<shell-sidebar class=\"shell__sidebar\" [id]=\"sidebarId\" [attr.data-layout]=\"resolvedLayout()\" />\n\n<!--\n The frame wraps topbar, breadcrumb row, main and footer. In `inset` it is the\n rounded card (and, with `scroll: 'page'`, the scroll container); in every\n other layout it is `display: contents`, so its children stay shell grid items.\n-->\n<div #frame class=\"shell__frame\">\n @if (showTopbar() && inset()) {\n <shell-topbar class=\"shell__topbar\" />\n }\n\n <!-- Before <main>, so the skip link jumps past the breadcrumbs too. -->\n @if (showSubheader()) {\n <shell-subheader class=\"shell__subheader\" [attr.data-sticky]=\"config().subheader.sticky || null\" />\n }\n\n <main #main class=\"shell__main\" [id]=\"mainId\" tabindex=\"-1\">\n <div class=\"shell__content\">\n <ng-content />\n </div>\n </main>\n\n @if (showFooter()) {\n <shell-footer class=\"shell__footer\" />\n }\n</div>\n\n<!--\n Scrim for the modal drawer. Always rendered so it can fade out as well as\n in; while closed it is invisible and ignores the pointer. It stacks above\n main content but below the sidebar, which is raised by --shell-z-sidebar.\n-->\n<div\n class=\"shell__scrim\"\n aria-hidden=\"true\"\n [attr.data-open]=\"sidebar().modal\"\n [attr.data-motion]=\"config().motion.scrim\"\n (click)=\"store.closeSidebar()\"\n></div>\n", styles: ["@charset \"UTF-8\";:host{--shell-sidebar-track: var(--shell-sidebar-width, 264px);--shell-sidebar-top-offset: var(--shell-topbar-height, 64px);--shell-subheader-top-offset: var(--shell-topbar-height, 64px);display:grid;grid-template-columns:var(--shell-sidebar-track) minmax(0,1fr);grid-template-rows:auto auto minmax(0,1fr) auto;grid-template-areas:\"topbar topbar\" \"sidebar subheader\" \"sidebar main\" \"sidebar footer\";isolation:isolate;color-scheme:var(--shell-color-scheme, light);color:var(--shell-fg, #1b1c1f);background:var(--shell-bg, var(--shell-surface, #ffffff));font-family:var(--shell-font-family, inherit);font-size:var(--shell-font-size, 14px);transition-property:grid-template-columns;transition-duration:calc(var(--shell-collapse-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-collapse-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){:host{transition-duration:1ms}}:host([data-motion-collapse=off]){--shell-collapse-duration: 0s}:host([data-layout=sidebar-full]){grid-template-areas:\"sidebar topbar\" \"sidebar subheader\" \"sidebar main\" \"sidebar footer\"}:host([data-layout=stacked]){grid-template-areas:\"topbar topbar\" \"sidebar subheader\" \"sidebar main\" \"footer footer\"}:host([data-layout=sidebar-full]),:host([data-topbar=false]){--shell-sidebar-top-offset: 0px}:host([data-topbar=false]){--shell-subheader-top-offset: 0px}:host([data-scroll=main]){--shell-sidebar-block-size: 100%;block-size:100dvh;overflow:clip}:host([data-scroll=page]){--shell-sidebar-block-size: calc(100dvh - var(--shell-sidebar-top-offset));min-block-size:100dvh}.shell__frame{display:contents}:host([data-layout=inset]){--shell-sidebar-top-offset: 0px;--shell-sidebar-block-size: 100%;grid-template-rows:minmax(0,1fr);grid-template-areas:\"sidebar frame\";block-size:100dvh;overflow:clip;background:var(--shell-bg, var(--shell-surface-raised, #f7f7f8))}:host([data-layout=inset]) .shell__frame{grid-area:frame;z-index:var(--shell-z-main, 0);display:grid;grid-template-rows:auto auto minmax(0,1fr) auto;grid-template-areas:\"topbar\" \"subheader\" \"main\" \"footer\";min-inline-size:0;margin:var(--shell-frame-inset, 8px);border-width:var(--shell-frame-border-width, 0);border-style:var(--shell-frame-border-style, solid);border-color:var(--shell-frame-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-frame-radius, 14px);background:var(--shell-frame-bg, var(--shell-surface, #ffffff));box-shadow:var(--shell-frame-shadow, 0 1px 2px rgba(0, 0, 0, .04), 0 8px 24px -8px rgba(0, 0, 0, .14));overflow:clip}:host([data-layout=inset][data-scroll=page]) .shell__frame{grid-template-rows:auto auto 1fr auto;overflow:auto;overscroll-behavior:contain}:host([data-resizing]){cursor:col-resize;-webkit-user-select:none;user-select:none;transition-duration:0s}.shell__topbar{grid-area:topbar;z-index:var(--shell-z-topbar, 20)}:host([data-scroll=page]) .shell__topbar{position:sticky;inset-block-start:0}.shell__subheader{grid-area:subheader;z-index:calc(var(--shell-z-topbar, 20) - 1)}:host([data-scroll=page]) .shell__subheader[data-sticky]{position:sticky;inset-block-start:var(--shell-subheader-top-offset)}.shell__sidebar{grid-area:sidebar;z-index:var(--shell-z-sidebar, 40)}.shell__main{position:relative;grid-area:main;min-inline-size:0;z-index:var(--shell-z-main, 0);margin:var(--shell-main-margin, 0);padding:var(--shell-main-padding, 24px);border-width:var(--shell-main-border-width, 0);border-style:var(--shell-main-border-style, solid);border-color:var(--shell-main-border-color, var(--shell-border, #e6e6e9));border-radius:var(--shell-main-radius, 0px);background:var(--shell-main-bg, var(--shell-surface, #ffffff));color:var(--shell-main-fg, var(--shell-fg, #1b1c1f));box-shadow:var(--shell-main-shadow, none);container:shell-main/inline-size}.shell__main:focus{outline:none}:host([data-scroll=main]) .shell__main{overflow:auto;overscroll-behavior:contain}.shell__content{max-inline-size:var(--shell-main-max-width, none);margin-inline:auto}.shell__footer{grid-area:footer;z-index:var(--shell-z-topbar, 20)}.shell__scrim{position:fixed;inset:0;z-index:var(--shell-z-scrim, 30);background:var(--shell-drawer-scrim, var(--shell-scrim, rgba(15, 17, 21, .45)));-webkit-backdrop-filter:blur(var(--shell-drawer-scrim-blur, 0px));backdrop-filter:blur(var(--shell-drawer-scrim-blur, 0px));opacity:0;visibility:hidden;pointer-events:none;transition-property:opacity,visibility;transition-duration:calc(var(--shell-drawer-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-drawer-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.shell__scrim{transition-duration:1ms}}.shell__scrim[data-open=true]{opacity:1;visibility:visible;pointer-events:auto}.shell__scrim[data-motion=none]{transition-duration:0s}.shell__skip-link{position:fixed;inset-block-start:8px;inset-inline-start:8px;z-index:var(--shell-z-skip-link, 60);padding:8px 14px;border-radius:var(--shell-radius-sm, 6px);background:var(--shell-surface, #ffffff);color:var(--shell-fg, #1b1c1f);box-shadow:var(--shell-shadow-panel, 0 12px 32px -12px rgba(15, 17, 21, .28));text-decoration:none;transform:translateY(calc(-100% - 16px));transition-property:transform;transition-duration:calc(var(--shell-transition-duration, .18s) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1))}@media(prefers-reduced-motion:reduce){.shell__skip-link{transition-duration:1ms}}.shell__skip-link:focus-visible{transform:none;outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:2px}\n"] }]
1654
+ }], ctorParameters: () => [], propDecorators: { main: [{ type: i0.ViewChild, args: ['main', { isSignal: true }] }], frame: [{ type: i0.ViewChild, args: ['frame', { isSignal: true }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], scroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "scroll", required: false }] }], topbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "topbar", required: false }] }], subheader: [{ type: i0.Input, args: [{ isSignal: true, alias: "subheader", required: false }] }], footer: [{ type: i0.Input, args: [{ isSignal: true, alias: "footer", required: false }] }] } });
1655
+
1656
+ /**
1657
+ * A single navigation entry: a link, or an expandable section when it has
1658
+ * children.
1659
+ *
1660
+ * Renders `<a>` for anything navigable and `<button>` for a pure section
1661
+ * header. Those are different semantics for assistive tech, and only one of
1662
+ * them belongs in the tab order as a link. A section whose subtree contains the
1663
+ * active route opens itself and is marked, so deep links never land in a
1664
+ * collapsed tree.
1665
+ */
1666
+ class ShellNavItemComponent {
1667
+ item = input.required(...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
1668
+ depth = input(0, ...(ngDevMode ? [{ debugName: "depth" }] : /* istanbul ignore next */ []));
1669
+ /** False on an icon rail: labels and badges give way to icons and tooltips. */
1670
+ labels = input(true, ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
1671
+ iconTemplate = input(null, ...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
1672
+ /** Replaces the row content; see `ShellNavComponent.itemTemplate`. */
1673
+ itemTemplate = input(null, ...(ngDevMode ? [{ debugName: "itemTemplate" }] : /* istanbul ignore next */ []));
1674
+ router = inject(Router, { optional: true });
1675
+ /** Current URL, so "contains the active route" re-evaluates on navigation. */
1676
+ url = this.router
1677
+ ? toSignal(this.router.events.pipe(filter((event) => event instanceof NavigationEnd), map((event) => event.urlAfterRedirects)), { initialValue: this.router.url })
1678
+ : signal('');
1679
+ children = computed(() => this.item().children ?? [], ...(ngDevMode ? [{ debugName: "children" }] : /* istanbul ignore next */ []));
1680
+ hasChildren = computed(() => this.children().length > 0, ...(ngDevMode ? [{ debugName: "hasChildren" }] : /* istanbul ignore next */ []));
1681
+ isLink = computed(() => {
1682
+ const item = this.item();
1683
+ return !!(item.route || item.href) && !item.disabled;
1684
+ }, ...(ngDevMode ? [{ debugName: "isLink" }] : /* istanbul ignore next */ []));
1685
+ childActive = computed(() => {
1686
+ this.url();
1687
+ return this.hasChildren() && this.containsActive(this.children());
1688
+ }, ...(ngDevMode ? [{ debugName: "childActive" }] : /* istanbul ignore next */ []));
1689
+ /**
1690
+ * User-toggleable, but re-derives when the data or the active route changes —
1691
+ * so a server-driven `expanded` flag or a deep link still wins, without
1692
+ * clobbering a click the moment anything else re-renders.
1693
+ */
1694
+ expandedState = linkedSignal(() => (this.item().expanded ?? false) || this.childActive(), ...(ngDevMode ? [{ debugName: "expandedState" }] : /* istanbul ignore next */ []));
1695
+ expanded = computed(() => this.hasChildren() && this.expandedState(), ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
1696
+ /** Tooltip only when the label is not rendered — otherwise it is noise. */
1697
+ title = computed(() => (this.labels() ? null : this.item().label), ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
1698
+ /** Stand-in for a missing icon on a rail, so the row is never blank. */
1699
+ initial = computed(() => this.item().label.trim().charAt(0).toUpperCase(), ...(ngDevMode ? [{ debugName: "initial" }] : /* istanbul ignore next */ []));
1700
+ itemContext = computed(() => {
1701
+ const item = this.item();
1702
+ return {
1703
+ $implicit: item,
1704
+ item,
1705
+ depth: this.depth(),
1706
+ labels: this.labels(),
1707
+ expanded: this.expanded(),
1708
+ };
1709
+ }, ...(ngDevMode ? [{ debugName: "itemContext" }] : /* istanbul ignore next */ []));
1710
+ toggle() {
1711
+ this.expandedState.update((open) => !open);
1712
+ }
1713
+ /** `readonly unknown[]` is friendlier as an API than RouterLink's `any[]`. */
1714
+ routerTarget(item) {
1715
+ const route = item.route;
1716
+ if (route === undefined) {
1717
+ return [];
1718
+ }
1719
+ return typeof route === 'string' ? route : [...route];
1720
+ }
1721
+ containsActive(items) {
1722
+ const router = this.router;
1723
+ if (!router) {
1724
+ return false;
1725
+ }
1726
+ return items.some((item) => {
1727
+ if (item.route !== undefined) {
1728
+ const tree = typeof item.route === 'string'
1729
+ ? router.parseUrl(item.route)
1730
+ : router.createUrlTree([...item.route]);
1731
+ const options = {
1732
+ paths: item.exact ? 'exact' : 'subset',
1733
+ queryParams: 'ignored',
1734
+ fragment: 'ignored',
1735
+ matrixParams: 'ignored',
1736
+ };
1737
+ if (router.isActive(tree, options)) {
1738
+ return true;
1739
+ }
1740
+ }
1741
+ return this.containsActive(item.children ?? []);
1742
+ });
1743
+ }
1744
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellNavItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1745
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: ShellNavItemComponent, isStandalone: true, selector: "shell-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 }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, iconTemplate: { classPropertyName: "iconTemplate", publicName: "iconTemplate", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-depth": "depth()", "attr.data-expanded": "expanded()", "attr.data-labels": "labels()", "attr.data-child-active": "childActive() || null", "attr.data-disabled": "item().disabled || null", "style.--shell-nav-depth": "depth()" } }, ngImport: i0, template: "@let entry = item();\n@let icon = entry.icon;\n@let badge = entry.badge;\n\n@if (isLink()) {\n @if (entry.route) {\n <a\n class=\"nav-item__row\"\n [routerLink]=\"routerTarget(entry)\"\n routerLinkActive=\"nav-item__row--active\"\n [routerLinkActiveOptions]=\"{ exact: !!entry.exact }\"\n ariaCurrentWhenActive=\"page\"\n [attr.title]=\"title()\"\n [attr.target]=\"entry.target\"\n >\n <ng-container *ngTemplateOutlet=\"row\" />\n </a>\n } @else {\n <a\n class=\"nav-item__row\"\n [href]=\"entry.href\"\n [attr.title]=\"title()\"\n [attr.target]=\"entry.target\"\n [attr.rel]=\"entry.target === '_blank' ? 'noopener' : null\"\n >\n <ng-container *ngTemplateOutlet=\"row\" />\n </a>\n }\n} @else if (hasChildren()) {\n <button\n type=\"button\"\n class=\"nav-item__row nav-item__row--section\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.title]=\"title()\"\n [disabled]=\"entry.disabled\"\n (click)=\"toggle()\"\n >\n <ng-container *ngTemplateOutlet=\"row\" />\n <span class=\"nav-item__chevron\" aria-hidden=\"true\"></span>\n </button>\n} @else {\n <span class=\"nav-item__row nav-item__row--static\" [attr.title]=\"title()\">\n <ng-container *ngTemplateOutlet=\"row\" />\n </span>\n}\n\n@if (hasChildren()) {\n <!--\n Height animation via grid-template-rows 0fr \u2192 1fr: no JS measurement, and\n it degrades to an instant show/hide under prefers-reduced-motion.\n `inert` keeps collapsed children out of the tab order and the a11y tree.\n -->\n <div class=\"nav-item__children\">\n <ul class=\"nav-item__child-list\" [attr.inert]=\"expanded() ? null : true\">\n @for (child of children(); track child.id ?? child.label) {\n <li>\n <shell-nav-item\n [item]=\"child\"\n [depth]=\"depth() + 1\"\n [labels]=\"labels()\"\n [iconTemplate]=\"iconTemplate()\"\n [itemTemplate]=\"itemTemplate()\"\n />\n </li>\n }\n </ul>\n </div>\n}\n\n<ng-template #row>\n @if (itemTemplate(); as custom) {\n <!--\n Custom content inside the library's row element: link semantics, active\n state and sections still come from here; the template owns the rest.\n -->\n <ng-container *ngTemplateOutlet=\"custom; context: itemContext()\" />\n } @else {\n @if (icon) {\n <span class=\"nav-item__icon\" aria-hidden=\"true\">\n @if (iconTemplate()) {\n <ng-container\n *ngTemplateOutlet=\"\n iconTemplate()!;\n context: { $implicit: icon, icon: icon, item: entry }\n \"\n />\n } @else {\n {{ icon }}\n }\n </span>\n } @else if (!labels()) {\n <!-- Without an icon a rail row would be empty: show the label's initial. -->\n <span class=\"nav-item__glyph\" aria-hidden=\"true\">{{ initial() }}</span>\n }\n\n @if (labels()) {\n <span class=\"nav-item__label\">{{ entry.label }}</span>\n\n @if (badge !== undefined && badge !== null) {\n <span class=\"nav-item__badge\">{{ badge }}</span>\n }\n } @else {\n <!-- The label still has to reach screen readers on an icon rail. -->\n <span class=\"nav-item__sr-label\">{{ entry.label }}</span>\n\n @if (badge !== undefined && badge !== null) {\n <span class=\"nav-item__dot\" aria-hidden=\"true\"></span>\n }\n }\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block}.nav-item__row{position:relative;display:flex;align-items:center;gap:var(--shell-nav-item-gap, 10px);box-sizing:border-box;inline-size:100%;min-block-size:var(--shell-nav-item-height, 36px);padding-inline:10px;padding-inline-start:calc(10px + var(--shell-nav-depth, 0) * 14px);border:0;border-radius:var(--shell-nav-item-radius, var(--shell-radius-sm, 6px));background:transparent;color:var(--shell-nav-item-fg, var(--shell-fg, #1b1c1f));font:inherit;text-align:start;text-decoration:none;cursor:pointer;transition-property:background-color,color;transition-duration:calc(var(--shell-nav-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav-item__row{transition-duration:1ms}}.nav-item__row:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:-2px}.nav-item__row:hover{background:var(--shell-nav-item-hover-bg, var(--shell-hover-surface, #f0f0f3))}.nav-item__row--static{cursor:default;color:var(--shell-fg-muted, #6b6f76)}.nav-item__row--static:hover{background:transparent}.nav-item__row--active{background:var(--shell-nav-item-active-bg, var(--shell-accent-surface, #eef0fe));color:var(--shell-nav-item-active-fg, var(--shell-accent, #4f46e5));font-weight:600}.nav-item__row--active:hover{background:var(--shell-nav-item-active-bg, var(--shell-accent-surface, #eef0fe))}:host-context(shell-nav[data-indicator=bar]) .nav-item__row--active,:host-context(shell-nav[data-indicator=none]) .nav-item__row--active{background:var(--shell-nav-item-active-bg, transparent)}:host-context(shell-nav[data-indicator=bar]) .nav-item__row--active:hover,:host-context(shell-nav[data-indicator=none]) .nav-item__row--active:hover{background:var(--shell-nav-item-hover-bg, var(--shell-hover-surface, #f0f0f3))}:host-context(shell-nav[data-indicator=bar]) .nav-item__row--active:before{content:\"\";position:absolute;inset-block:7px;inset-inline-start:0;inline-size:var(--shell-nav-indicator-size, 3px);border-radius:999px;background:var(--shell-nav-indicator-color, var(--shell-accent, #4f46e5))}:host([data-child-active])>.nav-item__row--section{color:var(--shell-nav-item-active-fg, var(--shell-accent, #4f46e5))}:host([data-disabled]) .nav-item__row{opacity:.5;pointer-events:none}.nav-item__icon{display:grid;place-items:center;flex:0 0 auto;inline-size:var(--shell-nav-icon-size, 18px);block-size:var(--shell-nav-icon-size, 18px);font-size:var(--shell-nav-icon-size, 18px);line-height:1}.nav-item__glyph{display:grid;place-items:center;flex:0 0 auto;inline-size:var(--shell-nav-icon-size, 18px);block-size:var(--shell-nav-icon-size, 18px);border-radius:5px;background:var(--shell-active-surface, #e9e9ee);color:var(--shell-fg-muted, #6b6f76);font-size:11px;font-weight:700;line-height:1}.nav-item__row--active .nav-item__glyph{background:var(--shell-accent, #4f46e5);color:var(--shell-accent-fg, #ffffff)}.nav-item__label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1 1 auto}.nav-item__badge{flex:0 0 auto;min-inline-size:20px;padding-inline:6px;border-radius:999px;background:var(--shell-active-surface, #e9e9ee);color:var(--shell-fg-muted, #6b6f76);font-size:var(--shell-font-size-sm, 12.5px);font-weight:600;line-height:20px;text-align:center}.nav-item__dot{position:absolute;inset-block-start:6px;inset-inline-end:8px;inline-size:7px;block-size:7px;border-radius:50%;background:var(--shell-accent, #4f46e5)}.nav-item__sr-label{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.nav-item__chevron{flex:0 0 auto;inline-size:6px;block-size:6px;margin-inline-end:4px;border-inline-end:2px solid currentcolor;border-block-end:2px solid currentcolor;color:var(--shell-fg-muted, #6b6f76);transform:rotate(-45deg);transition-property:transform;transition-duration:calc(var(--shell-nav-expand-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-expand-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav-item__chevron{transition-duration:1ms}}:host-context([dir=rtl]) .nav-item__chevron{transform:rotate(135deg)}:host([data-expanded=true])>.nav-item__row>.nav-item__chevron{transform:rotate(45deg)}:host([data-labels=false]) .nav-item__row{justify-content:center;padding-inline:0}:host([data-labels=false]) .nav-item__chevron{display:none}.nav-item__children{display:grid;grid-template-rows:0fr;transition-property:grid-template-rows;transition-duration:calc(var(--shell-nav-expand-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-expand-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav-item__children{transition-duration:1ms}}:host([data-expanded=true])>.nav-item__children{grid-template-rows:1fr}.nav-item__child-list{display:flex;flex-direction:column;gap:var(--shell-sidebar-gap, 2px);min-block-size:0;margin:0;padding:0;overflow:hidden;list-style:none}:host([data-expanded=true])>.nav-item__children>.nav-item__child-list{padding-block-start:var(--shell-sidebar-gap, 2px)}\n"], dependencies: [{ kind: "component", type: ShellNavItemComponent, selector: "shell-nav-item", inputs: ["item", "depth", "labels", "iconTemplate", "itemTemplate"] }, { 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"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1746
+ }
1747
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellNavItemComponent, decorators: [{
1748
+ type: Component,
1749
+ args: [{ selector: 'shell-nav-item', imports: [NgTemplateOutlet, RouterLink, RouterLinkActive, ShellNavItemComponent], host: {
1750
+ '[attr.data-depth]': 'depth()',
1751
+ '[attr.data-expanded]': 'expanded()',
1752
+ '[attr.data-labels]': 'labels()',
1753
+ '[attr.data-child-active]': 'childActive() || null',
1754
+ '[attr.data-disabled]': 'item().disabled || null',
1755
+ '[style.--shell-nav-depth]': 'depth()',
1756
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let entry = item();\n@let icon = entry.icon;\n@let badge = entry.badge;\n\n@if (isLink()) {\n @if (entry.route) {\n <a\n class=\"nav-item__row\"\n [routerLink]=\"routerTarget(entry)\"\n routerLinkActive=\"nav-item__row--active\"\n [routerLinkActiveOptions]=\"{ exact: !!entry.exact }\"\n ariaCurrentWhenActive=\"page\"\n [attr.title]=\"title()\"\n [attr.target]=\"entry.target\"\n >\n <ng-container *ngTemplateOutlet=\"row\" />\n </a>\n } @else {\n <a\n class=\"nav-item__row\"\n [href]=\"entry.href\"\n [attr.title]=\"title()\"\n [attr.target]=\"entry.target\"\n [attr.rel]=\"entry.target === '_blank' ? 'noopener' : null\"\n >\n <ng-container *ngTemplateOutlet=\"row\" />\n </a>\n }\n} @else if (hasChildren()) {\n <button\n type=\"button\"\n class=\"nav-item__row nav-item__row--section\"\n [attr.aria-expanded]=\"expanded()\"\n [attr.title]=\"title()\"\n [disabled]=\"entry.disabled\"\n (click)=\"toggle()\"\n >\n <ng-container *ngTemplateOutlet=\"row\" />\n <span class=\"nav-item__chevron\" aria-hidden=\"true\"></span>\n </button>\n} @else {\n <span class=\"nav-item__row nav-item__row--static\" [attr.title]=\"title()\">\n <ng-container *ngTemplateOutlet=\"row\" />\n </span>\n}\n\n@if (hasChildren()) {\n <!--\n Height animation via grid-template-rows 0fr \u2192 1fr: no JS measurement, and\n it degrades to an instant show/hide under prefers-reduced-motion.\n `inert` keeps collapsed children out of the tab order and the a11y tree.\n -->\n <div class=\"nav-item__children\">\n <ul class=\"nav-item__child-list\" [attr.inert]=\"expanded() ? null : true\">\n @for (child of children(); track child.id ?? child.label) {\n <li>\n <shell-nav-item\n [item]=\"child\"\n [depth]=\"depth() + 1\"\n [labels]=\"labels()\"\n [iconTemplate]=\"iconTemplate()\"\n [itemTemplate]=\"itemTemplate()\"\n />\n </li>\n }\n </ul>\n </div>\n}\n\n<ng-template #row>\n @if (itemTemplate(); as custom) {\n <!--\n Custom content inside the library's row element: link semantics, active\n state and sections still come from here; the template owns the rest.\n -->\n <ng-container *ngTemplateOutlet=\"custom; context: itemContext()\" />\n } @else {\n @if (icon) {\n <span class=\"nav-item__icon\" aria-hidden=\"true\">\n @if (iconTemplate()) {\n <ng-container\n *ngTemplateOutlet=\"\n iconTemplate()!;\n context: { $implicit: icon, icon: icon, item: entry }\n \"\n />\n } @else {\n {{ icon }}\n }\n </span>\n } @else if (!labels()) {\n <!-- Without an icon a rail row would be empty: show the label's initial. -->\n <span class=\"nav-item__glyph\" aria-hidden=\"true\">{{ initial() }}</span>\n }\n\n @if (labels()) {\n <span class=\"nav-item__label\">{{ entry.label }}</span>\n\n @if (badge !== undefined && badge !== null) {\n <span class=\"nav-item__badge\">{{ badge }}</span>\n }\n } @else {\n <!-- The label still has to reach screen readers on an icon rail. -->\n <span class=\"nav-item__sr-label\">{{ entry.label }}</span>\n\n @if (badge !== undefined && badge !== null) {\n <span class=\"nav-item__dot\" aria-hidden=\"true\"></span>\n }\n }\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block}.nav-item__row{position:relative;display:flex;align-items:center;gap:var(--shell-nav-item-gap, 10px);box-sizing:border-box;inline-size:100%;min-block-size:var(--shell-nav-item-height, 36px);padding-inline:10px;padding-inline-start:calc(10px + var(--shell-nav-depth, 0) * 14px);border:0;border-radius:var(--shell-nav-item-radius, var(--shell-radius-sm, 6px));background:transparent;color:var(--shell-nav-item-fg, var(--shell-fg, #1b1c1f));font:inherit;text-align:start;text-decoration:none;cursor:pointer;transition-property:background-color,color;transition-duration:calc(var(--shell-nav-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav-item__row{transition-duration:1ms}}.nav-item__row:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:-2px}.nav-item__row:hover{background:var(--shell-nav-item-hover-bg, var(--shell-hover-surface, #f0f0f3))}.nav-item__row--static{cursor:default;color:var(--shell-fg-muted, #6b6f76)}.nav-item__row--static:hover{background:transparent}.nav-item__row--active{background:var(--shell-nav-item-active-bg, var(--shell-accent-surface, #eef0fe));color:var(--shell-nav-item-active-fg, var(--shell-accent, #4f46e5));font-weight:600}.nav-item__row--active:hover{background:var(--shell-nav-item-active-bg, var(--shell-accent-surface, #eef0fe))}:host-context(shell-nav[data-indicator=bar]) .nav-item__row--active,:host-context(shell-nav[data-indicator=none]) .nav-item__row--active{background:var(--shell-nav-item-active-bg, transparent)}:host-context(shell-nav[data-indicator=bar]) .nav-item__row--active:hover,:host-context(shell-nav[data-indicator=none]) .nav-item__row--active:hover{background:var(--shell-nav-item-hover-bg, var(--shell-hover-surface, #f0f0f3))}:host-context(shell-nav[data-indicator=bar]) .nav-item__row--active:before{content:\"\";position:absolute;inset-block:7px;inset-inline-start:0;inline-size:var(--shell-nav-indicator-size, 3px);border-radius:999px;background:var(--shell-nav-indicator-color, var(--shell-accent, #4f46e5))}:host([data-child-active])>.nav-item__row--section{color:var(--shell-nav-item-active-fg, var(--shell-accent, #4f46e5))}:host([data-disabled]) .nav-item__row{opacity:.5;pointer-events:none}.nav-item__icon{display:grid;place-items:center;flex:0 0 auto;inline-size:var(--shell-nav-icon-size, 18px);block-size:var(--shell-nav-icon-size, 18px);font-size:var(--shell-nav-icon-size, 18px);line-height:1}.nav-item__glyph{display:grid;place-items:center;flex:0 0 auto;inline-size:var(--shell-nav-icon-size, 18px);block-size:var(--shell-nav-icon-size, 18px);border-radius:5px;background:var(--shell-active-surface, #e9e9ee);color:var(--shell-fg-muted, #6b6f76);font-size:11px;font-weight:700;line-height:1}.nav-item__row--active .nav-item__glyph{background:var(--shell-accent, #4f46e5);color:var(--shell-accent-fg, #ffffff)}.nav-item__label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1 1 auto}.nav-item__badge{flex:0 0 auto;min-inline-size:20px;padding-inline:6px;border-radius:999px;background:var(--shell-active-surface, #e9e9ee);color:var(--shell-fg-muted, #6b6f76);font-size:var(--shell-font-size-sm, 12.5px);font-weight:600;line-height:20px;text-align:center}.nav-item__dot{position:absolute;inset-block-start:6px;inset-inline-end:8px;inline-size:7px;block-size:7px;border-radius:50%;background:var(--shell-accent, #4f46e5)}.nav-item__sr-label{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.nav-item__chevron{flex:0 0 auto;inline-size:6px;block-size:6px;margin-inline-end:4px;border-inline-end:2px solid currentcolor;border-block-end:2px solid currentcolor;color:var(--shell-fg-muted, #6b6f76);transform:rotate(-45deg);transition-property:transform;transition-duration:calc(var(--shell-nav-expand-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-expand-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav-item__chevron{transition-duration:1ms}}:host-context([dir=rtl]) .nav-item__chevron{transform:rotate(135deg)}:host([data-expanded=true])>.nav-item__row>.nav-item__chevron{transform:rotate(45deg)}:host([data-labels=false]) .nav-item__row{justify-content:center;padding-inline:0}:host([data-labels=false]) .nav-item__chevron{display:none}.nav-item__children{display:grid;grid-template-rows:0fr;transition-property:grid-template-rows;transition-duration:calc(var(--shell-nav-expand-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-expand-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav-item__children{transition-duration:1ms}}:host([data-expanded=true])>.nav-item__children{grid-template-rows:1fr}.nav-item__child-list{display:flex;flex-direction:column;gap:var(--shell-sidebar-gap, 2px);min-block-size:0;margin:0;padding:0;overflow:hidden;list-style:none}:host([data-expanded=true])>.nav-item__children>.nav-item__child-list{padding-block-start:var(--shell-sidebar-gap, 2px)}\n"] }]
1757
+ }], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], iconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTemplate", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }] } });
1758
+
1759
+ /**
1760
+ * Lets code other than the owner of the nav array add entries to it — a
1761
+ * feature library adding "Roadmap" to the app's "Workspace" group, or a
1762
+ * section adding context links while it is open.
1763
+ *
1764
+ * Contributions target a group by `ShellNavGroup.id` and are merged into every
1765
+ * `<shell-nav>` that renders a group with that id. The app keeps control of
1766
+ * structure and order: it declares the groups (an empty group is fine — empty
1767
+ * groups are not rendered), features only fill them.
1768
+ */
1769
+ class ShellNavRegistry {
1770
+ contributions = signal([], ...(ngDevMode ? [{ debugName: "contributions" }] : /* istanbul ignore next */ []));
1771
+ sequence = 0;
1772
+ /**
1773
+ * Add items to the group with this id. Returns a function that removes them
1774
+ * again. Prefer `contributeNavItems()` / `provideShellNavItems()`, which
1775
+ * remove them automatically.
1776
+ */
1777
+ contribute(groupId, items, options = {}) {
1778
+ const entry = {
1779
+ groupId,
1780
+ items,
1781
+ order: options.order ?? 0,
1782
+ sequence: this.sequence++,
1783
+ };
1784
+ this.contributions.update((current) => [...current, entry]);
1785
+ return () => this.contributions.update((current) => current.filter((candidate) => candidate !== entry));
1786
+ }
1787
+ /** Items contributed to a group, in order. Reactive when read in a signal context. */
1788
+ itemsFor(groupId) {
1789
+ return this.contributions()
1790
+ .filter((contribution) => contribution.groupId === groupId)
1791
+ .sort((a, b) => a.order - b.order || a.sequence - b.sequence)
1792
+ .flatMap((contribution) => isSignal(contribution.items) ? contribution.items() : contribution.items);
1793
+ }
1794
+ /**
1795
+ * `groups` with contributed items appended to the groups whose `id` matches.
1796
+ * Returns the input untouched when there is nothing to add, so object
1797
+ * identity stays stable for the common case. Reactive.
1798
+ */
1799
+ merge(groups) {
1800
+ if (!this.contributions().length) {
1801
+ return groups;
1802
+ }
1803
+ return groups.map((group) => {
1804
+ const extra = group.id === undefined ? [] : this.itemsFor(group.id);
1805
+ return extra.length ? { ...group, items: [...group.items, ...extra] } : group;
1806
+ });
1807
+ }
1808
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellNavRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1809
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellNavRegistry, providedIn: 'root' });
1810
+ }
1811
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellNavRegistry, decorators: [{
1812
+ type: Injectable,
1813
+ args: [{ providedIn: 'root' }]
1814
+ }] });
1815
+ /**
1816
+ * Contribute nav items for the lifetime of the calling component, directive or
1817
+ * service. Call from an injection context.
1818
+ *
1819
+ * ```ts
1820
+ * export class ProjectPage {
1821
+ * constructor() {
1822
+ * contributeNavItems('workspace', [{ label: 'Project settings', route: '/settings' }]);
1823
+ * }
1824
+ * }
1825
+ * ```
1826
+ */
1827
+ function contributeNavItems(groupId, items, options) {
1828
+ const remove = inject(ShellNavRegistry).contribute(groupId, items, options);
1829
+ inject(DestroyRef).onDestroy(remove);
1830
+ }
1831
+ /**
1832
+ * Contribute nav items for the lifetime of an environment injector. In the app
1833
+ * config the entries exist from startup — the usual way for a feature to own
1834
+ * its link:
1835
+ *
1836
+ * ```ts
1837
+ * providers: [provideShellNavItems('workspace', [{ label: 'Roadmap', route: '/roadmap' }])]
1838
+ * ```
1839
+ *
1840
+ * In a lazy route's `providers` they only appear once that route has loaded,
1841
+ * so do not use that to add the link *to* the lazy route itself.
1842
+ */
1843
+ function provideShellNavItems(groupId, items, options) {
1844
+ return provideEnvironmentInitializer(() => contributeNavItems(groupId, items, options));
1845
+ }
1846
+
1847
+ /**
1848
+ * Renders grouped navigation. Data in, markup out — so the same array feeds
1849
+ * the sidebar, the mobile drawer and anything else that needs the nav tree.
1850
+ *
1851
+ * ```html
1852
+ * <ng-template shellSlot="sidebar-nav">
1853
+ * <shell-nav [groups]="navigation" [iconTemplate]="icon" />
1854
+ * </ng-template>
1855
+ * ```
1856
+ *
1857
+ * `labels` follows the sidebar by default, so a rail needs no configuration.
1858
+ * Items contributed through `ShellNavRegistry` are merged into the groups with
1859
+ * a matching `id`; groups that end up empty are not rendered.
1860
+ */
1861
+ class ShellNavComponent {
1862
+ /** Groups, or a flat item list which is wrapped in one unlabelled group. */
1863
+ groups = input([], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
1864
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1865
+ /** Override the sidebar's label state — useful when reusing `shell-nav`. */
1866
+ labels = input(null, ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
1867
+ /** Hide group headings even when labels show (a denser, Slack-like list). */
1868
+ headings = input(true, { ...(ngDevMode ? { debugName: "headings" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1869
+ /** Renders `ShellNavItem.icon` keys. Without it, the key is shown as text. */
1870
+ iconTemplate = input(null, ...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
1871
+ /**
1872
+ * Replaces the *content* of every row (icon, label, badge). The row element
1873
+ * itself — link, section button, active state, children — stays the
1874
+ * library's, so routing and accessibility keep working.
1875
+ */
1876
+ itemTemplate = input(null, ...(ngDevMode ? [{ debugName: "itemTemplate" }] : /* istanbul ignore next */ []));
1877
+ store = inject(ShellStore);
1878
+ registry = inject(ShellNavRegistry);
1879
+ /** Per-group overrides on top of the data's own `collapsed` flag. */
1880
+ groupOverrides = signal(new Map(), ...(ngDevMode ? [{ debugName: "groupOverrides" }] : /* istanbul ignore next */ []));
1881
+ showLabels = computed(() => this.labels() ?? this.store.sidebar().labels, ...(ngDevMode ? [{ debugName: "showLabels" }] : /* istanbul ignore next */ []));
1882
+ resolvedGroups = computed(() => {
1883
+ const groups = this.groups();
1884
+ const items = this.items();
1885
+ const base = groups.length ? groups : items.length ? [{ items }] : [];
1886
+ return this.registry.merge(base).filter((group) => group.items.length > 0);
1887
+ }, ...(ngDevMode ? [{ debugName: "resolvedGroups" }] : /* istanbul ignore next */ []));
1888
+ groupKey(group, index) {
1889
+ return group.id ?? group.label ?? `group-${index}`;
1890
+ }
1891
+ isCollapsed(group, index) {
1892
+ return this.groupOverrides().get(this.groupKey(group, index)) ?? !!group.collapsed;
1893
+ }
1894
+ toggleGroup(group, index) {
1895
+ const key = this.groupKey(group, index);
1896
+ const collapsed = this.isCollapsed(group, index);
1897
+ this.groupOverrides.update((current) => new Map(current).set(key, !collapsed));
1898
+ }
1899
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1900
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: ShellNavComponent, isStandalone: true, selector: "shell-nav", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, headings: { classPropertyName: "headings", publicName: "headings", isSignal: true, isRequired: false, transformFunction: null }, iconTemplate: { classPropertyName: "iconTemplate", publicName: "iconTemplate", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-labels": "showLabels()", "attr.data-indicator": "store.config().appearance.navIndicator", "attr.data-motion-expand": "store.config().motion.navExpand ? null : \"off\"" } }, ngImport: i0, template: "@for (group of resolvedGroups(); track groupKey(group, $index); let index = $index) {\n @let collapsed = isCollapsed(group, index);\n @let showHeading = showLabels() && headings() && !!group.label;\n\n <div class=\"nav__group\" [attr.data-collapsed]=\"collapsed\">\n @if (showHeading) {\n @if (group.collapsible) {\n <button\n type=\"button\"\n class=\"nav__heading nav__heading--button\"\n [attr.aria-expanded]=\"!collapsed\"\n (click)=\"toggleGroup(group, index)\"\n >\n <span class=\"nav__heading-text\">{{ group.label }}</span>\n <span class=\"nav__heading-chevron\" aria-hidden=\"true\"></span>\n </button>\n } @else {\n <h2 class=\"nav__heading\">{{ group.label }}</h2>\n }\n } @else if (index > 0) {\n <!-- Without a heading, a rule keeps the groups legible on a rail. -->\n <hr class=\"nav__divider\" />\n }\n\n @if (!collapsed) {\n <ul class=\"nav__list\">\n @for (item of group.items; track item.id ?? item.label) {\n <li>\n <shell-nav-item\n [item]=\"item\"\n [labels]=\"showLabels()\"\n [iconTemplate]=\"iconTemplate()\"\n [itemTemplate]=\"itemTemplate()\"\n />\n </li>\n }\n </ul>\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:12px}:host([data-motion-expand=off]){--shell-nav-expand-duration: 0s}.nav__group{display:flex;flex-direction:column;gap:2px}.nav__heading{display:flex;align-items:center;gap:6px;margin:0;padding:6px 10px 2px;border:0;background:transparent;color:var(--shell-nav-heading-fg, var(--shell-fg-muted, #6b6f76));font-size:var(--shell-nav-heading-size, var(--shell-font-size-sm, 12.5px));font-weight:600;letter-spacing:.01em}.nav__heading--button{border-radius:var(--shell-radius-sm, 6px);cursor:pointer;transition-property:color;transition-duration:calc(var(--shell-nav-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav__heading--button{transition-duration:1ms}}.nav__heading--button:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:-2px}.nav__heading--button:hover{color:var(--shell-nav-item-fg, var(--shell-fg, #1b1c1f))}.nav__heading-text{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1 1 auto;text-align:start}.nav__heading-chevron{flex:0 0 auto;inline-size:6px;block-size:6px;border-inline-end:2px solid currentcolor;border-block-end:2px solid currentcolor;transform:rotate(45deg);transition-property:transform;transition-duration:calc(var(--shell-nav-expand-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-expand-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav__heading-chevron{transition-duration:1ms}}.nav__group[data-collapsed=true] .nav__heading-chevron{transform:rotate(-45deg)}.nav__divider{margin:6px 8px;border:0;border-block-start:1px solid var(--shell-sidebar-border-color, var(--shell-border, #e6e6e9))}.nav__list{display:flex;flex-direction:column;gap:var(--shell-sidebar-gap, 2px);margin:0;padding:0;list-style:none}\n"], dependencies: [{ kind: "component", type: ShellNavItemComponent, selector: "shell-nav-item", inputs: ["item", "depth", "labels", "iconTemplate", "itemTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1901
+ }
1902
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: ShellNavComponent, decorators: [{
1903
+ type: Component,
1904
+ args: [{ selector: 'shell-nav', imports: [ShellNavItemComponent], host: {
1905
+ '[attr.data-labels]': 'showLabels()',
1906
+ '[attr.data-indicator]': 'store.config().appearance.navIndicator',
1907
+ '[attr.data-motion-expand]': 'store.config().motion.navExpand ? null : "off"',
1908
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@for (group of resolvedGroups(); track groupKey(group, $index); let index = $index) {\n @let collapsed = isCollapsed(group, index);\n @let showHeading = showLabels() && headings() && !!group.label;\n\n <div class=\"nav__group\" [attr.data-collapsed]=\"collapsed\">\n @if (showHeading) {\n @if (group.collapsible) {\n <button\n type=\"button\"\n class=\"nav__heading nav__heading--button\"\n [attr.aria-expanded]=\"!collapsed\"\n (click)=\"toggleGroup(group, index)\"\n >\n <span class=\"nav__heading-text\">{{ group.label }}</span>\n <span class=\"nav__heading-chevron\" aria-hidden=\"true\"></span>\n </button>\n } @else {\n <h2 class=\"nav__heading\">{{ group.label }}</h2>\n }\n } @else if (index > 0) {\n <!-- Without a heading, a rule keeps the groups legible on a rail. -->\n <hr class=\"nav__divider\" />\n }\n\n @if (!collapsed) {\n <ul class=\"nav__list\">\n @for (item of group.items; track item.id ?? item.label) {\n <li>\n <shell-nav-item\n [item]=\"item\"\n [labels]=\"showLabels()\"\n [iconTemplate]=\"iconTemplate()\"\n [itemTemplate]=\"itemTemplate()\"\n />\n </li>\n }\n </ul>\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:12px}:host([data-motion-expand=off]){--shell-nav-expand-duration: 0s}.nav__group{display:flex;flex-direction:column;gap:2px}.nav__heading{display:flex;align-items:center;gap:6px;margin:0;padding:6px 10px 2px;border:0;background:transparent;color:var(--shell-nav-heading-fg, var(--shell-fg-muted, #6b6f76));font-size:var(--shell-nav-heading-size, var(--shell-font-size-sm, 12.5px));font-weight:600;letter-spacing:.01em}.nav__heading--button{border-radius:var(--shell-radius-sm, 6px);cursor:pointer;transition-property:color;transition-duration:calc(var(--shell-nav-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav__heading--button{transition-duration:1ms}}.nav__heading--button:focus-visible{outline:2px solid var(--shell-focus-ring, #4f46e5);outline-offset:-2px}.nav__heading--button:hover{color:var(--shell-nav-item-fg, var(--shell-fg, #1b1c1f))}.nav__heading-text{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1 1 auto;text-align:start}.nav__heading-chevron{flex:0 0 auto;inline-size:6px;block-size:6px;border-inline-end:2px solid currentcolor;border-block-end:2px solid currentcolor;transform:rotate(45deg);transition-property:transform;transition-duration:calc(var(--shell-nav-expand-duration, var(--shell-transition-duration, .18s)) * var(--shell-motion-scale, 1));transition-timing-function:var(--shell-nav-expand-easing, var(--shell-transition-easing, cubic-bezier(.2, 0, 0, 1)))}@media(prefers-reduced-motion:reduce){.nav__heading-chevron{transition-duration:1ms}}.nav__group[data-collapsed=true] .nav__heading-chevron{transform:rotate(-45deg)}.nav__divider{margin:6px 8px;border:0;border-block-start:1px solid var(--shell-sidebar-border-color, var(--shell-border, #e6e6e9))}.nav__list{display:flex;flex-direction:column;gap:var(--shell-sidebar-gap, 2px);margin:0;padding:0;list-style:none}\n"] }]
1909
+ }], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], headings: [{ type: i0.Input, args: [{ isSignal: true, alias: "headings", required: false }] }], iconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTemplate", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }] } });
1910
+
1911
+ /**
1912
+ * Add to a component's `imports` to get the whole shell vocabulary:
1913
+ *
1914
+ * ```ts
1915
+ * @Component({ imports: [APP_SHELL], … })
1916
+ * ```
1917
+ *
1918
+ * Tree-shaking still applies per component, so unused pieces are dropped.
1919
+ */
1920
+ const APP_SHELL = [
1921
+ AppShellComponent,
1922
+ ShellSlotDirective,
1923
+ ShellSlotOutletComponent,
1924
+ ShellNavComponent,
1925
+ ShellNavItemComponent,
1926
+ ShellBreadcrumbComponent,
1927
+ SidebarToggleDirective,
1928
+ ContainerSizeDirective,
1929
+ ];
1930
+
1931
+ /*
1932
+ * Public API of the app shell.
1933
+ *
1934
+ * Everything a host application needs, and nothing it should not reach for:
1935
+ * internals such as the focus trap live behind the barrel.
1936
+ */
1937
+ // Configuration & types
1938
+
1939
+ /**
1940
+ * Generated bundle index. Do not edit.
1941
+ */
1942
+
1943
+ export { APP_SHELL, AppShellComponent, BREADCRUMB_DATA_KEY, ContainerSizeDirective, DEFAULT_BREAKPOINTS, DEFAULT_SHELL_CONFIG, SHELL_CONFIG, SHELL_MAIN_ID, SHELL_REGION, SHELL_SIDEBAR_ID, SHELL_SIZES, SHELL_STATE_STORAGE, SIDEBAR_BEHAVIOR, SUBHEADER_DATA_KEY, ShellBreadcrumbComponent, ShellBreadcrumbStore, ShellFooterComponent, ShellNavComponent, ShellNavItemComponent, ShellNavRegistry, ShellSidebarComponent, ShellSlotDirective, ShellSlotOutletComponent, ShellSlotRegistry, ShellStore, ShellSubheaderComponent, ShellTopbarComponent, SidebarToggleDirective, atLeast, atMost, contributeBreadcrumbLabel, contributeNavItems, drawerSidebarBehavior, drawerState, hasSlot, hiddenState, isBelow, localShellStateStorage, manualSidebarBehavior, measureRegion, mergeShellConfig, observeWidth, panelState, provideAppShell, provideShellNavItems, railState, resolveSize, responsiveSidebarBehavior, sizeRank };
1944
+ //# sourceMappingURL=danjelp-ngx-app-shell.mjs.map