@ardimedia/angular-portal-azure 0.3.18 → 0.3.27

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.
@@ -1,6 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, inject, makeEnvironmentProviders, APP_INITIALIZER, input, output, Component, ElementRef, Injector, DestroyRef, effect, afterNextRender } from '@angular/core';
2
+ import { signal, computed, Injectable, inject, InjectionToken, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, DestroyRef, effect, APP_INITIALIZER, input, output, Component, ElementRef, Injector, afterNextRender, contentChild } from '@angular/core';
3
+ import { Router, NavigationEnd } from '@angular/router';
4
+ import { filter } from 'rxjs/operators';
3
5
  import { DOCUMENT, NgComponentOutlet } from '@angular/common';
6
+ import { NgForm } from '@angular/forms';
4
7
 
5
8
  function clearStatusBar() {
6
9
  return { text: '', style: 'none' };
@@ -15,19 +18,26 @@ function statusBarSuccess(text) {
15
18
  return { text, style: 'success' };
16
19
  }
17
20
 
21
+ let _nextBladeUid = 0;
22
+ /** @internal */
23
+ function nextBladeUid() { return ++_nextBladeUid; }
18
24
  /** Creates a blade definition with sensible defaults.
19
- * statusBar uses a getter/setter backed by a signal for zoneless change detection. */
20
- function createBlade(path, title, width = 315) {
21
- const _statusBar = signal(clearStatusBar(), ...(ngDevMode ? [{ debugName: "_statusBar" }] : []));
25
+ * statusBar and title use getter/setter pairs backed by signals for zoneless change detection. */
26
+ function createBlade(path, title, width = 315, params = {}, uid) {
27
+ const _statusBar = signal(clearStatusBar(), ...(ngDevMode ? [{ debugName: "_statusBar" }] : /* istanbul ignore next */ []));
28
+ const _title = signal(title, ...(ngDevMode ? [{ debugName: "_title" }] : /* istanbul ignore next */ []));
22
29
  return {
30
+ uid: uid ?? nextBladeUid(),
23
31
  path: path.toLowerCase(),
24
- title,
32
+ get title() { return _title(); },
33
+ set title(value) { _title.set(value); },
25
34
  subtitle: '',
26
35
  width,
27
36
  isInnerHtml: true,
28
37
  commands: [],
29
38
  get statusBar() { return _statusBar(); },
30
39
  set statusBar(value) { _statusBar.set(value); },
40
+ params,
31
41
  };
32
42
  }
33
43
 
@@ -63,6 +73,7 @@ const LABELS_DE_CH = {
63
73
  settings: 'Einstellungen',
64
74
  language: 'Sprache',
65
75
  appearance: 'Darstellung',
76
+ unsavedChangesConfirm: 'Die Änderungen wurden noch nicht gespeichert. Trotzdem verlassen?',
66
77
  };
67
78
  /** German (Germany) — Swiss spelling rules apply (no ß) */
68
79
  const LABELS_DE_DE = { ...LABELS_DE_CH };
@@ -92,6 +103,7 @@ const LABELS_EN = {
92
103
  settings: 'Settings',
93
104
  language: 'Language',
94
105
  appearance: 'Appearance',
106
+ unsavedChangesConfirm: 'You have unsaved changes. Leave anyway?',
95
107
  };
96
108
  /** French */
97
109
  const LABELS_FR = {
@@ -119,6 +131,7 @@ const LABELS_FR = {
119
131
  settings: 'Paramètres',
120
132
  language: 'Langue',
121
133
  appearance: 'Apparence',
134
+ unsavedChangesConfirm: 'Les modifications n\'ont pas été enregistrées. Quitter quand même ?',
122
135
  };
123
136
  /** Spanish */
124
137
  const LABELS_ES = {
@@ -146,6 +159,7 @@ const LABELS_ES = {
146
159
  settings: 'Configuración',
147
160
  language: 'Idioma',
148
161
  appearance: 'Apariencia',
162
+ unsavedChangesConfirm: 'Hay cambios sin guardar. ¿Salir de todos modos?',
149
163
  };
150
164
  /** Italian */
151
165
  const LABELS_IT = {
@@ -173,6 +187,7 @@ const LABELS_IT = {
173
187
  settings: 'Impostazioni',
174
188
  language: 'Lingua',
175
189
  appearance: 'Aspetto',
190
+ unsavedChangesConfirm: 'Le modifiche non sono state salvate. Uscire comunque?',
176
191
  };
177
192
  // ── Language preset registry ────────────────────────────────────────
178
193
  /** Keep DEFAULT_LABELS as alias for backward compatibility */
@@ -192,16 +207,19 @@ function registerLanguagePreset(preset) {
192
207
  }
193
208
 
194
209
  /** Creates a data blade definition with sensible defaults.
195
- * statusBar, item, items use getter/setter pairs backed by signals for zoneless change detection.
210
+ * statusBar, title, item, items use getter/setter pairs backed by signals for zoneless change detection.
196
211
  * Note: cannot use ...createBlade() spread here — spread copies getter values, not getter/setter pairs. */
197
- function createDataBlade(path, title, width = 315) {
198
- const _statusBar = signal(clearStatusBar(), ...(ngDevMode ? [{ debugName: "_statusBar" }] : []));
199
- const _item = signal({}, ...(ngDevMode ? [{ debugName: "_item" }] : []));
200
- const _items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : []));
201
- const _loading = signal(false, ...(ngDevMode ? [{ debugName: "_loading" }] : []));
212
+ function createDataBlade(path, title, width = 315, params = {}, uid) {
213
+ const _statusBar = signal(clearStatusBar(), ...(ngDevMode ? [{ debugName: "_statusBar" }] : /* istanbul ignore next */ []));
214
+ const _title = signal(title, ...(ngDevMode ? [{ debugName: "_title" }] : /* istanbul ignore next */ []));
215
+ const _item = signal({}, ...(ngDevMode ? [{ debugName: "_item" }] : /* istanbul ignore next */ []));
216
+ const _items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : /* istanbul ignore next */ []));
217
+ const _loading = signal(false, ...(ngDevMode ? [{ debugName: "_loading" }] : /* istanbul ignore next */ []));
202
218
  return {
219
+ uid: uid ?? nextBladeUid(),
203
220
  path: path.toLowerCase(),
204
- title,
221
+ get title() { return _title(); },
222
+ set title(value) { _title.set(value); },
205
223
  subtitle: '',
206
224
  width,
207
225
  isInnerHtml: true,
@@ -215,6 +233,7 @@ function createDataBlade(path, title, width = 315) {
215
233
  get loading() { return _loading(); },
216
234
  set loading(value) { _loading.set(value); },
217
235
  lifecycle: {},
236
+ params,
218
237
  };
219
238
  }
220
239
  /**
@@ -330,6 +349,10 @@ function getUserDisplayName(account) {
330
349
  return `${account.firstName || ''} ${account.lastName || ''}`.trim();
331
350
  }
332
351
 
352
+ /**
353
+ * Notification panel definition.
354
+ * Ported from AreaNotification in v0.2.346.
355
+ */
333
356
  function createNotificationPanel() {
334
357
  return {
335
358
  path: '',
@@ -426,32 +449,32 @@ function getAllStringValues(obj) {
426
449
  class PortalService {
427
450
  static LANG_STORAGE_KEY = 'apa-language';
428
451
  /** Localization labels (defaults to German/de-CH, override via PortalConfig.labels) */
429
- labels = signal({ ...DEFAULT_LABELS }, ...(ngDevMode ? [{ debugName: "labels" }] : []));
452
+ labels = signal({ ...DEFAULT_LABELS }, ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
430
453
  /** Current language code */
431
- currentLanguage = signal('de-CH', ...(ngDevMode ? [{ debugName: "currentLanguage" }] : []));
454
+ currentLanguage = signal('de-CH', ...(ngDevMode ? [{ debugName: "currentLanguage" }] : /* istanbul ignore next */ []));
432
455
  /** Whether the settings dropdown is open */
433
- isSettingsOpen = signal(false, ...(ngDevMode ? [{ debugName: "isSettingsOpen" }] : []));
456
+ isSettingsOpen = signal(false, ...(ngDevMode ? [{ debugName: "isSettingsOpen" }] : /* istanbul ignore next */ []));
434
457
  /** The blade stack — ordered left-to-right */
435
- blades = signal([], ...(ngDevMode ? [{ debugName: "blades" }] : []));
458
+ blades = signal([], ...(ngDevMode ? [{ debugName: "blades" }] : /* istanbul ignore next */ []));
436
459
  /** Panorama (startboard/dashboard) state */
437
- panorama = signal(createPanorama(''), ...(ngDevMode ? [{ debugName: "panorama" }] : []));
460
+ panorama = signal(createPanorama(''), ...(ngDevMode ? [{ debugName: "panorama" }] : /* istanbul ignore next */ []));
438
461
  /** Notification panel state */
439
- notification = signal(createNotificationPanel(), ...(ngDevMode ? [{ debugName: "notification" }] : []));
462
+ notification = signal(createNotificationPanel(), ...(ngDevMode ? [{ debugName: "notification" }] : /* istanbul ignore next */ []));
440
463
  /** Avatar menu state */
441
- avatarMenu = signal(createAvatarMenu(), ...(ngDevMode ? [{ debugName: "avatarMenu" }] : []));
464
+ avatarMenu = signal(createAvatarMenu(), ...(ngDevMode ? [{ debugName: "avatarMenu" }] : /* istanbul ignore next */ []));
442
465
  /** Shared parameter for passing data between blades */
443
- parameter = signal({ action: 'none', itemId: 0 }, ...(ngDevMode ? [{ debugName: "parameter" }] : []));
466
+ parameter = signal({ action: 'none', itemId: 0 }, ...(ngDevMode ? [{ debugName: "parameter" }] : /* istanbul ignore next */ []));
444
467
  /** Portal theme identifier */
445
- theme = signal('azure-blue', ...(ngDevMode ? [{ debugName: "theme" }] : []));
468
+ theme = signal('azure-blue', ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
446
469
  /** Whether the panorama is visible (true when no blades are open) */
447
- isPanoramaVisible = computed(() => this.blades().length === 0, ...(ngDevMode ? [{ debugName: "isPanoramaVisible" }] : []));
470
+ isPanoramaVisible = computed(() => this.blades().length === 0, ...(ngDevMode ? [{ debugName: "isPanoramaVisible" }] : /* istanbul ignore next */ []));
448
471
  /** Number of open blades */
449
- bladeCount = computed(() => this.blades().length, ...(ngDevMode ? [{ debugName: "bladeCount" }] : []));
472
+ bladeCount = computed(() => this.blades().length, ...(ngDevMode ? [{ debugName: "bladeCount" }] : /* istanbul ignore next */ []));
450
473
  /** Positioned tiles with layout coordinates */
451
474
  positionedTiles = computed(() => {
452
475
  const pano = this.panorama();
453
476
  return pano.tiles;
454
- }, ...(ngDevMode ? [{ debugName: "positionedTiles" }] : []));
477
+ }, ...(ngDevMode ? [{ debugName: "positionedTiles" }] : /* istanbul ignore next */ []));
455
478
  /** Consumer label overrides from PortalConfig — re-applied on every language switch */
456
479
  _configLabelOverrides = {};
457
480
  /**
@@ -559,20 +582,26 @@ class PortalService {
559
582
  }
560
583
  // --- Notification panel ---
561
584
  /** Show the notification panel */
562
- showNotification(path, width = 250) {
585
+ showNotification(path, width = 250, lifecycle) {
586
+ lifecycle?.onShow?.();
563
587
  this.notification.update((n) => ({
564
588
  ...n,
565
589
  path,
566
590
  width,
567
591
  isVisible: true,
592
+ lifecycle,
568
593
  }));
569
594
  }
570
- /** Hide the notification panel */
595
+ /** Hide the notification panel. Aborted if onHide() returns false. */
571
596
  hideNotification() {
597
+ const current = this.notification();
598
+ if (current.lifecycle?.onHide?.() === false)
599
+ return;
572
600
  this.notification.update((n) => ({
573
601
  ...n,
574
602
  path: '',
575
603
  isVisible: false,
604
+ lifecycle: undefined,
576
605
  }));
577
606
  }
578
607
  // --- Avatar menu ---
@@ -591,10 +620,64 @@ class PortalService {
591
620
  setAvatarMenuItems(items) {
592
621
  this.avatarMenu.update((m) => ({ ...m, items }));
593
622
  }
594
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PortalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
595
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PortalService, providedIn: 'root' });
623
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: PortalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
624
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: PortalService, providedIn: 'root' });
625
+ }
626
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: PortalService, decorators: [{
627
+ type: Injectable,
628
+ args: [{ providedIn: 'root' }]
629
+ }] });
630
+
631
+ /**
632
+ * Registry for mapping blade paths to Angular components.
633
+ *
634
+ * Allows consumer apps to register components for blade paths,
635
+ * enabling dynamic rendering in BladeHostComponent without
636
+ * manual @switch blocks.
637
+ *
638
+ * Usage in app bootstrap:
639
+ * ```typescript
640
+ * const registry = inject(BladeRegistry);
641
+ * registry.register('customers', CustomerNavComponent);
642
+ * registry.register('customers/list', CustomerListComponent, { title: 'All Customers', width: 585 });
643
+ * ```
644
+ *
645
+ * Or register multiple at once:
646
+ * ```typescript
647
+ * registry.registerAll({
648
+ * 'customers': CustomerNavComponent,
649
+ * 'customers/list': CustomerListComponent,
650
+ * });
651
+ * ```
652
+ */
653
+ class BladeRegistry {
654
+ registry = new Map();
655
+ /** Register a component for a blade path with optional metadata (title, width, params) */
656
+ register(path, component, metadata) {
657
+ this.registry.set(path.toLowerCase(), { component, ...metadata });
658
+ }
659
+ /** Register multiple blade path → component mappings */
660
+ registerAll(mappings) {
661
+ for (const [path, component] of Object.entries(mappings)) {
662
+ this.register(path, component);
663
+ }
664
+ }
665
+ /** Get the component registered for a path, if any */
666
+ get(path) {
667
+ return this.registry.get(path.toLowerCase())?.component;
668
+ }
669
+ /** Get the full registry entry (component + metadata) for a path */
670
+ getEntry(path) {
671
+ return this.registry.get(path.toLowerCase());
672
+ }
673
+ /** Check if a component is registered for a path */
674
+ has(path) {
675
+ return this.registry.has(path.toLowerCase());
676
+ }
677
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
678
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeRegistry, providedIn: 'root' });
596
679
  }
