@zambon-dev/library 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -23,6 +23,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
23
 
24
24
  ### ⚠ Breaking Changes / Migration
25
25
 
26
+ ## [1.4.0] - 2026-09-08
27
+
28
+ ### Added
29
+
30
+ - **Sidebar menu items can now open an external destination instead of an internal route.**
31
+ `SidebarMenu` gained an optional `openMode` (`SidebarMenuOpenMode`): `Internal` (the default),
32
+ `ExternalNewTab`, or `ExternalEmbedded`. Read it through the exported
33
+ `toSidebarMenuOpenMode(menu.openMode)` helper rather than comparing the field directly — the
34
+ value is deserialized straight from your menu endpoint, so the helper accepts the mode as a
35
+ camelCase string, as a case-insensitive variant of it, or as the enum ordinal an ASP.NET Core
36
+ enum property serializes to. Anything unrecognized — an omitted field from a backend that
37
+ predates this feature, `null`, or a mode a newer version adds — degrades to `Internal`, so a
38
+ menu you have not touched behaves exactly as it does today.
39
+
40
+ - **`SidebarService.menuExternalUrlSelected`** — a separate output carrying the items whose mode
41
+ is not `Internal`. They are deliberately **not** announced on `menuUrlSelected`, which keeps its
42
+ existing contract: the URL it carries is always an Angular route. Acting on external items needs
43
+ `@zambon-dev/shared` (which opens the browser tab or the embedded view), or your own subscription
44
+ to the new output. Until then an external item highlights and does nothing, rather than sending
45
+ `https://…` to the router.
46
+
47
+ - **`SidebarConfigs.externalLinkText`** — the tooltip on items that open in a new browser tab.
48
+ Defaults to `Opens in a new browser tab` and, like `errorText` and `loadingText`, is rendered
49
+ as-is rather than through the translate pipe, so pass an already-localized string.
50
+
51
+ ### Changed
52
+
53
+ - **Selection now follows the open mode.** An item that opens in a new browser tab is treated as an
54
+ action rather than a destination: it takes no selection pill and, importantly, does not clear the
55
+ selection of the view the user is still looking at. Items that open embedded become real
56
+ application tabs and keep today’s selection behaviour exactly. A parent that also carries a URL
57
+ still expands, so it cannot become impossible to open.
58
+
59
+ - **Items that open in a new browser tab show a trailing outbound glyph.** It sits in the same
60
+ absolutely-positioned slot as the parent chevron, so a long label still ellipsises inside the
61
+ anchor and the collapsed rail is unchanged, and it fades in with the sidebar like the other
62
+ affordances. Embedded items get no glyph — they stay inside the application. Items with no
63
+ `openMode`, or with `openMode: Internal`, render exactly as before.
64
+
65
+ ### ⚠ Breaking Changes / Migration
66
+
67
+ None. `openMode` is optional, so a backend that does not send it produces today’s behaviour, and
68
+ `menuUrlSelected` is unchanged for internal items. If you subscribe to `menuUrlSelected` yourself
69
+ and want external items too, add a subscription to `menuExternalUrlSelected`.
70
+
26
71
  ## [1.3.2] - 2026-07-30
27
72
 
28
73
  ### Added