597
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PortalService, decorators: [{
680
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeRegistry, decorators: [{
598
681
  type: Injectable,
599
682
  args: [{ providedIn: 'root' }]
600
683
  }] });
@@ -605,18 +688,92 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
605
688
  *
606
689
  * Manages the blade stack: adding, removing, cascade-closing,
607
690
  * and panorama visibility toggling.
691
+ *
692
+ * When a BladeRegistry is available, metadata (title, width) from the
693
+ * registry is used as defaults — explicit arguments take precedence.
608
694
  */
609
695
  class BladeService {
610
696
  portal = inject(PortalService);
697
+ registry = inject(BladeRegistry);
698
+ /**
699
+ * Unsaved-changes guard registry: blade path -> predicate returning true when the blade has
700
+ * unsaved edits. Populated by apa-blade-detail (from the projected NgForm). Consulted before a
701
+ * blade is closed/replaced or the page is left, so the user can confirm discarding changes.
702
+ * Message can be localised via PortalLabels.unsavedChangesConfirm; the navigation flow is
703
+ * synchronous, so a synchronous window.confirm is used.
704
+ */
705
+ dirtyChecks = new Map();
706
+ /** Optional override for the confirmation message (defaults to the de-CH label). */
707
+ unsavedChangesMessage = DEFAULT_LABELS.unsavedChangesConfirm;
708
+ constructor() {
709
+ // Native browser guard when the whole page/tab is closed or reloaded with unsaved edits.
710
+ if (typeof window !== 'undefined') {
711
+ window.addEventListener('beforeunload', (e) => {
712
+ if (this.anyDirty()) {
713
+ e.preventDefault();
714
+ e.returnValue = '';
715
+ }
716
+ });
717
+ }
718
+ }
719
+ /** Register a blade's unsaved-changes predicate (called by apa-blade-detail). */
720
+ registerDirtyCheck(path, isDirty) {
721
+ this.dirtyChecks.set(path.toLowerCase(), isDirty);
722
+ }
723
+ /** Remove a blade's unsaved-changes predicate (called when the detail is destroyed). */
724
+ unregisterDirtyCheck(path) {
725
+ this.dirtyChecks.delete(path.toLowerCase());
726
+ }
727
+ isPathDirty(path) {
728
+ const check = this.dirtyChecks.get(path.toLowerCase());
729
+ try {
730
+ return check ? check() === true : false;
731
+ }
732
+ catch {
733
+ return false;
734
+ }
735
+ }
736
+ anyDirty() {
737
+ for (const path of this.dirtyChecks.keys()) {
738
+ if (this.isPathDirty(path))
739
+ return true;
740
+ }
741
+ return false;
742
+ }
743
+ /**
744
+ * Returns true if it's OK to proceed with removing the given blade paths: none of them is dirty,
745
+ * or the user confirmed discarding the changes. Shows one confirmation regardless of how many
746
+ * dirty blades are affected.
747
+ */
748
+ mayDiscard(removedPaths) {
749
+ const anyDirty = removedPaths.some((p) => this.isPathDirty(p));
750
+ if (!anyDirty)
751
+ return true;
752
+ return typeof window === 'undefined' ? true : window.confirm(this.unsavedChangesMessage);
753
+ }
754
+ /** Drop dirty-check entries for paths that are no longer open. */
755
+ pruneDirtyChecks() {
756
+ const open = new Set(this.portal.blades().map((b) => b.path));
757
+ for (const path of [...this.dirtyChecks.keys()]) {
758
+ if (!open.has(path))
759
+ this.dirtyChecks.delete(path);
760
+ }
761
+ }
611
762
  /**
612
763
  * Set the first blade (e.g., when opening a top-level item from a tile).
613
764
  * Clears all existing blades, hides panorama, and adds the new blade.
614
765
  *
615
766
  * Ported from AreaBlades.setFirstBlade() in v0.2.346.
616
767
  */
617
- setFirstBlade(path, title = '', width = 315) {
768
+ setFirstBlade(path, title = '', width) {
769
+ const existing = this.portal.blades();
770
+ if (existing.length > 0 && !this.mayDiscard(existing.map((b) => b.path))) {
771
+ return existing[0];
772
+ }
773
+ this.dirtyChecks.clear();
618
774
  this.portal.blades.set([]);
619
- const blade = createBlade(path.toLowerCase(), title, width);
775
+ const entry = this.registry.getEntry(path);
776
+ const blade = createBlade(path.toLowerCase(), title || entry?.title || path, width ?? entry?.width ?? 315);
620
777
  this.portal.blades.set([blade]);
621
778
  return blade;
622
779
  }
@@ -626,21 +783,23 @@ class BladeService {
626
783
  *
627
784
  * Ported from AreaBlades.addBlade() in v0.2.346.
628
785
  */
629
- addBlade(path, senderPath = '', title = '', width = 315) {
786
+ addBlade(path, senderPath = '', title = '', width, params) {
630
787
  if (!path)
631
788
  return undefined;
632
789
  const normalizedPath = path.toLowerCase();
633
- const blades = this.portal.blades();
634
- // Check if blade already exists
635
- const existing = blades.find((b) => b.path === normalizedPath);
790
+ // Cascade close first: remove blades after the sender
791
+ // This ensures a blade at the same path gets recreated with new params
792
+ if (senderPath) {
793
+ if (!this.clearChild(senderPath))
794
+ return undefined;
795
+ }
796
+ // Check if blade already exists (after cascade close)
797
+ const existing = this.portal.blades().find((b) => b.path === normalizedPath);
636
798
  if (existing) {
637
799
  return existing;
638
800
  }
639
- // Cascade close: remove blades after the sender
640
- if (senderPath) {
641
- this.clearChild(senderPath);
642
- }
643
- const blade = createBlade(normalizedPath, title, width);
801
+ const entry = this.registry.getEntry(normalizedPath);
802
+ const blade = createBlade(normalizedPath, title || entry?.title || normalizedPath, width ?? entry?.width ?? 315, params);
644
803
  this.portal.blades.update((b) => [...b, blade]);
645
804
  return blade;
646
805
  }
@@ -648,7 +807,7 @@ class BladeService {
648
807
  * Open a blade from a navigation event (e.g., tile click, nav item click).
649
808
  * Wraps addBlade with AddBladeEventArgs for compatibility.
650
809
  */
651
- openBlade(args, title = '', width = 315) {
810
+ openBlade(args, title = '', width) {
652
811
  return this.addBlade(args.path, args.pathSender, title, width);
653
812
  }
654
813
  /**
@@ -656,7 +815,10 @@ class BladeService {
656
815
  * Ported from AreaBlades.clearAll() in v0.2.346.
657
816
  */
658
817
  clearAll() {
818
+ if (!this.mayDiscard(this.portal.blades().map((b) => b.path)))
819
+ return;
659
820
  this.portal.blades.set([]);
821
+ this.dirtyChecks.clear();
660
822
  }
661
823
  /**
662
824
  * Remove a specific blade and all blades to its right.
@@ -669,7 +831,10 @@ class BladeService {
669
831
  const blades = this.portal.blades();
670
832
  const index = blades.findIndex((b) => b.path === normalizedPath);
671
833
  if (index >= 0) {
834
+ if (!this.mayDiscard(blades.slice(index).map((b) => b.path)))
835
+ return false;
672
836
  this.portal.blades.set(blades.slice(0, index));
837
+ this.pruneDirtyChecks();
673
838
  }
674
839
  else {
675
840
  // Check notification area
@@ -678,6 +843,7 @@ class BladeService {
678
843
  this.portal.hideNotification();
679
844
  }
680
845
  }
846
+ return true;
681
847
  }
682
848
  /**
683
849
  * Remove all blades AFTER a given path (keeps the blade itself).
@@ -687,13 +853,17 @@ class BladeService {
687
853
  */
688
854
  clearChild(path) {
689
855
  if (!path)
690
- return;
856
+ return true;
691
857
  const normalizedPath = path.toLowerCase();
692
858
  const blades = this.portal.blades();
693
859
  const index = blades.findIndex((b) => b.path === normalizedPath);
694
860
  if (index >= 0) {
861
+ if (!this.mayDiscard(blades.slice(index + 1).map((b) => b.path)))
862
+ return false;
695
863
  this.portal.blades.set(blades.slice(0, index + 1));
864
+ this.pruneDirtyChecks();
696
865
  }
866
+ return true;
697
867
  }
698
868
  /**
699
869
  * Remove blades at and beyond a specific 1-based level.
@@ -703,7 +873,10 @@ class BladeService {
703
873
  const adjustedLevel = level <= 0 ? 1 : level;
704
874
  const blades = this.portal.blades();
705
875
  if (adjustedLevel <= blades.length) {
876
+ if (!this.mayDiscard(blades.slice(adjustedLevel - 1).map((b) => b.path)))
877
+ return;
706
878
  this.portal.blades.set(blades.slice(0, adjustedLevel - 1));
879
+ this.pruneDirtyChecks();
707
880
  }
708
881
  }
709
882
  /**
@@ -713,7 +886,10 @@ class BladeService {
713
886
  clearLastLevel() {
714
887
  const blades = this.portal.blades();
715
888
  if (blades.length > 0) {
889
+ if (!this.mayDiscard([blades[blades.length - 1].path]))
890
+ return;
716
891
  this.portal.blades.set(blades.slice(0, -1));
892
+ this.pruneDirtyChecks();
717
893
  }
718
894
  }
719
895
  /**
@@ -735,63 +911,313 @@ class BladeService {
735
911
  isBladeOpen(path) {
736
912
  return this.portal.blades().some((b) => b.path === path.toLowerCase());
737
913
  }
738
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
739
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeService, providedIn: 'root' });
914
+ /**
915
+ * Get URL-persisted parameters for a blade by path.
916
+ * Returns empty object if blade not found or has no params.
917
+ */
918
+ getBladeParams(path) {
919
+ return this.getBlade(path)?.params ?? {};
920
+ }
921
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
922
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeService, providedIn: 'root' });
740
923
  }
741
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeService, decorators: [{
924
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeService, decorators: [{
742
925
  type: Injectable,
743
926
  args: [{ providedIn: 'root' }]
744
- }] });
927
+ }], ctorParameters: () => [] });
745
928
 
929
+ /** @internal */
930
+ const BLADE_ROUTER_CONFIG = new InjectionToken('BLADE_ROUTER_CONFIG');
746
931
  /**
747
- * Registry for mapping blade paths to Angular components.
748
- *
749
- * Allows consumer apps to register components for blade paths,
750
- * enabling dynamic rendering in BladeHostComponent without
751
- * manual @switch blocks.
932
+ * Enables opt-in URL synchronization for the blade stack.
752
933
  *
753
- * Usage in app bootstrap:
934
+ * Add alongside `provideRouter()` and `providePortalAzure()`:
754
935
  * ```typescript
755
- * const registry = inject(BladeRegistry);
756
- * registry.register('customers', CustomerNavComponent);
757
- * registry.register('customers/list', CustomerListComponent);
936
+ * export const appConfig: ApplicationConfig = {
937
+ * providers: [
938
+ * provideRouter(routes),
939
+ * providePortalAzure({ title: 'My Portal', ... }),
940
+ * provideBladeRouter(),
941
+ * ],
942
+ * };
758
943
  * ```
759
944
  *
760
- * Or register multiple at once:
945
+ * Optionally pass a config to set a fixed route prefix:
761
946
  * ```typescript
762
- * registry.registerAll({
763
- * 'customers': CustomerNavComponent,
764
- * 'customers/list': CustomerListComponent,
765
- * });
947
+ * provideBladeRouter({ prefix: 'app' }) // → /app/customers/list
948
+ * provideBladeRouter({ prefix: '' }) // → /customers/list (no prefix)
766
949
  * ```
950
+ *
951
+ * Without this provider, blade navigation remains purely in-memory.
767
952
  */