@@ -155,7 +200,8 @@ config options and the `getUserProfile()` method still exist but are no longer c
155
200
  available via [GitHub Releases](https://github.com/RicardoZambon/ZLibraries/releases) and the
156
201
  `library-v*` tags.
157
202
 
158
- [Unreleased]: https://github.com/RicardoZambon/ZLibraries/compare/library-v1.3.2...HEAD
203
+ [Unreleased]: https://github.com/RicardoZambon/ZLibraries/compare/library-v1.4.0...HEAD
204
+ [1.4.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/library-v1.4.0
159
205
  [1.3.2]: https://github.com/RicardoZambon/ZLibraries/releases/tag/library-v1.3.2
160
206
  [1.3.1]: https://github.com/RicardoZambon/ZLibraries/releases/tag/library-v1.3.1
161
207
  [1.2.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/library-v1.2.0
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Component, inject, Injectable, EventEmitter, InjectionToken, Input, Output, KeyValueDiffers, ViewContainerRef, TemplateRef, ElementRef, HostListener, ViewChild, ChangeDetectorRef, ChangeDetectionStrategy, NgZone, Directive, ContentChildren, Pipe, NgModule } from '@angular/core';
3
- import { Subject, ReplaySubject, take, throwError, tap, map, takeUntil, filter, debounceTime, startWith, pairwise, Observable, forkJoin, switchMap, of, delay } from 'rxjs';
3
+ import { Subject, ReplaySubject, take, throwError, tap, map, takeUntil, filter, debounceTime, startWith, pairwise, Observable, forkJoin, switchMap, of, delay, merge } from 'rxjs';
4
4
  import { Overlay, OverlayPositionBuilder } from '@angular/cdk/overlay';
5
5
  import { TemplatePortal } from '@angular/cdk/portal';
6
6
  import * as i1 from '@angular/common';
@@ -974,6 +974,8 @@ const SIDEBAR_CONFIGS = new InjectionToken('', {
974
974
  });
975
975
  class SidebarConfigs {
976
976
  errorText = 'Error';
977
+ /** Tooltip for items that open in a new browser tab. Rendered as-is, like {@link errorText}. */
978
+ externalLinkText = 'Opens in a new browser tab';
977
979
  loadingText = 'Loading';
978
980
  logoCollapsedPath;
979
981
  logoExpandedPath;
@@ -990,6 +992,14 @@ class SidebarMenu {
990
992
  id = 0;
991
993
  label = '';
992
994
  isSelected = false;
995
+ /**
996
+ * How {@link url} is opened. An absent value means {@link SidebarMenuOpenMode.Internal}.
997
+ *
998
+ * Left optional and without an initializer on purpose: the constructor is an `Object.assign`,
999
+ * which would overwrite a default with the `null` a nullable backend column serializes to.
1000
+ * Read it through `toSidebarMenuOpenMode(menu.openMode)` instead of comparing it directly.
1001
+ */
1002
+ openMode;
993
1003
  parent = null;
994
1004
  region;
995
1005
  url;
@@ -998,11 +1008,62 @@ class SidebarMenu {
998
1008
  }
999
1009
  }
1000
1010
 
1011
+ /**
1012
+ * How a {@link SidebarMenu}'s `url` is opened when the item is clicked.
1013
+ *
1014
+ * Read the value off a menu through {@link toSidebarMenuOpenMode} rather than comparing the
1015
+ * field directly: it crosses HTTP, so it can arrive as a string, as a number, as `null`, or not
1016
+ * at all.
1017
+ */
1018
+ var SidebarMenuOpenMode;
1019
+ (function (SidebarMenuOpenMode) {
1020
+ /** `url` is an internal Angular route and opens an application tab. The default. */
1021
+ SidebarMenuOpenMode["Internal"] = "internal";
1022
+ /** `url` is an absolute http(s) address and opens in a new browser tab. */
1023
+ SidebarMenuOpenMode["ExternalNewTab"] = "externalNewTab";
1024
+ /** `url` is an absolute http(s) address and opens embedded in an application tab. */
1025
+ SidebarMenuOpenMode["ExternalEmbedded"] = "externalEmbedded";
1026
+ })(SidebarMenuOpenMode || (SidebarMenuOpenMode = {}));
1027
+ /** Ordinals matching the mode column stored by the backend, for a numeric payload. */
1028
+ const ORDINAL_MODES = [
1029
+ SidebarMenuOpenMode.Internal,
1030
+ SidebarMenuOpenMode.ExternalNewTab,
1031
+ SidebarMenuOpenMode.ExternalEmbedded,
1032
+ ];
1033
+ /**
1034
+ * Parses the wire value of `SidebarMenu.openMode`.
1035
+ *
1036
+ * Deliberately a parser rather than a `?? Internal` fallback, because the field is deserialized
1037
+ * straight from each application's menu endpoint and its shape is not guaranteed:
1038
+ *
1039
+ * - a **number** is read as the backend's enum ordinal, which is what an ASP.NET Core enum
1040
+ * property serializes to unless that backend registers a string converter;
1041
+ * - a **string** is matched case-insensitively, so `'externalNewTab'` and `'ExternalNewTab'`
1042
+ * both work;
1043
+ * - anything else — an omitted field from a backend that predates this feature, `null`, or a
1044
+ * mode introduced by a newer one — degrades to {@link SidebarMenuOpenMode.Internal}.
1045
+ *
1046
+ * Degrading to `Internal` keeps an unrecognized mode behaving the way menus behaved before
1047
+ * external destinations existed, instead of producing an item that does nothing when clicked.
1048
+ */
1049
+ function toSidebarMenuOpenMode(value) {
1050
+ if (typeof value === 'number') {
1051
+ return ORDINAL_MODES[value] ?? SidebarMenuOpenMode.Internal;
1052
+ }
1053
+ if (typeof value === 'string') {
1054
+ const normalized = value.toLowerCase();
1055
+ return ORDINAL_MODES.find((mode) => mode.toLowerCase() === normalized)
1056
+ ?? SidebarMenuOpenMode.Internal;
1057
+ }
1058
+ return SidebarMenuOpenMode.Internal;
1059
+ }
1060
+
1001
1061
  class SidebarService {
1002
1062
  //#region ViewChilds, Inputs, Outputs
1003
1063
  childrenFailed = new EventEmitter();
1004
1064
  childrenLoading = new EventEmitter();
1005
1065
  childrenInitialized = new EventEmitter();
1066
+ menuExternalUrlSelected = new EventEmitter();
1006
1067
  menuUrlSelected = new EventEmitter();
1007
1068
  selectionChanged = new EventEmitter();
1008
1069
  //#endregion
@@ -1049,6 +1110,19 @@ class SidebarService {
1049
1110
  this.loadChildren(menu);
1050
1111
  }
1051
1112
  const hasUrl = (menu.url?.length ?? 0) > 0;
1113
+ const openMode = toSidebarMenuOpenMode(menu.openMode);
1114
+ const isExternal = hasUrl && openMode !== SidebarMenuOpenMode.Internal;
1115
+ // External destinations are announced on their own emitter, so a consumer that only knows
1116
+ // menuUrlSelected never receives an absolute URL it would hand to the Angular router.
1117
+ const urlSelected = isExternal ? this.menuExternalUrlSelected : this.menuUrlSelected;
1118
+ // Opening a new browser tab is an action, not a destination: the user stays on the view
1119
+ // already on screen, so the item must neither take the selection nor clear that view's.
1120
+ // Parents still fall through, so a parent that carries a URL keeps expanding instead of
1121
+ // becoming impossible to open.
1122
+ if (isExternal && openMode === SidebarMenuOpenMode.ExternalNewTab && menu.childCount === 0) {
1123
+ urlSelected.emit(menu);
1124
+ return;
1125
+ }
1052
1126
  // In case we're deselecting the menu because a child was selected, we need to keep track of the original selection state.
1053
1127
  const isSelected = menu.isSelected;
1054
1128
  if (!!this.selectedMenu && this.selectedMenu !== menu) {
@@ -1058,12 +1132,12 @@ class SidebarService {
1058
1132
  this.selectedMenu = menu;
1059
1133
  this.selectMenu(menu);
1060
1134
  if (hasUrl) {
1061
- this.menuUrlSelected.emit(menu);
1135
+ urlSelected.emit(menu);
1062
1136
  }
1063
1137
  }
1064
1138
  else if (hasUrl) {
1065
1139
  // Menu with a URL is already selected — navigate again instead of toggling off.
1066
- this.menuUrlSelected.emit(menu);
1140
+ urlSelected.emit(menu);
1067
1141
  }
1068
1142
  else {
1069
1143
  this.selectedMenu = null;
@@ -3123,6 +3197,7 @@ class SidebarItemComponent extends BaseComponent {
3123
3197
  isSelected = false;
3124
3198
  _childHeight = 0;
3125
3199
  changeDetectorRef = inject(ChangeDetectorRef);
3200
+ sidebarConfigs = inject(SIDEBAR_CONFIGS);
3126
3201
  sidebarService = inject(SidebarService);
3127
3202
  //#endregion
3128
3203
  //#region Properties
@@ -3132,6 +3207,9 @@ class SidebarItemComponent extends BaseComponent {
3132
3207
  get areChildrenLoaded() {
3133
3208
  return this.isParent && (this.menu.children?.length ?? 0) > 0;
3134
3209
  }
3210
+ get externalLinkText() {
3211
+ return this.sidebarConfigs.externalLinkText;
3212
+ }
3135
3213
  get isActive() {
3136
3214
  return this.sidebarService.isActive;
3137
3215
  }
@@ -3141,6 +3219,10 @@ class SidebarItemComponent extends BaseComponent {
3141
3219
  get isParent() {
3142
3220
  return this.menu.childCount > 0;
3143
3221
  }
3222
+ get opensInNewBrowserTab() {
3223
+ return (this.menu.url?.length ?? 0) > 0
3224
+ && toSidebarMenuOpenMode(this.menu.openMode) === SidebarMenuOpenMode.ExternalNewTab;
3225
+ }
3144
3226
  get hasIcon() {
3145
3227
  return !!this.menu.icon && this.menu.icon.length > 0;
3146
3228
  }
@@ -3216,7 +3298,7 @@ class SidebarItemComponent extends BaseComponent {
3216
3298
  this._childHeight = this.getChildHeight(this.menu);
3217
3299
  }
3218
3300
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: SidebarItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3219
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.6", type: SidebarItemComponent, isStandalone: true, selector: "lib-sidebar-item", inputs: { isLast: "isLast", level: "level", menu: "menu" }, host: { properties: { "class.active": "isActive", "class.expanded": "!isCollapsed", "class.first-level": "level === 0", "class.last-child": "isLast", "class.selected": "isSelected" } }, viewQueries: [{ propertyName: "menuContainer", first: true, predicate: ["menuContainer"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<li>\n <div #menuContainer\n [class.failed]=\"hasFailed\"\n [class.loading]=\"isLoading\"\n [class.parent]=\"isParent\"\n [class.selected]=\"isSelected\"\n (click)=\"onSelectItem()\">\n <a>\n <i *ngIf=\"hasIcon && level === 0\" class=\"fa-solid fa-fw {{ menu.icon }}\"></i>\n <span>{{ menu.label | translate }}</span>\n </a>\n\n <div class=\"loading\" *ngIf=\"isParent\">\n <svg class=\"animate-spin text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n </div>\n\n <ul *ngIf=\"isParent\"\n [class.open]=\"isSelected\"\n [ngStyle]=\"{\n 'height.px': childHeight,\n 'max-height.px': childHeight\n }\">\n <lib-sidebar-item *ngFor=\"let child of menu.children; trackBy: trackByFn; let last = last\"\n [menu]=\"child\"\n [level]=\"level+1\"\n [isLast]=\"last\">\n </lib-sidebar-item>\n </ul>\n</li>", styles: ["@charset \"UTF-8\";:host{position:relative;display:block}@media (max-width: 767px){:host:not(.active) li>ul{height:0px!important}}@media (min-width: 768px){:host:not(.expanded):not(.active) li>ul{height:0px!important}:host.expanded li>div{margin-inline:.5rem}:host.expanded li>div.parent:not(.loading):after{opacity:1}:host.expanded li>div.parent a{padding-right:2rem}:host.expanded li>div.parent.loading .loading{opacity:1!important}:host.expanded li>div a i{margin-left:0;font-size:1.125rem;line-height:1.75rem}:host.expanded li>div a span{opacity:1}}:host.active li>div{margin-inline:.5rem}:host.active li>div.parent:not(.loading):after{opacity:1}:host.active li>div.parent a{padding-right:2rem}:host.active li>div.parent.loading .loading{opacity:1!important}:host.active li>div a i{margin-left:0;font-size:1.125rem;line-height:1.75rem}:host.active li>div a span{opacity:1}:host.expanded li .selected:not(.loading).parent:after,:host.active li .selected:not(.loading).parent:after{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}li{position:relative;height:100%}li>div{position:relative;overflow:hidden;height:var(--sidebar-item-height);line-height:var(--sidebar-item-height);border-radius:var(--sidebar-item-radius);transition-property:margin,background-color;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration)}li>div:hover{background-color:var(--sidebar-item-hover-bg)}li>div.selected{background-color:var(--sidebar-item-selected-bg);color:var(--sidebar-item-selected-text)}li>div.parent:after{position:absolute;top:0;right:.75rem;width:10px;font-weight:800;transition-property:opacity,transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);opacity:0;font-family:\"Font Awesome 6 Free\";transition-duration:var(--sidebar-animation-duration);will-change:opacity,transform}li>div.parent:not(.failed):after{--tw-content: \"\\f104\";content:var(--tw-content)}li>div.parent.failed:after{right:1rem;--tw-content: \"\\f071\";content:var(--tw-content)}li>div a{display:block;height:100%;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:.75rem;padding-right:.75rem;text-align:left;width:var(--sidebar-expanded-width)}li>div a i{margin-right:.5rem;font-size:1.25rem;line-height:1.75rem;transition-property:font-size,line-height,margin-left;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);margin-left:var(--sidebar-icon-collapsed-margin);transition-duration:var(--sidebar-animation-duration);will-change:font-size,line-height,margin-left}li>div a span{text-overflow:ellipsis;opacity:0;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div .loading{position:absolute;top:0;right:.55rem;height:100%;width:1rem;opacity:0;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div .loading svg{height:100%;width:100%}li>ul{overflow:hidden;transition-property:height,max-height;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:height,max-height}li>ul a{padding-left:1.25rem}:host(:not(.first-level)) li>div>a{position:relative;padding-left:1.5rem}:host(:not(.first-level)) li>div>a:before{content:\"\";position:absolute;left:.6rem;top:0;bottom:0;width:0;border-left:1px solid var(--sidebar-tree-line)}:host(:not(.first-level)) li>div>a:after{content:\"\";position:absolute;left:.6rem;top:calc(50% - .55rem);width:.6rem;height:.55rem;border-left:1px solid var(--sidebar-tree-line);border-bottom:1px solid var(--sidebar-tree-line);border-bottom-left-radius:.4rem}:host(.last-child:not(.first-level)) li>div>a:before{bottom:calc(50% + .4rem)}\n"], dependencies: [{ kind: "component", type: SidebarItemComponent, selector: "lib-sidebar-item", inputs: ["isLast", "level", "menu"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
3301
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.6", type: SidebarItemComponent, isStandalone: true, selector: "lib-sidebar-item", inputs: { isLast: "isLast", level: "level", menu: "menu" }, host: { properties: { "class.active": "isActive", "class.expanded": "!isCollapsed", "class.first-level": "level === 0", "class.last-child": "isLast", "class.selected": "isSelected" } }, viewQueries: [{ propertyName: "menuContainer", first: true, predicate: ["menuContainer"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<li>\n <div #menuContainer\n [class.external]=\"opensInNewBrowserTab\"\n [class.failed]=\"hasFailed\"\n [class.loading]=\"isLoading\"\n [class.parent]=\"isParent\"\n [class.selected]=\"isSelected\"\n (click)=\"onSelectItem()\">\n <a [attr.title]=\"opensInNewBrowserTab ? externalLinkText : null\">\n <i *ngIf=\"hasIcon && level === 0\" class=\"fa-solid fa-fw {{ menu.icon }}\"></i>\n <span>{{ menu.label | translate }}</span>\n </a>\n\n <div class=\"loading\" *ngIf=\"isParent\">\n <svg class=\"animate-spin text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n </div>\n\n <ul *ngIf=\"isParent\"\n [class.open]=\"isSelected\"\n [ngStyle]=\"{\n 'height.px': childHeight,\n 'max-height.px': childHeight\n }\">\n <lib-sidebar-item *ngFor=\"let child of menu.children; trackBy: trackByFn; let last = last\"\n [menu]=\"child\"\n [level]=\"level+1\"\n [isLast]=\"last\">\n </lib-sidebar-item>\n </ul>\n</li>", styles: ["@charset \"UTF-8\";:host{position:relative;display:block}@media (max-width: 767px){:host:not(.active) li>ul{height:0px!important}}@media (min-width: 768px){:host:not(.expanded):not(.active) li>ul{height:0px!important}:host.expanded li>div{margin-inline:.5rem}:host.expanded li>div.external:after{opacity:1}:host.expanded li>div.external a{padding-right:2rem}:host.expanded li>div.parent:not(.loading):after{opacity:1}:host.expanded li>div.parent a{padding-right:2rem}:host.expanded li>div.parent.loading .loading{opacity:1!important}:host.expanded li>div a i{margin-left:0;font-size:1.125rem;line-height:1.75rem}:host.expanded li>div a span{opacity:1}}:host.active li>div{margin-inline:.5rem}:host.active li>div.external:after{opacity:1}:host.active li>div.external a{padding-right:2rem}:host.active li>div.parent:not(.loading):after{opacity:1}:host.active li>div.parent a{padding-right:2rem}:host.active li>div.parent.loading .loading{opacity:1!important}:host.active li>div a i{margin-left:0;font-size:1.125rem;line-height:1.75rem}:host.active li>div a span{opacity:1}:host.expanded li .selected:not(.loading).parent:after,:host.active li .selected:not(.loading).parent:after{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}li{position:relative;height:100%}li>div{position:relative;overflow:hidden;height:var(--sidebar-item-height);line-height:var(--sidebar-item-height);border-radius:var(--sidebar-item-radius);transition-property:margin,background-color;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration)}li>div:hover{background-color:var(--sidebar-item-hover-bg)}li>div.selected{background-color:var(--sidebar-item-selected-bg);color:var(--sidebar-item-selected-text)}li>div.external:after{position:absolute;top:0;right:.75rem;width:10px;font-size:.75rem;line-height:1rem;font-weight:800;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);opacity:0;--tw-content: \"\\f08e\";content:var(--tw-content);font-family:\"Font Awesome 6 Free\";transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div.parent:after{position:absolute;top:0;right:.75rem;width:10px;font-weight:800;transition-property:opacity,transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);opacity:0;font-family:\"Font Awesome 6 Free\";transition-duration:var(--sidebar-animation-duration);will-change:opacity,transform}li>div.parent:not(.failed):after{--tw-content: \"\\f104\";content:var(--tw-content)}li>div.parent.failed:after{right:1rem;--tw-content: \"\\f071\";content:var(--tw-content)}li>div a{display:block;height:100%;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:.75rem;padding-right:.75rem;text-align:left;width:var(--sidebar-expanded-width)}li>div a i{margin-right:.5rem;font-size:1.25rem;line-height:1.75rem;transition-property:font-size,line-height,margin-left;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);margin-left:var(--sidebar-icon-collapsed-margin);transition-duration:var(--sidebar-animation-duration);will-change:font-size,line-height,margin-left}li>div a span{text-overflow:ellipsis;opacity:0;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div .loading{position:absolute;top:0;right:.55rem;height:100%;width:1rem;opacity:0;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div .loading svg{height:100%;width:100%}li>ul{overflow:hidden;transition-property:height,max-height;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:height,max-height}li>ul a{padding-left:1.25rem}:host(:not(.first-level)) li>div>a{position:relative;padding-left:1.5rem}:host(:not(.first-level)) li>div>a:before{content:\"\";position:absolute;left:.6rem;top:0;bottom:0;width:0;border-left:1px solid var(--sidebar-tree-line)}:host(:not(.first-level)) li>div>a:after{content:\"\";position:absolute;left:.6rem;top:calc(50% - .55rem);width:.6rem;height:.55rem;border-left:1px solid var(--sidebar-tree-line);border-bottom:1px solid var(--sidebar-tree-line);border-bottom-left-radius:.4rem}:host(.last-child:not(.first-level)) li>div>a:before{bottom:calc(50% + .4rem)}\n"], dependencies: [{ kind: "component", type: SidebarItemComponent, selector: "lib-sidebar-item", inputs: ["isLast", "level", "menu"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
3220
3302
  }
3221
3303
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: SidebarItemComponent, decorators: [{
3222
3304
  type: Component,
@@ -3231,7 +3313,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
3231
3313
  '[class.first-level]': 'level === 0',
3232
3314
  '[class.last-child]': 'isLast',
3233
3315
  '[class.selected]': 'isSelected',
3234
- }, template: "<li>\n <div #menuContainer\n [class.failed]=\"hasFailed\"\n [class.loading]=\"isLoading\"\n [class.parent]=\"isParent\"\n [class.selected]=\"isSelected\"\n (click)=\"onSelectItem()\">\n <a>\n <i *ngIf=\"hasIcon && level === 0\" class=\"fa-solid fa-fw {{ menu.icon }}\"></i>\n <span>{{ menu.label | translate }}</span>\n </a>\n\n <div class=\"loading\" *ngIf=\"isParent\">\n <svg class=\"animate-spin text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n </div>\n\n <ul *ngIf=\"isParent\"\n [class.open]=\"isSelected\"\n [ngStyle]=\"{\n 'height.px': childHeight,\n 'max-height.px': childHeight\n }\">\n <lib-sidebar-item *ngFor=\"let child of menu.children; trackBy: trackByFn; let last = last\"\n [menu]=\"child\"\n [level]=\"level+1\"\n [isLast]=\"last\">\n </lib-sidebar-item>\n </ul>\n</li>", styles: ["@charset \"UTF-8\";:host{position:relative;display:block}@media (max-width: 767px){:host:not(.active) li>ul{height:0px!important}}@media (min-width: 768px){:host:not(.expanded):not(.active) li>ul{height:0px!important}:host.expanded li>div{margin-inline:.5rem}:host.expanded li>div.parent:not(.loading):after{opacity:1}:host.expanded li>div.parent a{padding-right:2rem}:host.expanded li>div.parent.loading .loading{opacity:1!important}:host.expanded li>div a i{margin-left:0;font-size:1.125rem;line-height:1.75rem}:host.expanded li>div a span{opacity:1}}:host.active li>div{margin-inline:.5rem}:host.active li>div.parent:not(.loading):after{opacity:1}:host.active li>div.parent a{padding-right:2rem}:host.active li>div.parent.loading .loading{opacity:1!important}:host.active li>div a i{margin-left:0;font-size:1.125rem;line-height:1.75rem}:host.active li>div a span{opacity:1}:host.expanded li .selected:not(.loading).parent:after,:host.active li .selected:not(.loading).parent:after{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}li{position:relative;height:100%}li>div{position:relative;overflow:hidden;height:var(--sidebar-item-height);line-height:var(--sidebar-item-height);border-radius:var(--sidebar-item-radius);transition-property:margin,background-color;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration)}li>div:hover{background-color:var(--sidebar-item-hover-bg)}li>div.selected{background-color:var(--sidebar-item-selected-bg);color:var(--sidebar-item-selected-text)}li>div.parent:after{position:absolute;top:0;right:.75rem;width:10px;font-weight:800;transition-property:opacity,transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);opacity:0;font-family:\"Font Awesome 6 Free\";transition-duration:var(--sidebar-animation-duration);will-change:opacity,transform}li>div.parent:not(.failed):after{--tw-content: \"\\f104\";content:var(--tw-content)}li>div.parent.failed:after{right:1rem;--tw-content: \"\\f071\";content:var(--tw-content)}li>div a{display:block;height:100%;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:.75rem;padding-right:.75rem;text-align:left;width:var(--sidebar-expanded-width)}li>div a i{margin-right:.5rem;font-size:1.25rem;line-height:1.75rem;transition-property:font-size,line-height,margin-left;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);margin-left:var(--sidebar-icon-collapsed-margin);transition-duration:var(--sidebar-animation-duration);will-change:font-size,line-height,margin-left}li>div a span{text-overflow:ellipsis;opacity:0;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div .loading{position:absolute;top:0;right:.55rem;height:100%;width:1rem;opacity:0;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div .loading svg{height:100%;width:100%}li>ul{overflow:hidden;transition-property:height,max-height;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:height,max-height}li>ul a{padding-left:1.25rem}:host(:not(.first-level)) li>div>a{position:relative;padding-left:1.5rem}:host(:not(.first-level)) li>div>a:before{content:\"\";position:absolute;left:.6rem;top:0;bottom:0;width:0;border-left:1px solid var(--sidebar-tree-line)}:host(:not(.first-level)) li>div>a:after{content:\"\";position:absolute;left:.6rem;top:calc(50% - .55rem);width:.6rem;height:.55rem;border-left:1px solid var(--sidebar-tree-line);border-bottom:1px solid var(--sidebar-tree-line);border-bottom-left-radius:.4rem}:host(.last-child:not(.first-level)) li>div>a:before{bottom:calc(50% + .4rem)}\n"] }]
3316
+ }, template: "<li>\n <div #menuContainer\n [class.external]=\"opensInNewBrowserTab\"\n [class.failed]=\"hasFailed\"\n [class.loading]=\"isLoading\"\n [class.parent]=\"isParent\"\n [class.selected]=\"isSelected\"\n (click)=\"onSelectItem()\">\n <a [attr.title]=\"opensInNewBrowserTab ? externalLinkText : null\">\n <i *ngIf=\"hasIcon && level === 0\" class=\"fa-solid fa-fw {{ menu.icon }}\"></i>\n <span>{{ menu.label | translate }}</span>\n </a>\n\n <div class=\"loading\" *ngIf=\"isParent\">\n <svg class=\"animate-spin text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n </div>\n\n <ul *ngIf=\"isParent\"\n [class.open]=\"isSelected\"\n [ngStyle]=\"{\n 'height.px': childHeight,\n 'max-height.px': childHeight\n }\">\n <lib-sidebar-item *ngFor=\"let child of menu.children; trackBy: trackByFn; let last = last\"\n [menu]=\"child\"\n [level]=\"level+1\"\n [isLast]=\"last\">\n </lib-sidebar-item>\n </ul>\n</li>", styles: ["@charset \"UTF-8\";:host{position:relative;display:block}@media (max-width: 767px){:host:not(.active) li>ul{height:0px!important}}@media (min-width: 768px){:host:not(.expanded):not(.active) li>ul{height:0px!important}:host.expanded li>div{margin-inline:.5rem}:host.expanded li>div.external:after{opacity:1}:host.expanded li>div.external a{padding-right:2rem}:host.expanded li>div.parent:not(.loading):after{opacity:1}:host.expanded li>div.parent a{padding-right:2rem}:host.expanded li>div.parent.loading .loading{opacity:1!important}:host.expanded li>div a i{margin-left:0;font-size:1.125rem;line-height:1.75rem}:host.expanded li>div a span{opacity:1}}:host.active li>div{margin-inline:.5rem}:host.active li>div.external:after{opacity:1}:host.active li>div.external a{padding-right:2rem}:host.active li>div.parent:not(.loading):after{opacity:1}:host.active li>div.parent a{padding-right:2rem}:host.active li>div.parent.loading .loading{opacity:1!important}:host.active li>div a i{margin-left:0;font-size:1.125rem;line-height:1.75rem}:host.active li>div a span{opacity:1}:host.expanded li .selected:not(.loading).parent:after,:host.active li .selected:not(.loading).parent:after{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}li{position:relative;height:100%}li>div{position:relative;overflow:hidden;height:var(--sidebar-item-height);line-height:var(--sidebar-item-height);border-radius:var(--sidebar-item-radius);transition-property:margin,background-color;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration)}li>div:hover{background-color:var(--sidebar-item-hover-bg)}li>div.selected{background-color:var(--sidebar-item-selected-bg);color:var(--sidebar-item-selected-text)}li>div.external:after{position:absolute;top:0;right:.75rem;width:10px;font-size:.75rem;line-height:1rem;font-weight:800;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);opacity:0;--tw-content: \"\\f08e\";content:var(--tw-content);font-family:\"Font Awesome 6 Free\";transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div.parent:after{position:absolute;top:0;right:.75rem;width:10px;font-weight:800;transition-property:opacity,transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);opacity:0;font-family:\"Font Awesome 6 Free\";transition-duration:var(--sidebar-animation-duration);will-change:opacity,transform}li>div.parent:not(.failed):after{--tw-content: \"\\f104\";content:var(--tw-content)}li>div.parent.failed:after{right:1rem;--tw-content: \"\\f071\";content:var(--tw-content)}li>div a{display:block;height:100%;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:.75rem;padding-right:.75rem;text-align:left;width:var(--sidebar-expanded-width)}li>div a i{margin-right:.5rem;font-size:1.25rem;line-height:1.75rem;transition-property:font-size,line-height,margin-left;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);margin-left:var(--sidebar-icon-collapsed-margin);transition-duration:var(--sidebar-animation-duration);will-change:font-size,line-height,margin-left}li>div a span{text-overflow:ellipsis;opacity:0;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div .loading{position:absolute;top:0;right:.55rem;height:100%;width:1rem;opacity:0;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:opacity}li>div .loading svg{height:100%;width:100%}li>ul{overflow:hidden;transition-property:height,max-height;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:var(--sidebar-animation-duration);will-change:height,max-height}li>ul a{padding-left:1.25rem}:host(:not(.first-level)) li>div>a{position:relative;padding-left:1.5rem}:host(:not(.first-level)) li>div>a:before{content:\"\";position:absolute;left:.6rem;top:0;bottom:0;width:0;border-left:1px solid var(--sidebar-tree-line)}:host(:not(.first-level)) li>div>a:after{content:\"\";position:absolute;left:.6rem;top:calc(50% - .55rem);width:.6rem;height:.55rem;border-left:1px solid var(--sidebar-tree-line);border-bottom:1px solid var(--sidebar-tree-line);border-bottom-left-radius:.4rem}:host(.last-child:not(.first-level)) li>div>a:before{bottom:calc(50% + .4rem)}\n"] }]
3235
3317
  }], ctorParameters: () => [], propDecorators: { menuContainer: [{
3236
3318
  type: ViewChild,
3237
3319
  args: ['menuContainer']
@@ -3305,7 +3387,7 @@ class SidebarComponent extends BaseComponent {
3305
3387
  this.updateShouldActivate();
3306
3388
  }
3307
3389
  ngOnInit() {
3308
- this.sidebarService.menuUrlSelected
3390
+ merge(this.sidebarService.menuUrlSelected, this.sidebarService.menuExternalUrlSelected)
3309
3391
  .pipe(takeUntil(this.destroy$))
3310
3392
  .subscribe((_menu) => this.deactivate());
3311
3393
  this.sidebarService.loadRoot()
@@ -3849,5 +3931,5 @@ class DateValidators {
3849
3931
  * Generated bundle index. Do not edit.
3850
3932
  */
3851
3933
 
3852
- export { BaseComponent, BaseDataset, CatalogSelectComponent, CatalogService, DataGridComponent, DataGridConfigsProvider, DataGridDataset, DataGridRowComponent, DataProviderService, DateHelpers, DateValidators, EnumTranslatePipe, FormGroupComponent, FormGroupService, FormInputComponent, FormInputGroupComponent, FormService, GridConfigsProvider, GridDataset, GroupAccordionComponent, GroupContainerComponent, GroupScrollSpyComponent, GuidHelper, LibraryModule, ModalComponent, MultiEditorComponent, MultiEditorDataset, MultiSelectComponent, MultiSelectResultDataset, MultiSelectResultGridComponent, ReplaceManyPipe, ReplacePipe, RibbonButtonComponent, RibbonComponent, RibbonGroupChild, RibbonGroupComponent, RouterFormatter, SIDEBAR_CONFIGS, ScrollSpyDirective, SidebarComponent, SidebarConfigs, SidebarMenu, SidebarService };
3934
+ export { BaseComponent, BaseDataset, CatalogSelectComponent, CatalogService, DataGridComponent, DataGridConfigsProvider, DataGridDataset, DataGridRowComponent, DataProviderService, DateHelpers, DateValidators, EnumTranslatePipe, FormGroupComponent, FormGroupService, FormInputComponent, FormInputGroupComponent, FormService, GridConfigsProvider, GridDataset, GroupAccordionComponent, GroupContainerComponent, GroupScrollSpyComponent, GuidHelper, LibraryModule, ModalComponent, MultiEditorComponent, MultiEditorDataset, MultiSelectComponent, MultiSelectResultDataset, MultiSelectResultGridComponent, ReplaceManyPipe, ReplacePipe, RibbonButtonComponent, RibbonComponent, RibbonGroupChild, RibbonGroupComponent, RouterFormatter, SIDEBAR_CONFIGS, ScrollSpyDirective, SidebarComponent, SidebarConfigs, SidebarMenu, SidebarMenuOpenMode, SidebarService, toSidebarMenuOpenMode };
3853
3935
  //# sourceMappingURL=zambon-dev-library.mjs.map