768
- class BladeRegistry {
769
- registry = new Map();
770
- /** Register a component for a blade path */
771
- register(path, component) {
772
- this.registry.set(path.toLowerCase(), component);
953
+ function provideBladeRouter(config) {
954
+ return makeEnvironmentProviders([
955
+ BladeRouterService,
956
+ { provide: BLADE_ROUTER_CONFIG, useValue: config ?? {} },
957
+ {
958
+ provide: ENVIRONMENT_INITIALIZER,
959
+ multi: true,
960
+ useFactory: () => () => inject(BladeRouterService),
961
+ },
962
+ ]);
963
+ }
964
+
965
+ /**
966
+ * Optional service that syncs the blade stack with the browser URL.
967
+ *
968
+ * When enabled, blade paths are stored as URL path segments with matrix params:
969
+ * `/crm/customers/list/detail;id=1`
970
+ *
971
+ * This makes blade states bookmarkable, shareable, and back-button navigable.
972
+ *
973
+ * Opt-in: add `provideBladeRouter()` to your app providers alongside `provideRouter()`.
974
+ * Without it, blade navigation remains purely in-memory (no URL changes).
975
+ */
976
+ class BladeRouterService {
977
+ router = inject(Router);
978
+ portal = inject(PortalService);
979
+ registry = inject(BladeRegistry);
980
+ destroyRef = inject(DestroyRef);
981
+ config = inject(BLADE_ROUTER_CONFIG, { optional: true }) ?? {};
982
+ _syncingFromUrl = false;
983
+ _initialRestoreDone = false;
984
+ constructor() {
985
+ // Sync: blade stack changes → URL path segments
986
+ effect(() => {
987
+ const blades = this.portal.blades();
988
+ if (this._syncingFromUrl)
989
+ return;
990
+ if (blades.length === 0 && !this._initialRestoreDone)
991
+ return;
992
+ const prefix = this.getEffectivePrefix();
993
+ const bladePath = this.encodeBladesToPath(blades);
994
+ const targetUrl = prefix
995
+ ? (bladePath ? `/${prefix}/${bladePath}` : `/${prefix}`)
996
+ : (bladePath ? `/${bladePath}` : `/`);
997
+ const currentPath = this.router.url.split('?')[0].split(';')[0];
998
+ // Only navigate if the path actually changed (avoid loops)
999
+ if (this.normalizeUrl(currentPath) !== this.normalizeUrl(targetUrl)) {
1000
+ this.router.navigateByUrl(targetUrl);
1001
+ }
1002
+ });
1003
+ // Sync: URL path segments → blade stack (on NavigationEnd)
1004
+ const sub = this.router.events
1005
+ .pipe(filter((e) => e instanceof NavigationEnd))
1006
+ .subscribe((e) => {
1007
+ this._initialRestoreDone = true;
1008
+ const url = e.urlAfterRedirects;
1009
+ // Backward compat: handle legacy ?blades= query param
1010
+ const legacyParam = this.extractLegacyBladesParam(url);
1011
+ if (legacyParam !== null) {
1012
+ this.handleLegacyUrl(legacyParam);
1013
+ return;
1014
+ }
1015
+ this.restoreFromPath(url);
1016
+ });
1017
+ this.destroyRef.onDestroy(() => sub.unsubscribe());
773
1018
  }
774
- /** Register multiple blade path → component mappings */
775
- registerAll(mappings) {
776
- for (const [path, component] of Object.entries(mappings)) {
777
- this.register(path, component);
1019
+ /**
1020
+ * Encode the blade stack into URL path segments.
1021
+ * Each blade contributes its "short name" (suffix after parent prefix).
1022
+ * Blades with params get Angular matrix params appended.
1023
+ *
1024
+ * Example: [customers, customers/list, customers/detail{id:1}]
1025
+ * → "customers/list/detail;id=1"
1026
+ */
1027
+ encodeBladesToPath(blades) {
1028
+ if (blades.length === 0)
1029
+ return '';
1030
+ const segments = [];
1031
+ const allPaths = [];
1032
+ for (const blade of blades) {
1033
+ // Compute short name: strip the longest matching ancestor prefix
1034
+ let shortName = blade.path;
1035
+ for (let i = allPaths.length - 1; i >= 0; i--) {
1036
+ if (blade.path.startsWith(allPaths[i] + '/')) {
1037
+ shortName = blade.path.substring(allPaths[i].length + 1);
1038
+ break;
1039
+ }
1040
+ }
1041
+ // Append matrix params if any
1042
+ let segment = shortName;
1043
+ if (blade.params && Object.keys(blade.params).length > 0) {
1044
+ const paramStr = Object.entries(blade.params)
1045
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
1046
+ .join(';');
1047
+ segment += ';' + paramStr;
1048
+ }
1049
+ segments.push(segment);
1050
+ allPaths.push(blade.path);
778
1051
  }
1052
+ return segments.join('/');
779
1053
  }
780
- /** Get the component registered for a path, if any */
781
- get(path) {
782
- return this.registry.get(path.toLowerCase());
1054
+ /**
1055
+ * Decode URL path segments into blade definitions.
1056
+ * Resolves short segment names to full blade paths using the registry.
1057
+ *
1058
+ * Example: "customers/list/detail;id=1" with route prefix "crm"
1059
+ * → [customers, customers/list, customers/detail] with detail.params={id:'1'}
1060
+ */
1061
+ decodeBladesFromPath(pathAfterPrefix) {
1062
+ if (!pathAfterPrefix)
1063
+ return [];
1064
+ const rawSegments = pathAfterPrefix.split('/').filter(Boolean);
1065
+ const blades = [];
1066
+ const resolvedPaths = [];
1067
+ for (const rawSegment of rawSegments) {
1068
+ // Parse matrix params from segment: "detail;id=1" → name="detail", params={id:'1'}
1069
+ const { name, params } = this.parseSegment(rawSegment);
1070
+ // Resolve short name to full blade path
1071
+ const fullPath = this.resolveSegment(name, resolvedPaths);
1072
+ if (!fullPath) {
1073
+ console.warn(`[BladeRouter] Could not resolve segment "${name}" to a registered blade path`);
1074
+ continue;
1075
+ }
1076
+ const entry = this.registry.getEntry(fullPath);
1077
+ const blade = createBlade(fullPath, entry?.title ?? fullPath, entry?.width ?? 315, params);
1078
+ blades.push(blade);
1079
+ resolvedPaths.push(fullPath);
1080
+ }
1081
+ return blades;
783
1082
  }
784
- /** Check if a component is registered for a path */
785
- has(path) {
786
- return this.registry.has(path.toLowerCase());
1083
+ /**
1084
+ * Parse a URL segment into its name and matrix parameters.
1085
+ * "detail;id=1;mode=edit" → { name: "detail", params: { id: "1", mode: "edit" } }
1086
+ */
1087
+ parseSegment(segment) {
1088
+ const parts = segment.split(';');
1089
+ const name = decodeURIComponent(parts[0]);
1090
+ const params = {};
1091
+ for (let i = 1; i < parts.length; i++) {
1092
+ const eqIndex = parts[i].indexOf('=');
1093
+ if (eqIndex > 0) {
1094
+ const key = decodeURIComponent(parts[i].substring(0, eqIndex));
1095
+ const value = decodeURIComponent(parts[i].substring(eqIndex + 1));
1096
+ params[key] = value;
1097
+ }
1098
+ }
1099
+ return { name, params };
787
1100
  }
788
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
789
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeRegistry, providedIn: 'root' });
1101
+ /**
1102
+ * Resolve a short segment name to a full blade path using the registry.
1103
+ * Tries parent/segment first, then walks up ancestors.
1104
+ */
1105
+ resolveSegment(name, previousPaths) {
1106
+ // Direct match: the segment itself is a registered path
1107
+ if (this.registry.has(name)) {
1108
+ return name.toLowerCase();
1109
+ }
1110
+ // Try prepending each previous blade path (most recent first)
1111
+ for (let i = previousPaths.length - 1; i >= 0; i--) {
1112
+ const candidate = previousPaths[i] + '/' + name;
1113
+ if (this.registry.has(candidate)) {
1114
+ return candidate.toLowerCase();
1115
+ }
1116
+ }
1117
+ return null;
1118
+ }
1119
+ /** Restore blade stack from a path-based URL */
1120
+ restoreFromPath(url) {
1121
+ const prefix = this.getEffectivePrefix();
1122
+ const path = url.split('?')[0]; // strip query params
1123
+ let pathAfterPrefix;
1124
+ if (prefix) {
1125
+ const prefixPattern = '/' + prefix;
1126
+ if (!path.startsWith(prefixPattern))
1127
+ return;
1128
+ pathAfterPrefix = path.substring(prefixPattern.length + 1); // +1 for trailing /
1129
+ }
1130
+ else {
1131
+ // No prefix: everything after the leading / is blade path
1132
+ pathAfterPrefix = path.substring(1);
1133
+ }
1134
+ const newBlades = this.decodeBladesFromPath(pathAfterPrefix);
1135
+ const currentPaths = this.portal.blades().map((b) => b.path);
1136
+ const newPaths = newBlades.map((b) => b.path);
1137
+ // Skip if blade stack already matches (paths and params)
1138
+ if (newPaths.length === currentPaths.length &&
1139
+ newPaths.every((p, i) => p === currentPaths[i]) &&
1140
+ this.paramsMatch(newBlades, this.portal.blades())) {
1141
+ return;
1142
+ }
1143
+ this._syncingFromUrl = true;
1144
+ this.portal.blades.set(newBlades);
1145
+ // Restore parameter signal for backward compat (from deepest blade with params)
1146
+ const deepestWithParams = [...newBlades].reverse().find((b) => Object.keys(b.params).length > 0);
1147
+ if (deepestWithParams && deepestWithParams.params['id']) {
1148
+ this.portal.setParameter({
1149
+ action: 'edit',
1150
+ itemId: Number(deepestWithParams.params['id']) || 0,
1151
+ });
1152
+ }
1153
+ queueMicrotask(() => (this._syncingFromUrl = false));
1154
+ }
1155
+ /** Check if params match between two blade arrays */
1156
+ paramsMatch(a, b) {
1157
+ if (a.length !== b.length)
1158
+ return false;
1159
+ return a.every((blade, i) => {
1160
+ const aKeys = Object.keys(blade.params);
1161
+ const bKeys = Object.keys(b[i].params);
1162
+ if (aKeys.length !== bKeys.length)
1163
+ return false;
1164
+ return aKeys.every((k) => blade.params[k] === b[i].params[k]);
1165
+ });
1166
+ }
1167
+ /** Get the route prefix from the current URL (e.g., 'crm' from '/crm/...') */
1168
+ getRoutePrefix() {
1169
+ const url = this.router.url.split('?')[0];
1170
+ const firstSegment = url.split('/').filter(Boolean)[0] ?? '';
1171
+ return firstSegment.split(';')[0];
1172
+ }
1173
+ /** Normalize URL for comparison (strip trailing slashes) */
1174
+ normalizeUrl(url) {
1175
+ return url.replace(/\/+$/, '') || '/';
1176
+ }
1177
+ // --- Legacy backward compatibility ---
1178
+ /** Extract legacy `blades` query parameter from URL */
1179
+ extractLegacyBladesParam(url) {
1180
+ const qIndex = url.indexOf('?');
1181
+ if (qIndex === -1)
1182
+ return null;
1183
+ const params = new URLSearchParams(url.substring(qIndex));
1184
+ return params.get('blades');
1185
+ }
1186
+ /** Handle legacy ?blades= URL by redirecting to new path format */
1187
+ handleLegacyUrl(bladesParam) {
1188
+ const paths = bladesParam.split(',').filter(Boolean);
1189
+ if (paths.length === 0)
1190
+ return;
1191
+ // Create temporary blades to encode
1192
+ const blades = paths.map((path) => {
1193
+ const entry = this.registry.getEntry(path);
1194
+ return createBlade(path, entry?.title ?? path, entry?.width ?? 315);
1195
+ });
1196
+ const prefix = this.getEffectivePrefix();
1197
+ const bladePath = this.encodeBladesToPath(blades);
1198
+ const newUrl = prefix
1199
+ ? (bladePath ? `/${prefix}/${bladePath}` : `/${prefix}`)
1200
+ : (bladePath ? `/${bladePath}` : `/`);
1201
+ // Redirect to new format
1202
+ this.router.navigateByUrl(newUrl, { replaceUrl: true });
1203
+ }
1204
+ /**
1205
+ * Return the effective route prefix. If a prefix was configured via
1206
+ * `provideBladeRouter({ prefix })`, use it (including empty string).
1207
+ * Otherwise fall back to dynamically reading the first URL segment.
1208
+ */
1209
+ getEffectivePrefix() {
1210
+ if (this.config.prefix !== undefined) {
1211
+ return this.config.prefix;
1212
+ }
1213
+ return this.getRoutePrefix();
1214
+ }
1215
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeRouterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1216
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeRouterService });
790
1217
  }
791
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeRegistry, decorators: [{
792
- type: Injectable,
793
- args: [{ providedIn: 'root' }]
794
- }] });
1218
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeRouterService, decorators: [{
1219
+ type: Injectable
1220
+ }], ctorParameters: () => [] });
795
1221
 
796
1222
  /**
797
1223
  * Provide the angular-portal-azure library configuration.
@@ -834,27 +1260,32 @@ function providePortalAzure(config) {
834
1260
  * ```
835
1261
  */
836
1262
  class TileComponent {
837
- tile = input.required(...(ngDevMode ? [{ debugName: "tile" }] : []));
1263
+ tile = input.required(...(ngDevMode ? [{ debugName: "tile" }] : /* istanbul ignore next */ []));
838
1264
  tileClick = output();
839
1265
  onClick() {
840
- this.tileClick.emit(this.tile());
1266
+ if (!this.tile().disabled) {
1267
+ this.tileClick.emit(this.tile());
1268
+ }
841
1269
  }
842
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
843
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: TileComponent, isStandalone: true, selector: "apa-tile", inputs: { tile: { classPropertyName: "tile", publicName: "tile", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { tileClick: "tileClick" }, ngImport: i0, template: `
1270
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: TileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1271
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: TileComponent, isStandalone: true, selector: "apa-tile", inputs: { tile: { classPropertyName: "tile", publicName: "tile", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { tileClick: "tileClick" }, ngImport: i0, template: `
844
1272
  <section
845
1273
  class="fxs-tile fx-rightClick fxs-flowlayout-element"
846
1274
  [class.fxs-tilesize-normal]="tile().size === 'normal'"
847
1275
  [class.fxs-tilesize-mini]="tile().size === 'mini'"
848
1276
  [class.fxs-tilesize-herowide]="tile().size === 'herowide'"
849
1277
  [class.fxs-tilesize-small]="tile().size === 'small'">
850
- <div class="fxs-part fxs-part-clickable"
1278
+ <div class="fxs-part"
1279
+ [class.fxs-part-clickable]="!tile().disabled"
851
1280
  role="button"
852
- tabindex="0"
1281
+ [attr.tabindex]="tile().disabled ? -1 : 0"
853
1282
  [attr.aria-label]="tile().title"
1283
+ [attr.aria-disabled]="tile().disabled || null"
854
1284
  (click)="onClick()"
855
1285
  (keydown.enter)="onClick()"
856
1286
  (keydown.space)="onClick(); $event.preventDefault()"
857
- style="cursor:pointer;">
1287
+ [style.cursor]="tile().disabled ? 'default' : 'pointer'"
1288
+ [style.opacity]="tile().disabled ? 0.45 : 1">
858
1289
  <header class="fxs-part-title">
859
1290
  <h2 class="msportalfx-tooltip-overflow">{{ tile().title }}</h2>
860
1291
  @if (tile().subtitle) {
@@ -867,7 +1298,7 @@ class TileComponent {
867
1298
  </section>
868
1299
  `, isInline: true, styles: [":host{display:contents}\n"] });
869
1300
  }
870
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: TileComponent, decorators: [{
1301
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: TileComponent, decorators: [{
871
1302
  type: Component,
872
1303
  args: [{ selector: 'apa-tile', standalone: true, template: `
873
1304
  <section
@@ -876,14 +1307,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
876
1307
  [class.fxs-tilesize-mini]="tile().size === 'mini'"
877
1308
  [class.fxs-tilesize-herowide]="tile().size === 'herowide'"
878
1309
  [class.fxs-tilesize-small]="tile().size === 'small'">
879
- <div class="fxs-part fxs-part-clickable"
1310
+ <div class="fxs-part"
1311
+ [class.fxs-part-clickable]="!tile().disabled"
880
1312
  role="button"
881
- tabindex="0"
1313
+ [attr.tabindex]="tile().disabled ? -1 : 0"
882
1314
  [attr.aria-label]="tile().title"
1315
+ [attr.aria-disabled]="tile().disabled || null"
883
1316
  (click)="onClick()"
884
1317
  (keydown.enter)="onClick()"
885
1318
  (keydown.space)="onClick(); $event.preventDefault()"
886
- style="cursor:pointer;">
1319
+ [style.cursor]="tile().disabled ? 'default' : 'pointer'"
1320
+ [style.opacity]="tile().disabled ? 0.45 : 1">
887
1321
  <header class="fxs-part-title">
888
1322
  <h2 class="msportalfx-tooltip-overflow">{{ tile().title }}</h2>
889
1323
  @if (tile().subtitle) {
@@ -904,20 +1338,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
904
1338
  * Displays tiles on the startboard. When a tile is clicked, it opens
905
1339
  * the first blade via BladeService.setFirstBlade().
906
1340
  *
1341
+ * When `autoNavigate` is true (default), clicking a tile automatically
1342
+ * opens the first blade. Set it to false to handle tile clicks manually
1343
+ * via the `(tileClick)` output.
1344
+ *
907
1345
  * Usage:
908
1346
  * ```html
909
1347
  * <apa-panorama />
1348
+ * <apa-panorama [autoNavigate]="false" (tileClick)="onTileClick($event)" />
910
1349
  * ```
911
1350
  */
912
1351
  class PanoramaComponent {
913
1352
  portal = inject(PortalService);
914
1353
  bladeService = inject(BladeService);
1354
+ /** When false, tile clicks only emit tileClick without opening a blade. */
1355
+ autoNavigate = input(true, ...(ngDevMode ? [{ debugName: "autoNavigate" }] : /* istanbul ignore next */ []));
1356
+ /** Emitted when a tile is clicked (always, regardless of autoNavigate). */
1357
+ tileClick = output();
915
1358
  panorama = this.portal.panorama;
916
1359
  onTileClick(tile) {
917
- this.bladeService.setFirstBlade(tile.bladePath, tile.title);
1360
+ this.tileClick.emit(tile);
1361
+ if (this.autoNavigate()) {
1362
+ this.bladeService.setFirstBlade(tile.bladePath, tile.title);
1363
+ }
918
1364
  }
919
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PanoramaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
920
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: PanoramaComponent, isStandalone: true, selector: "apa-panorama", ngImport: i0, template: `
1365
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: PanoramaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1366
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: PanoramaComponent, isStandalone: true, selector: "apa-panorama", inputs: { autoNavigate: { classPropertyName: "autoNavigate", publicName: "autoNavigate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { tileClick: "tileClick" }, ngImport: i0, template: `
921
1367
  @if (portal.isPanoramaVisible()) {
922
1368
  <div class="fxs-panorama-homearea" [class.collapsed]="!panorama().showTiles">
923
1369
  <div class="fxs-startboard-target fxs-startboard fx-rightClick" [class.collapsed]="!panorama().showTiles">
@@ -943,7 +1389,7 @@ class PanoramaComponent {
943
1389
  }
944
1390
  `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "component", type: TileComponent, selector: "apa-tile", inputs: ["tile"], outputs: ["tileClick"] }] });
945
1391
  }
946
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PanoramaComponent, decorators: [{
1392
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: PanoramaComponent, decorators: [{
947
1393
  type: Component,
948
1394
  args: [{ selector: 'apa-panorama', standalone: true, imports: [TileComponent], template: `
949
1395
  @if (portal.isPanoramaVisible()) {
@@ -970,7 +1416,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
970
1416
  </div>
971
1417
  }
972
1418
  `, styles: [":host{display:contents}\n"] }]
973
- }] });
1419
+ }], propDecorators: { autoNavigate: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoNavigate", required: false }] }], tileClick: [{ type: i0.Output, args: ["tileClick"] }] } });
974
1420
 
975
1421
  /**
976
1422
  * Root portal shell component.
@@ -995,7 +1441,7 @@ class PortalLayoutComponent {
995
1441
  elementRef = inject(ElementRef);
996
1442
  injector = inject(Injector);
997
1443
  destroyRef = inject(DestroyRef);
998
- isDark = signal(false, ...(ngDevMode ? [{ debugName: "isDark" }] : []));
1444
+ isDark = signal(false, ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
999
1445
  /** Available languages from the preset registry */
1000
1446
  availableLanguages = Array.from(LANGUAGE_PRESETS.values()).map((p) => ({ code: p.code, displayName: p.displayName }));
1001
1447
  constructor() {
@@ -1110,8 +1556,8 @@ class PortalLayoutComponent {
1110
1556
  targetScroll = Math.min(targetScroll, currentScroll + viewportWidth);
1111
1557
  scrollContainer.scrollTo({ left: targetScroll, behavior: 'smooth' });
1112
1558
  }
1113
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PortalLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1114
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: PortalLayoutComponent, isStandalone: true, selector: "apa-portal-layout", ngImport: i0, template: `
1559
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: PortalLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1560
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: PortalLayoutComponent, isStandalone: true, selector: "apa-portal-layout", ngImport: i0, template: `
1115
1561
  <div class="fxs-portal fxs-theme-blue">
1116
1562
  <!-- Top bar -->
1117
1563
  <header class="fxs-topbar" role="banner">
@@ -1198,7 +1644,7 @@ class PortalLayoutComponent {
1198
1644
  </div>
1199
1645
  `, isInline: true });
1200
1646
  }
1201
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PortalLayoutComponent, decorators: [{
1647
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: PortalLayoutComponent, decorators: [{
1202
1648
  type: Component,
1203
1649
  args: [{
1204
1650
  selector: 'apa-portal-layout',
@@ -1304,7 +1750,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1304
1750
  * ```
1305
1751
  */
1306
1752
  class CommandBarComponent {
1307
- commands = input([], ...(ngDevMode ? [{ debugName: "commands" }] : []));
1753
+ commands = input([], ...(ngDevMode ? [{ debugName: "commands" }] : /* istanbul ignore next */ []));
1308
1754
  visibleCommands() {
1309
1755
  return this.commands().filter((c) => c.visible);
1310
1756
  }
@@ -1316,8 +1762,8 @@ class CommandBarComponent {
1316
1762
  }
1317
1763
  }
1318
1764
  }
1319
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1320
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: CommandBarComponent, isStandalone: true, selector: "apa-command-bar", inputs: { commands: { classPropertyName: "commands", publicName: "commands", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1765
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: CommandBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1766
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: CommandBarComponent, isStandalone: true, selector: "apa-command-bar", inputs: { commands: { classPropertyName: "commands", publicName: "commands", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1321
1767
  <nav class="fxs-commandBar" role="toolbar" aria-label="Commands">
1322
1768
  <ul class="fxs-commandBar-itemList" role="list">
1323
1769
  @for (cmd of visibleCommands(); track cmd.key) {
@@ -1344,7 +1790,7 @@ class CommandBarComponent {
1344
1790
  </nav>
1345
1791
  `, isInline: true, styles: [":host{display:block}\n"] });
1346
1792
  }
1347
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandBarComponent, decorators: [{
1793
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: CommandBarComponent, decorators: [{
1348
1794
  type: Component,
1349
1795
  args: [{ selector: 'apa-command-bar', standalone: true, template: `
1350
1796
  <nav class="fxs-commandBar" role="toolbar" aria-label="Commands">
@@ -1389,7 +1835,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1389
1835
  * ```
1390
1836
  */
1391
1837
  class BladeComponent {
1392
- blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : []));
1838
+ blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : /* istanbul ignore next */ []));
1393
1839
  bladeClose = output();
1394
1840
  portal = inject(PortalService);
1395
1841
  bladeService = inject(BladeService);
@@ -1398,8 +1844,8 @@ class BladeComponent {
1398
1844
  this.bladeClose.emit(b);
1399
1845
  this.bladeService.closeBlade(b);
1400
1846
  }
1401
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1402
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BladeComponent, isStandalone: true, selector: "apa-blade", inputs: { blade: { classPropertyName: "blade", publicName: "blade", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { bladeClose: "bladeClose" }, ngImport: i0, template: `
1847
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1848
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: BladeComponent, isStandalone: true, selector: "apa-blade", inputs: { blade: { classPropertyName: "blade", publicName: "blade", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { bladeClose: "bladeClose" }, ngImport: i0, template: `
1403
1849
  <section class="fxs-blade-locked fxs-blade fx-rightClick fxs-bladesize-small"
1404
1850
  role="region"
1405
1851
  [attr.aria-label]="blade().title"
@@ -1458,7 +1904,7 @@ class BladeComponent {
1458
1904
  </section>
1459
1905
  `, isInline: true, styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "component", type: CommandBarComponent, selector: "apa-command-bar", inputs: ["commands"] }] });
1460
1906
  }
1461
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeComponent, decorators: [{
1907
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeComponent, decorators: [{
1462
1908
  type: Component,
1463
1909
  args: [{ selector: 'apa-blade', standalone: true, imports: [CommandBarComponent], template: `
1464
1910
  <section class="fxs-blade-locked fxs-blade fx-rightClick fxs-bladesize-small"
@@ -1533,54 +1979,76 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1533
1979
  *
1534
1980
  * Usage:
1535
1981
  * ```html
1982
+ * <!-- Default: each component is wrapped in <apa-blade> -->
1536
1983
  * <apa-blade-host />
1984
+ *
1985
+ * <!-- No wrapper: components render directly (they manage their own blade chrome) -->
1986
+ * <apa-blade-host [wrapBlade]="false" />
1537
1987
  * ```
1538
1988
  */
1539
1989
  class BladeHostComponent {
1990
+ /** Whether to wrap each component in an `<apa-blade>` element. Default: true. */
1991
+ wrapBlade = input(true, ...(ngDevMode ? [{ debugName: "wrapBlade" }] : /* istanbul ignore next */ []));
1540
1992
  portal = inject(PortalService);
1541
1993
  registry = inject(BladeRegistry);
1542
1994
  getComponent(path) {
1543
1995
  return this.registry.get(path) ?? null;
1544
1996
  }
1545
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1546
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BladeHostComponent, isStandalone: true, selector: "apa-blade-host", ngImport: i0, template: `
1997
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1998
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: BladeHostComponent, isStandalone: true, selector: "apa-blade-host", inputs: { wrapBlade: { classPropertyName: "wrapBlade", publicName: "wrapBlade", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1547
1999
  <div id="apa-blade-area" class="fxs-journey-target fxs-journey">
1548
2000
  <div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
1549
- @for (blade of portal.blades(); track blade.path) {
2001
+ @for (blade of portal.blades(); track blade.uid) {
1550
2002
  <div class="azureportalblade fxs-stacklayout-child">
1551
- <apa-blade [blade]="blade">
2003
+ @if (wrapBlade()) {
2004
+ <apa-blade [blade]="blade">
2005
+ @if (getComponent(blade.path); as component) {
2006
+ <ng-container *ngComponentOutlet="component" />
2007
+ } @else {
2008
+ <p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
2009
+ }
2010
+ </apa-blade>
2011
+ } @else {
1552
2012
  @if (getComponent(blade.path); as component) {
1553
2013
  <ng-container *ngComponentOutlet="component" />
1554
2014
  } @else {
1555
2015
  <p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
1556
2016
  }
1557
- </apa-blade>
2017
+ }
1558
2018
  </div>
1559
2019
  }
1560
2020
  </div>
1561
2021
  </div>
1562
2022
  `, isInline: true, styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "component", type: BladeComponent, selector: "apa-blade", inputs: ["blade"], outputs: ["bladeClose"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }] });
1563
2023
  }
1564
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeHostComponent, decorators: [{
2024
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeHostComponent, decorators: [{
1565
2025
  type: Component,
1566
2026
  args: [{ selector: 'apa-blade-host', standalone: true, imports: [BladeComponent, NgComponentOutlet], template: `
1567
2027
  <div id="apa-blade-area" class="fxs-journey-target fxs-journey">
1568
2028
  <div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
1569
- @for (blade of portal.blades(); track blade.path) {
2029
+ @for (blade of portal.blades(); track blade.uid) {
1570
2030
  <div class="azureportalblade fxs-stacklayout-child">
1571
- <apa-blade [blade]="blade">
2031
+ @if (wrapBlade()) {
2032
+ <apa-blade [blade]="blade">
2033
+ @if (getComponent(blade.path); as component) {
2034
+ <ng-container *ngComponentOutlet="component" />
2035
+ } @else {
2036
+ <p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
2037
+ }
2038
+ </apa-blade>
2039
+ } @else {
1572
2040
  @if (getComponent(blade.path); as component) {
1573
2041
  <ng-container *ngComponentOutlet="component" />
1574
2042
  } @else {
1575
2043
  <p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
1576
2044
  }
1577
- </apa-blade>
2045
+ }
1578
2046
  </div>
1579
2047
  }
1580
2048
  </div>
1581
2049
  </div>
1582
2050
  `, styles: [":host{display:block;height:100%}\n"] }]
1583
- }] });
2051
+ }], propDecorators: { wrapBlade: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapBlade", required: false }] }] } });
1584
2052
 
1585
2053
  /**
1586
2054
  * Navigation blade content — renders a list of nav items.
@@ -1597,8 +2065,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1597
2065
  * ```
1598
2066
  */
1599
2067
  class BladeNavComponent {
1600
- items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
1601
- senderPath = input('', ...(ngDevMode ? [{ debugName: "senderPath" }] : []));
2068
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2069
+ senderPath = input('', ...(ngDevMode ? [{ debugName: "senderPath" }] : /* istanbul ignore next */ []));
1602
2070
  bladeService = inject(BladeService);
1603
2071
  visibleItems() {
1604
2072
  return this.items().filter((item) => item.isVisible);
@@ -1613,8 +2081,8 @@ class BladeNavComponent {
1613
2081
  }
1614
2082
  item.callback?.();
1615
2083
  }
1616
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1617
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BladeNavComponent, isStandalone: true, selector: "apa-blade-nav", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, senderPath: { classPropertyName: "senderPath", publicName: "senderPath", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2084
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2085
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: BladeNavComponent, isStandalone: true, selector: "apa-blade-nav", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, senderPath: { classPropertyName: "senderPath", publicName: "senderPath", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1618
2086
  <table class="azc-grid-full azc-grid-multiselectable" role="grid" aria-label="Navigation">
1619
2087
  <colgroup>
1620
2088
  <col class="col0" style="width:28px;">
@@ -1644,7 +2112,7 @@ class BladeNavComponent {
1644
2112
  </table>
1645
2113
  `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1}\n"] });
1646
2114
  }
1647
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeNavComponent, decorators: [{
2115
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeNavComponent, decorators: [{
1648
2116
  type: Component,
1649
2117
  args: [{ selector: 'apa-blade-nav', standalone: true, template: `
1650
2118
  <table class="azc-grid-full azc-grid-multiselectable" role="grid" aria-label="Navigation">
@@ -1697,11 +2165,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1697
2165
  * ```
1698
2166
  */
1699
2167
  class BladeGridComponent {
1700
- items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
1701
- senderPath = input('', ...(ngDevMode ? [{ debugName: "senderPath" }] : []));
1702
- displayField = input('title', ...(ngDevMode ? [{ debugName: "displayField" }] : []));
1703
- bladePathField = input('bladePath', ...(ngDevMode ? [{ debugName: "bladePathField" }] : []));
1704
- searchable = input(true, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
2168
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2169
+ senderPath = input('', ...(ngDevMode ? [{ debugName: "senderPath" }] : /* istanbul ignore next */ []));
2170
+ displayField = input('title', ...(ngDevMode ? [{ debugName: "displayField" }] : /* istanbul ignore next */ []));
2171
+ bladePathField = input('bladePath', ...(ngDevMode ? [{ debugName: "bladePathField" }] : /* istanbul ignore next */ []));
2172
+ idField = input('id', ...(ngDevMode ? [{ debugName: "idField" }] : /* istanbul ignore next */ []));
2173
+ iconClass = input('ti ti-point-filled', ...(ngDevMode ? [{ debugName: "iconClass" }] : /* istanbul ignore next */ []));
2174
+ searchable = input(true, ...(ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []));
1705
2175
  itemClick = output();
1706
2176
  searchText = '';
1707
2177
  bladeService = inject(BladeService);
@@ -1719,11 +2189,13 @@ class BladeGridComponent {
1719
2189
  this.itemClick.emit(item);
1720
2190
  const bladePath = item[this.bladePathField()];
1721
2191
  if (bladePath) {
1722
- this.bladeService.addBlade(bladePath, this.senderPath());
2192
+ const itemId = item[this.idField()];
2193
+ const params = itemId != null ? { id: String(itemId) } : undefined;
2194
+ this.bladeService.addBlade(bladePath, this.senderPath(), '', undefined, params);
1723
2195
  }
1724
2196
  }
1725
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1726
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BladeGridComponent, isStandalone: true, selector: "apa-blade-grid", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, senderPath: { classPropertyName: "senderPath", publicName: "senderPath", isSignal: true, isRequired: false, transformFunction: null }, displayField: { classPropertyName: "displayField", publicName: "displayField", isSignal: true, isRequired: false, transformFunction: null }, bladePathField: { classPropertyName: "bladePathField", publicName: "bladePathField", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, ngImport: i0, template: `
2197
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2198
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: BladeGridComponent, isStandalone: true, selector: "apa-blade-grid", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, senderPath: { classPropertyName: "senderPath", publicName: "senderPath", isSignal: true, isRequired: false, transformFunction: null }, displayField: { classPropertyName: "displayField", publicName: "displayField", isSignal: true, isRequired: false, transformFunction: null }, bladePathField: { classPropertyName: "bladePathField", publicName: "bladePathField", isSignal: true, isRequired: false, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: true, isRequired: false, transformFunction: null }, iconClass: { classPropertyName: "iconClass", publicName: "iconClass", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, ngImport: i0, template: `
1727
2199
  @if (searchable()) {
1728
2200
  <div style="padding:0 0 10px 0;">
1729
2201
  <input type="search"
@@ -1746,11 +2218,7 @@ class BladeGridComponent {
1746
2218
  (click)="onRowClick(item)"
1747
2219
  (keydown.enter)="onRowClick(item)">
1748
2220
  <td class="msportalfx-gridcolumn-asseticon" role="gridcell" aria-hidden="true">
1749
- <svg xmlns="http://www.w3.org/2000/svg" class="msportal-fx-svg-placeholder" viewBox="0 0 50 50" focusable="false" style="height:21px;width:21px;">
1750
- <rect class="msportalfx-svg-c04" x="19.8" y="39.4" width="10.6" height="3.4"/>
1751
- <polygon class="msportalfx-svg-c04" points="23.1,50 27,50 30.3,46.5 19.8,46.5"/>
1752
- <path class="msportalfx-svg-c20" d="M41.2 14.7v-.3c0-7.7-6.6-14.1-14.7-14.2-.2-.3-4.8.1-4.8.1-7.3.9-13 7-13 14.1 0 .2-.8 5.8 4.9 10.5 2.6 2.3 5.3 8.5 5.7 10.3l.3.6h10.6l.3-.6c.4-1.8 3.2-8 5.7-10.2C41.9 20.2 41.2 14.9 41.2 14.7z"/>
1753
- </svg>
2221
+ <i [class]="iconClass()" style="font-size:20px; color:var(--apa-accent);"></i>
1754
2222
  </td>
1755
2223
  <td tabindex="0" role="gridcell">
1756
2224
  <span>{{ getDisplayValue(item) }}</span>
@@ -1761,7 +2229,7 @@ class BladeGridComponent {
1761
2229
  </table>
1762
2230
  `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1}\n"] });
1763
2231
  }
1764
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeGridComponent, decorators: [{
2232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeGridComponent, decorators: [{
1765
2233
  type: Component,
1766
2234
  args: [{ selector: 'apa-blade-grid', standalone: true, template: `
1767
2235
  @if (searchable()) {
@@ -1786,11 +2254,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1786
2254
  (click)="onRowClick(item)"
1787
2255
  (keydown.enter)="onRowClick(item)">
1788
2256
  <td class="msportalfx-gridcolumn-asseticon" role="gridcell" aria-hidden="true">
1789
- <svg xmlns="http://www.w3.org/2000/svg" class="msportal-fx-svg-placeholder" viewBox="0 0 50 50" focusable="false" style="height:21px;width:21px;">
1790
- <rect class="msportalfx-svg-c04" x="19.8" y="39.4" width="10.6" height="3.4"/>
1791
- <polygon class="msportalfx-svg-c04" points="23.1,50 27,50 30.3,46.5 19.8,46.5"/>
1792
- <path class="msportalfx-svg-c20" d="M41.2 14.7v-.3c0-7.7-6.6-14.1-14.7-14.2-.2-.3-4.8.1-4.8.1-7.3.9-13 7-13 14.1 0 .2-.8 5.8 4.9 10.5 2.6 2.3 5.3 8.5 5.7 10.3l.3.6h10.6l.3-.6c.4-1.8 3.2-8 5.7-10.2C41.9 20.2 41.2 14.9 41.2 14.7z"/>
1793
- </svg>
2257
+ <i [class]="iconClass()" style="font-size:20px; color:var(--apa-accent);"></i>
1794
2258
  </td>
1795
2259
  <td tabindex="0" role="gridcell">
1796
2260
  <span>{{ getDisplayValue(item) }}</span>
@@ -1800,7 +2264,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1800
2264
  </tbody>
1801
2265
  </table>
1802
2266
  `, styles: [":host{display:flex;flex-direction:column;flex:1}\n"] }]
1803
- }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], senderPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "senderPath", required: false }] }], displayField: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayField", required: false }] }], bladePathField: [{ type: i0.Input, args: [{ isSignal: true, alias: "bladePathField", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
2267
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], senderPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "senderPath", required: false }] }], displayField: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayField", required: false }] }], bladePathField: [{ type: i0.Input, args: [{ isSignal: true, alias: "bladePathField", required: false }] }], idField: [{ type: i0.Input, args: [{ isSignal: true, alias: "idField", required: false }] }], iconClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconClass", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
1804
2268
 
1805
2269
  /**
1806
2270
  * Detail/edit blade content — renders a form area for editing an item.
@@ -1824,22 +2288,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1824
2288
  * ```
1825
2289
  */
1826
2290
  class BladeDetailComponent {
1827
- blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : []));
1828
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1829
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.0", type: BladeDetailComponent, isStandalone: true, selector: "apa-blade-detail", inputs: { blade: { classPropertyName: "blade", publicName: "blade", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2291
+ blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : /* istanbul ignore next */ []));
2292
+ bladeService = inject(BladeService);
2293
+ /** The template-driven form projected into this detail, if any (auto-discovered). */
2294
+ form = contentChild(NgForm, { ...(ngDevMode ? { debugName: "form" } : /* istanbul ignore next */ {}), descendants: true });
2295
+ ngAfterContentInit() {
2296
+ const blade = this.blade();
2297
+ const isDirty = () => this.form()?.dirty ?? false;
2298
+ // Expose on the lifecycle (for consumers) and register with the BladeService so navigation
2299
+ // away from a dirty blade prompts for confirmation. No per-detail wiring needed — every
2300
+ // detail that projects a <form> is guarded automatically.
2301
+ blade.lifecycle.isDirty = isDirty;
2302
+ this.bladeService.registerDirtyCheck(blade.path, isDirty);
2303
+ // After a successful save, reset the form to pristine so the guard does not fire on a
2304
+ // saved-but-not-yet-navigated blade. Wrap (don't replace) any existing onSavedItem hook.
2305
+ const originalOnSaved = blade.lifecycle.onSavedItem;
2306
+ blade.lifecycle.onSavedItem = () => {
2307
+ this.form()?.form.markAsPristine();
2308
+ originalOnSaved?.();
2309
+ };
2310
+ }
2311
+ ngOnDestroy() {
2312
+ this.bladeService.unregisterDirtyCheck(this.blade().path);
2313
+ }
2314
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2315
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.3", type: BladeDetailComponent, isStandalone: true, selector: "apa-blade-detail", inputs: { blade: { classPropertyName: "blade", publicName: "blade", isSignal: true, isRequired: true, transformFunction: null } }, queries: [{ propertyName: "form", first: true, predicate: NgForm, descendants: true, isSignal: true }], ngImport: i0, template: `
1830
2316
  <div class="apa-blade-detail">
1831
2317
  <ng-content />
1832
2318
  </div>
1833
2319
  `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0}.apa-blade-detail{flex:1;display:flex;flex-direction:column;min-height:0}\n"] });
1834
2320
  }
1835
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BladeDetailComponent, decorators: [{
2321
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeDetailComponent, decorators: [{
1836
2322
  type: Component,
1837
2323
  args: [{ selector: 'apa-blade-detail', standalone: true, template: `
1838
2324
  <div class="apa-blade-detail">
1839
2325
  <ng-content />
1840
2326
  </div>
1841
2327
  `, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0}.apa-blade-detail{flex:1;display:flex;flex-direction:column;min-height:0}\n"] }]
1842
- }], propDecorators: { blade: [{ type: i0.Input, args: [{ isSignal: true, alias: "blade", required: true }] }] } });
2328
+ }], propDecorators: { blade: [{ type: i0.Input, args: [{ isSignal: true, alias: "blade", required: true }] }], form: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgForm), { ...{ descendants: true }, isSignal: true }] }] } });
1843
2329
  /**
1844
2330
  * Create standard detail blade commands (new, save, delete, cancel).
1845
2331
  * Convenience function for setting up typical detail/edit blade commands
@@ -1875,11 +2361,22 @@ function createDetailCommands(handlers, labels = DEFAULT_LABELS) {
1875
2361
  */
1876
2362
  class NotificationPanelComponent {
1877
2363
  portal = inject(PortalService);
2364
+ wasVisible = false;
2365
+ constructor() {
2366
+ effect(() => {
2367
+ const visible = this.portal.notification().isVisible;
2368
+ if (visible && !this.wasVisible) {
2369
+ const lifecycle = this.portal.notification().lifecycle;
2370
+ queueMicrotask(() => lifecycle?.onShowed?.());
2371
+ }
2372
+ this.wasVisible = visible;
2373
+ });
2374
+ }
1878
2375
  onClose() {
1879
2376
  this.portal.hideNotification();
1880
2377
  }
1881
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NotificationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1882
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: NotificationPanelComponent, isStandalone: true, selector: "apa-notification-panel", ngImport: i0, template: `
2378
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: NotificationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2379
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: NotificationPanelComponent, isStandalone: true, selector: "apa-notification-panel", ngImport: i0, template: `
1883
2380
  @if (portal.notification().isVisible) {
1884
2381
  <div class="apa-notification-panel"
1885
2382
  role="complementary"
@@ -1897,7 +2394,7 @@ class NotificationPanelComponent {
1897
2394
  }
1898
2395
  `, isInline: true, styles: [":host{display:contents}\n"] });
1899
2396
  }
1900
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: NotificationPanelComponent, decorators: [{
2397
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: NotificationPanelComponent, decorators: [{
1901
2398
  type: Component,
1902
2399
  args: [{ selector: 'apa-notification-panel', standalone: true, template: `
1903
2400
  @if (portal.notification().isVisible) {
@@ -1916,7 +2413,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1916
2413
  </div>
1917
2414
  }
1918
2415
  `, styles: [":host{display:contents}\n"] }]
1919
- }] });
2416
+ }], ctorParameters: () => [] });
1920
2417
 
1921
2418
  /**
1922
2419
  * Avatar menu — user account dropdown in the portal header.
@@ -1940,8 +2437,8 @@ class AvatarMenuComponent {
1940
2437
  displayName() {
1941
2438
  return getUserDisplayName(this.userAccount());
1942
2439
  }
1943
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AvatarMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1944
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: AvatarMenuComponent, isStandalone: true, selector: "apa-avatar-menu", ngImport: i0, template: `
2440
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: AvatarMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2441
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: AvatarMenuComponent, isStandalone: true, selector: "apa-avatar-menu", ngImport: i0, template: `
1945
2442
  <div class="fxs-avatarmenu">
1946
2443
  <a class="fxs-has-hover" (click)="portal.toggleAvatarMenu()">
1947
2444
  <div class="fxs-avatarmenu-header">
@@ -1959,7 +2456,7 @@ class AvatarMenuComponent {
1959
2456
  </div>
1960
2457
  `, isInline: true });
1961
2458
  }
1962
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AvatarMenuComponent, decorators: [{
2459
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: AvatarMenuComponent, decorators: [{
1963
2460
  type: Component,
1964
2461
  args: [{
1965
2462
  selector: 'apa-avatar-menu',
@@ -1998,10 +2495,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1998
2495
  * ```
1999
2496
  */
2000
2497
  class SidebarComponent {
2001
- items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
2002
- collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
2003
- width = input(200, ...(ngDevMode ? [{ debugName: "width" }] : []));
2004
- collapsedWidth = input(50, ...(ngDevMode ? [{ debugName: "collapsedWidth" }] : []));
2498
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2499
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
2500
+ width = input(200, ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
2501
+ collapsedWidth = input(50, ...(ngDevMode ? [{ debugName: "collapsedWidth" }] : /* istanbul ignore next */ []));
2005
2502
  bladeService = inject(BladeService);
2006
2503
  visibleItems() {
2007
2504
  return this.items().filter((item) => item.isVisible);
@@ -2012,8 +2509,8 @@ class SidebarComponent {
2012
2509
  }
2013
2510
  item.callback?.();
2014
2511
  }
2015
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2016
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: SidebarComponent, isStandalone: true, selector: "apa-sidebar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2512
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2513
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: SidebarComponent, isStandalone: true, selector: "apa-sidebar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2017
2514
  <nav class="apa-sidebar" aria-label="Sidebar"
2018
2515
  [class.apa-sidebar-collapsed]="collapsed()"
2019
2516
  [style.width.px]="collapsed() ? collapsedWidth() : width()">
@@ -2040,7 +2537,7 @@ class SidebarComponent {
2040
2537
  </nav>
2041
2538
  `, isInline: true, styles: [".apa-sidebar{display:flex;flex-direction:column;background-color:var(--apa-chrome);height:100%;padding-top:10px;transition:width .2s ease}.apa-sidebar-item{display:flex;align-items:center;padding:10px 15px;color:var(--apa-chrome-text);text-decoration:none;font-size:13px;transition:background-color .15s ease}.apa-sidebar-item:hover{background-color:var(--apa-chrome-hover)}.apa-sidebar-icon{margin-right:10px;width:20px;text-align:center}.apa-sidebar-collapsed .apa-sidebar-icon{margin-right:0}.apa-sidebar-label{flex:1}.apa-sidebar-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;background-color:var(--apa-accent);color:#fff;font-size:11px;font-weight:600;line-height:1}\n"] });
2042
2539
  }
2043
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SidebarComponent, decorators: [{
2540
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: SidebarComponent, decorators: [{
2044
2541
  type: Component,
2045
2542
  args: [{ selector: 'apa-sidebar', standalone: true, template: `
2046
2543
  <nav class="apa-sidebar" aria-label="Sidebar"
@@ -2079,5 +2576,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
2079
2576
  * Generated bundle index. Do not edit.
2080
2577
  */
2081
2578
 
2082
- export { AvatarMenuComponent, BladeComponent, BladeDetailComponent, BladeGridComponent, BladeHostComponent, BladeNavComponent, BladeRegistry, BladeService, CommandBarComponent, DEFAULT_LABELS, LABELS_DE_CH, LABELS_DE_DE, LABELS_EN, LABELS_ES, LABELS_FR, LABELS_IT, LANGUAGE_PRESETS, NotificationPanelComponent, PanoramaComponent, PortalLayoutComponent, PortalService, SidebarComponent, TILE_DIMENSIONS, TileComponent, TileSize, clearStatusBar, createAvatarMenu, createBlade, createCommand, createDataBlade, createDetailCommands, createNavItem, createNotificationPanel, createPanorama, createTile, executeDeleteItem, executeLoadItem, executeLoadItems, executeSaveItem, filterItems, getUserDisplayName, layoutTiles, providePortalAzure, registerLanguagePreset, statusBarError, statusBarInfo, statusBarSuccess };
2579
+ export { AvatarMenuComponent, BLADE_ROUTER_CONFIG, BladeComponent, BladeDetailComponent, BladeGridComponent, BladeHostComponent, BladeNavComponent, BladeRegistry, BladeRouterService, BladeService, CommandBarComponent, DEFAULT_LABELS, LABELS_DE_CH, LABELS_DE_DE, LABELS_EN, LABELS_ES, LABELS_FR, LABELS_IT, LANGUAGE_PRESETS, NotificationPanelComponent, PanoramaComponent, PortalLayoutComponent, PortalService, SidebarComponent, TILE_DIMENSIONS, TileComponent, TileSize, clearStatusBar, createAvatarMenu, createBlade, createCommand, createDataBlade, createDetailCommands, createNavItem, createNotificationPanel, createPanorama, createTile, executeDeleteItem, executeLoadItem, executeLoadItems, executeSaveItem, filterItems, getUserDisplayName, layoutTiles, nextBladeUid, provideBladeRouter, providePortalAzure, registerLanguagePreset, statusBarError, statusBarInfo, statusBarSuccess };
2083
2580
  //# sourceMappingURL=ardimedia-angular-portal-azure.mjs.map