@zambon-dev/shared 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Component, inject, Injectable, ViewChild, Input, HostListener, NgModule, Pipe } from '@angular/core';
2
+ import { Component, inject, Injectable, ViewChild, HostListener, Input, EventEmitter, Output, NgModule, Pipe } from '@angular/core';
3
3
  import * as i1 from '@angular/router';
4
4
  import { RouterModule, Router, ActivatedRoute } from '@angular/router';
5
5
  import { AuthService, APP_CONFIG, TabService, Tab, TabsComponent, ButtonFiltersComponent, ModalBase, ChildList, ViewBase } from '@zambon-dev/framework';
6
6
  import { SidebarService, ModalComponent, SidebarComponent, DataGridDataset, DataGridComponent, DataProviderService } from '@zambon-dev/library';
7
7
  import { TranslatePipe, TranslateService, TranslateLoader, provideTranslateService } from '@ngx-translate/core';
8
- import { map, catchError, interval, mergeMap, tap, Subject, take, takeUntil, finalize, shareReplay, switchMap, of } from 'rxjs';
8
+ import { map, catchError, interval, mergeMap, tap, BehaviorSubject, Subject, take, takeUntil, finalize, shareReplay, switchMap, of } from 'rxjs';
9
9
  import { HttpClient, HttpBackend } from '@angular/common/http';
10
10
  import { JwtHelperService, JwtInterceptor } from '@auth0/angular-jwt';
11
- import { NgClass, NgFor, NgIf, JsonPipe, DatePipe } from '@angular/common';
11
+ import { HubConnectionState, HubConnectionBuilder } from '@microsoft/signalr';
12
+ import { AsyncPipe, NgIf, JsonPipe, DatePipe } from '@angular/common';
12
13
  import * as i1$1 from '@angular/forms';
13
14
  import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
14
15
  import { DomSanitizer } from '@angular/platform-browser';
@@ -94,6 +95,84 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
94
95
  }]
95
96
  }], ctorParameters: () => [] });
96
97
 
98
+ /**
99
+ * Streams top-bar notifications from a SignalR hub.
100
+ *
101
+ * The hub is configured via `AppConfig.notificationsEnabled` and `AppConfig.notificationsUrl`.
102
+ * When enabled, {@link start} opens a connection (authenticated with the current JWT) and
103
+ * listens for the server to push the notification list via the `ReceiveNotifications` client
104
+ * method. Consuming apps do not interact with this service directly — the top bar drives it.
105
+ */
106
+ class NotificationsService {
107
+ //#region Variables
108
+ authService = inject(AuthService);
109
+ config = inject(APP_CONFIG);
110
+ connection;
111
+ notifications$ = new BehaviorSubject([]);
112
+ //#endregion
113
+ //#region Properties
114
+ /** Whether the notifications feature is enabled (toggled on and given a hub URL). */
115
+ get isEnabled() {
116
+ return this.config.notificationsEnabled && this.config.notificationsUrl.length > 0;
117
+ }
118
+ //#endregion
119
+ //#region Public methods
120
+ /** Emits the current list of notifications. */
121
+ getNotifications() {
122
+ return this.notifications$.asObservable();
123
+ }
124
+ /** Emits the number of unread notifications. */
125
+ getUnreadCount() {
126
+ return this.notifications$.pipe(map((notifications) => notifications.filter((n) => !n.isRead).length));
127
+ }
128
+ /** Marks every notification as read and notifies the hub. */
129
+ markAllAsRead() {
130
+ this.notifications$.next(this.notifications$.value.map((n) => ({ ...n, isRead: true })));
131
+ if (this.connection?.state === HubConnectionState.Connected) {
132
+ this.connection.invoke('MarkAllAsRead').catch((error) => {
133
+ console.error('Failed to mark notifications as read on the hub.', error);
134
+ });
135
+ }
136
+ }
137
+ /** Marks a single notification as read (optimistically; the server reconciles on the next push). */
138
+ markAsRead(notification) {
139
+ this.notifications$.next(this.notifications$.value.map((n) => (n === notification ? { ...n, isRead: true } : n)));
140
+ }
141
+ /** Opens the SignalR connection and starts receiving notifications. Idempotent and a no-op when disabled. */
142
+ start() {
143
+ if (!this.isEnabled || !!this.connection) {
144
+ return;
145
+ }
146
+ this.connection = new HubConnectionBuilder()
147
+ .withUrl(this.config.notificationsUrl, {
148
+ accessTokenFactory: () => this.authService.token ?? '',
149
+ })
150
+ .withAutomaticReconnect()
151
+ .build();
152
+ this.connection.on('ReceiveNotifications', (notifications) => {
153
+ this.notifications$.next(notifications ?? []);
154
+ });
155
+ this.connection.start().catch((error) => {
156
+ console.error('Failed to connect to the notifications hub.', error);
157
+ });
158
+ }
159
+ /** Closes the SignalR connection. */
160
+ async stop() {
161
+ if (this.connection) {
162
+ await this.connection.stop();
163
+ this.connection = undefined;
164
+ }
165
+ }
166
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: NotificationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
167
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: NotificationsService, providedIn: 'root' });
168
+ }
169
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: NotificationsService, decorators: [{
170
+ type: Injectable,
171
+ args: [{
172
+ providedIn: 'root',
173
+ }]
174
+ }] });
175
+
97
176
  class OperationsHistoryService {
98
177
  //#region ViewChilds, Inputs, Outputs
99
178
  //#endregion
@@ -148,76 +227,137 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
148
227
  }]
149
228
  }] });
150
229
 
151
- class MainLayoutComponent {
230
+ class BrandComponent {
231
+ //#region Variables
232
+ config = inject(APP_CONFIG);
233
+ //#endregion
234
+ //#region Properties
235
+ get appName() {
236
+ return this.config.appName;
237
+ }
238
+ get companyName() {
239
+ return this.config.companyName;
240
+ }
241
+ get logoUrl() {
242
+ return this.config.logoUrl;
243
+ }
244
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: BrandComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
245
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: BrandComponent, isStandalone: true, selector: "shared-brand", ngImport: i0, template: "<div class=\"brand\">\n @if (logoUrl) {\n <img class=\"brand-logo\" [src]=\"logoUrl\" [alt]=\"appName\" />\n }\n\n <div class=\"brand-text\">\n <span class=\"brand-app\">{{ appName }}</span>\n @if (companyName) {\n <span class=\"brand-company\">{{ companyName }}</span>\n }\n </div>\n</div>\n", styles: ["@reference \"tailwindcss\";.brand{display:flex;min-width:0px;align-items:center;gap:.5rem}.brand .brand-app{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;line-height:1.25rem;font-weight:600}.brand .brand-company{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(120 113 108 / var(--tw-text-opacity, 1))}.brand .brand-logo{height:2rem;width:2rem;flex-shrink:0;object-fit:contain}.brand .brand-text{display:flex;min-width:0px;flex-direction:column;line-height:1.25}@media (max-width: 767px){.brand .brand-text{display:none}}\n"] });
246
+ }
247
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: BrandComponent, decorators: [{
248
+ type: Component,
249
+ args: [{ selector: 'shared-brand', template: "<div class=\"brand\">\n @if (logoUrl) {\n <img class=\"brand-logo\" [src]=\"logoUrl\" [alt]=\"appName\" />\n }\n\n <div class=\"brand-text\">\n <span class=\"brand-app\">{{ appName }}</span>\n @if (companyName) {\n <span class=\"brand-company\">{{ companyName }}</span>\n }\n </div>\n</div>\n", styles: ["@reference \"tailwindcss\";.brand{display:flex;min-width:0px;align-items:center;gap:.5rem}.brand .brand-app{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;line-height:1.25rem;font-weight:600}.brand .brand-company{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(120 113 108 / var(--tw-text-opacity, 1))}.brand .brand-logo{height:2rem;width:2rem;flex-shrink:0;object-fit:contain}.brand .brand-text{display:flex;min-width:0px;flex-direction:column;line-height:1.25}@media (max-width: 767px){.brand .brand-text{display:none}}\n"] }]
250
+ }] });
251
+
252
+ class EnvironmentBadgeComponent {
253
+ //#region Variables
254
+ config = inject(APP_CONFIG);
255
+ //#endregion
256
+ //#region Properties
257
+ /** CSS modifier class for the current environment, falling back to a neutral style. */
258
+ get badgeClass() {
259
+ const colorByEnvironment = {
260
+ DEV: 'env-dev',
261
+ QA: 'env-qa',
262
+ STG: 'env-stg',
263
+ };
264
+ return colorByEnvironment[this.environment] ?? 'env-neutral';
265
+ }
266
+ /** Normalized environment key (uppercase, trimmed). */
267
+ get environment() {
268
+ return (this.config.environment ?? '').trim().toUpperCase();
269
+ }
270
+ /** The badge is hidden in production and when no environment is configured. */
271
+ get isVisible() {
272
+ return this.environment.length > 0 && this.environment !== 'PROD';
273
+ }
274
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: EnvironmentBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
275
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: EnvironmentBadgeComponent, isStandalone: true, selector: "shared-environment-badge", ngImport: i0, template: "@if (isVisible) {\n <span class=\"env-badge {{ badgeClass }}\">\n <span class=\"env-dot\"></span>\n <span class=\"env-label\">{{ environment }}</span>\n </span>\n}\n", styles: ["@reference \"tailwindcss\";.env-badge{display:inline-flex;align-items:center;gap:.375rem;border-radius:9999px;padding:.325rem .625rem;font-size:.875rem;line-height:1.25rem;font-weight:600;text-transform:uppercase;letter-spacing:.025em;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-inset: inset }.env-badge .env-dot{height:.375rem;width:.375rem;flex-shrink:0;border-radius:9999px}.env-badge.env-dev{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1));--tw-ring-color: rgb(37 99 235 / .2) }.env-badge.env-dev .env-dot{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.env-badge.env-neutral{--tw-bg-opacity: 1;background-color:rgb(245 245 244 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(87 83 78 / var(--tw-text-opacity, 1));--tw-ring-color: rgb(120 113 108 / .2) }.env-badge.env-neutral .env-dot{--tw-bg-opacity: 1;background-color:rgb(120 113 108 / var(--tw-bg-opacity, 1))}.env-badge.env-qa{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1));--tw-ring-color: rgb(217 119 6 / .2) }.env-badge.env-qa .env-dot{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.env-badge.env-stg{--tw-bg-opacity: 1;background-color:rgb(245 243 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(109 40 217 / var(--tw-text-opacity, 1));--tw-ring-color: rgb(124 58 237 / .2) }.env-badge.env-stg .env-dot{--tw-bg-opacity: 1;background-color:rgb(139 92 246 / var(--tw-bg-opacity, 1))}\n"] });
276
+ }
277
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: EnvironmentBadgeComponent, decorators: [{
278
+ type: Component,
279
+ args: [{ selector: 'shared-environment-badge', template: "@if (isVisible) {\n <span class=\"env-badge {{ badgeClass }}\">\n <span class=\"env-dot\"></span>\n <span class=\"env-label\">{{ environment }}</span>\n </span>\n}\n", styles: ["@reference \"tailwindcss\";.env-badge{display:inline-flex;align-items:center;gap:.375rem;border-radius:9999px;padding:.325rem .625rem;font-size:.875rem;line-height:1.25rem;font-weight:600;text-transform:uppercase;letter-spacing:.025em;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-inset: inset }.env-badge .env-dot{height:.375rem;width:.375rem;flex-shrink:0;border-radius:9999px}.env-badge.env-dev{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1));--tw-ring-color: rgb(37 99 235 / .2) }.env-badge.env-dev .env-dot{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.env-badge.env-neutral{--tw-bg-opacity: 1;background-color:rgb(245 245 244 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(87 83 78 / var(--tw-text-opacity, 1));--tw-ring-color: rgb(120 113 108 / .2) }.env-badge.env-neutral .env-dot{--tw-bg-opacity: 1;background-color:rgb(120 113 108 / var(--tw-bg-opacity, 1))}.env-badge.env-qa{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1));--tw-ring-color: rgb(217 119 6 / .2) }.env-badge.env-qa .env-dot{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.env-badge.env-stg{--tw-bg-opacity: 1;background-color:rgb(245 243 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(109 40 217 / var(--tw-text-opacity, 1));--tw-ring-color: rgb(124 58 237 / .2) }.env-badge.env-stg .env-dot{--tw-bg-opacity: 1;background-color:rgb(139 92 246 / var(--tw-bg-opacity, 1))}\n"] }]
280
+ }] });
281
+
282
+ class NotificationsComponent {
152
283
  //#region ViewChilds, Inputs, Outputs
153
- logoutModal;
284
+ dropdown;
154
285
  //#endregion
155
286
  //#region Variables
156
- authenticationService = inject(AuthenticationService);
157
- destroy$ = new Subject();
287
+ notificationsService = inject(NotificationsService);
158
288
  router = inject(Router);
159
- sidebarService = inject(SidebarService);
160
- tabService = inject(TabService);
289
+ notifications$ = this.notificationsService.getNotifications();
290
+ showDropdown = false;
291
+ unreadCount$ = this.notificationsService.getUnreadCount();
161
292
  //#endregion
162
293
  //#region Properties
294
+ /** Whether the notifications feature is enabled; when false the bell is not rendered. */
295
+ get isEnabled() {
296
+ return this.notificationsService.isEnabled;
297
+ }
163
298
  //#endregion
164
299
  //#region Constructor and Angular life cycle methods
165
- ngOnDestroy() {
166
- this.destroy$.next(true);
167
- this.destroy$.complete();
168
- }
169
300
  ngOnInit() {
170
- const url = this.router.url;
171
- if (url !== '/' && !this.tabService.isUrlOpen(url)) {
172
- this.sidebarService.getMenuFromUrl(url)
173
- .pipe(take(1))
174
- .subscribe((item) => {
175
- if (!!item) {
176
- this.tabService.updateTabTitle(url, item.label);
177
- }
178
- });
179
- }
180
- this.sidebarService.menuUrlSelected
181
- .pipe(takeUntil(this.destroy$))
182
- .subscribe((item) => {
183
- const tab = new Tab({
184
- title: item.label,
185
- url: item.url,
186
- });
187
- this.tabService.openTab(tab);
188
- });
301
+ this.notificationsService.start();
189
302
  }
190
303
  //#endregion
191
304
  //#region Event handlers
192
- //#endregion
193
- //#region Public methods
194
- onLogoutClick() {
195
- this.logoutModal.toggleModal();
305
+ onDropdownClick() {
306
+ this.showDropdown = !this.showDropdown;
196
307
  }
197
- onLogoutConfirm() {
198
- this.authenticationService.signOut();
199
- this.router.navigate(['/login']);
308
+ onMarkAllAsRead() {
309
+ this.notificationsService.markAllAsRead();
200
310
  }
201
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: MainLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
202
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.6", type: MainLayoutComponent, isStandalone: true, selector: "shared-main-layout", viewQueries: [{ propertyName: "logoutModal", first: true, predicate: ["logoutModal"], descendants: true }], ngImport: i0, template: "<div class=\"main-container\">\n <lib-sidebar #sidebar></lib-sidebar>\n\n <div class=\"toolbar\">\n <div>\n <button class=\"btn\" (click)=\"sidebar.collapse()\">\n <i class=\"fa-solid fa-bars\"></i>\n </button>\n </div>\n <div>\n <button class=\"btn gray-300\" (click)=\"onLogoutClick()\">\n <i class=\"fa-solid fa-arrow-right-from-bracket\"></i>\n <span>{{ 'Main-Logout' | translate }}</span>\n </button>\n </div>\n </div>\n\n <div class=\"content\">\n <framework-tabs>\n <router-outlet></router-outlet>\n </framework-tabs>\n </div>\n</div>\n\n<lib-modal #logoutModal position=\"top\" size=\"xl\" [closeButtonText]=\"'Main-Logout-Modal-Message-Cancel' | translate\">\n <div body>\n <h5>{{ 'Main-Logout-Modal-Message' | translate }}</h5>\n </div>\n <div footer>\n <button type=\"button\" class=\"btn red-600\" (click)=\"onLogoutConfirm()\">{{ 'Main-Logout-Modal-Message-Confirm' | translate }}</button>\n </div>\n</lib-modal>", styles: ["@reference \"tailwindcss\";.main-container{display:grid;height:100vh;max-height:100vh;width:100vw;max-width:100%;--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1));grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);grid-template-areas:\"sidebar toolbar\" \"sidebar content\"}.main-container lib-sidebar{--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);grid-area:sidebar}.main-container .toolbar{display:flex;justify-content:space-between;border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(214 211 209 / var(--tw-border-opacity, 1));background-color:#ffffffe6;padding:.5rem;--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);grid-area:toolbar}.main-container .content{overflow-y:auto;padding:.75rem .75rem .5rem;grid-area:content}.main-container framework-tabs{height:100%}lib-modal h5{margin-bottom:.5rem;font-size:1.125rem;font-weight:500;line-height:1.25rem}\n"], dependencies: [{ kind: "component", type: ModalComponent, selector: "lib-modal", inputs: ["closeButtonText", "dialog", "modalProcessing", "position", "size", "title"], outputs: ["closed"] }, { kind: "component", type: SidebarComponent, selector: "lib-sidebar" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: TabsComponent, selector: "framework-tabs" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
311
+ onNotificationClick(notification) {
312
+ this.notificationsService.markAsRead(notification);
313
+ this.showDropdown = false;
314
+ const url = notification.callToActionUrl;
315
+ if (!url) {
316
+ return;
317
+ }
318
+ if (/^https?:\/\//i.test(url)) {
319
+ window.open(url, '_blank', 'noopener');
320
+ }
321
+ else {
322
+ this.router.navigateByUrl(url);
323
+ }
324
+ }
325
+ //#endregion
326
+ //#region Private methods
327
+ documentClick(event) {
328
+ if (this.showDropdown) {
329
+ let target = event.target;
330
+ while (target !== null && target.tagName?.toUpperCase() !== 'BODY') {
331
+ if (target === this.dropdown.nativeElement) {
332
+ return;
333
+ }
334
+ target = target.parentElement;
335
+ }
336
+ this.showDropdown = false;
337
+ }
338
+ }
339
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: NotificationsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
340
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: NotificationsComponent, isStandalone: true, selector: "shared-notifications", host: { listeners: { "window:click": "documentClick($event)" } }, viewQueries: [{ propertyName: "dropdown", first: true, predicate: ["dropdown"], descendants: true }], ngImport: i0, template: "@if (isEnabled) {\n <div #dropdown class=\"notifications\">\n <button class=\"btn gray-300 notifications-toggle\" type=\"button\" (click)=\"onDropdownClick()\"\n [attr.aria-label]=\"'TopBar-Notifications-Title' | translate\">\n <i class=\"fa-solid fa-bell\"></i>\n @if (unreadCount$ | async; as count) {\n <span class=\"notifications-badge\">{{ count }}</span>\n }\n </button>\n\n @if (showDropdown) {\n <div class=\"notifications-panel\">\n @let items = (notifications$ | async) ?? [];\n\n <div class=\"notifications-header\">\n <span>{{ 'TopBar-Notifications-Title' | translate }}</span>\n @if (items.length > 0) {\n <button type=\"button\" class=\"notifications-mark-all\" (click)=\"onMarkAllAsRead()\">\n {{ 'TopBar-Notifications-MarkAllRead' | translate }}\n </button>\n }\n </div>\n\n @if (items.length > 0) {\n <ul class=\"notifications-list\">\n @for (item of items; track $index) {\n <li class=\"notifications-item\" [class.unread]=\"!item.isRead\">\n <button type=\"button\" class=\"notifications-item-btn\"\n [class.has-cta]=\"!!item.callToActionUrl\" (click)=\"onNotificationClick(item)\">\n <span class=\"notifications-item-icon\"><i class=\"{{ item.icon }}\"></i></span>\n <span class=\"notifications-item-body\">\n <span class=\"notifications-item-title\">{{ item.title }}</span>\n <span class=\"notifications-item-description\">{{ item.description }}</span>\n </span>\n @if (item.callToActionUrl) {\n <i class=\"notifications-item-cta fa-solid fa-chevron-right\"></i>\n }\n </button>\n </li>\n }\n </ul>\n } @else {\n <div class=\"notifications-empty\">{{ 'TopBar-Notifications-Empty' | translate }}</div>\n }\n </div>\n }\n </div>\n}\n", styles: ["@reference \"tailwindcss\";.notifications{position:relative}.notifications .notifications-badge{position:absolute;top:-.25rem;right:-.25rem;height:1rem;min-width:1rem;border-radius:9999px;padding-left:.25rem;padding-right:.25rem;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.notifications .notifications-empty{padding:1.5rem .75rem;text-align:center;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(120 113 108 / var(--tw-text-opacity, 1))}.notifications .notifications-header{display:flex;align-items:center;justify-content:space-between;border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(231 229 228 / var(--tw-border-opacity, 1));padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem;font-weight:600}.notifications .notifications-list{max-height:20rem;overflow-y:auto}.notifications .notifications-list .notifications-item{border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(245 245 244 / var(--tw-border-opacity, 1))}.notifications .notifications-list .notifications-item:last-child{border-bottom-width:0px}.notifications .notifications-list .notifications-item.unread{background-color:#e6f4ff99}.notifications .notifications-list .notifications-item .notifications-item-body{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column}.notifications .notifications-list .notifications-item .notifications-item-btn{display:flex;width:100%;cursor:pointer;align-items:flex-start;gap:.625rem;padding:.5rem .75rem;text-align:left}.notifications .notifications-list .notifications-item .notifications-item-btn:hover{--tw-bg-opacity: 1;background-color:rgb(250 250 249 / var(--tw-bg-opacity, 1))}.notifications .notifications-list .notifications-item .notifications-item-cta{margin-top:.25rem;flex-shrink:0;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(168 162 158 / var(--tw-text-opacity, 1))}.notifications .notifications-list .notifications-item .notifications-item-description{font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(120 113 108 / var(--tw-text-opacity, 1))}.notifications .notifications-list .notifications-item .notifications-item-icon{margin-top:.125rem;display:flex;height:1.5rem;width:1.5rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(245 245 244 / var(--tw-bg-opacity, 1));font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(87 83 78 / var(--tw-text-opacity, 1))}.notifications .notifications-list .notifications-item .notifications-item-title{font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity: 1;color:rgb(41 37 36 / var(--tw-text-opacity, 1))}.notifications .notifications-mark-all{font-size:.75rem;line-height:1rem;font-weight:500;--tw-text-opacity: 1;color:rgb(0 107 182 / var(--tw-text-opacity, 1))}.notifications .notifications-mark-all:hover{--tw-text-opacity: 1;color:rgb(0 87 148 / var(--tw-text-opacity, 1))}.notifications .notifications-panel{position:absolute;right:0;top:100%;z-index:50;margin-top:.25rem;width:20rem;max-width:90vw;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(231 229 228 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.notifications .notifications-toggle{position:relative}\n"], dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
203
341
  }
204
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: MainLayoutComponent, decorators: [{
342
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: NotificationsComponent, decorators: [{
205
343
  type: Component,
206
- args: [{ selector: 'shared-main-layout', imports: [
207
- ModalComponent,
208
- SidebarComponent,
209
- RouterModule,
210
- TabsComponent,
344
+ args: [{ selector: 'shared-notifications', imports: [
345
+ AsyncPipe,
211
346
  TranslatePipe,
212
- ], template: "<div class=\"main-container\">\n <lib-sidebar #sidebar></lib-sidebar>\n\n <div class=\"toolbar\">\n <div>\n <button class=\"btn\" (click)=\"sidebar.collapse()\">\n <i class=\"fa-solid fa-bars\"></i>\n </button>\n </div>\n <div>\n <button class=\"btn gray-300\" (click)=\"onLogoutClick()\">\n <i class=\"fa-solid fa-arrow-right-from-bracket\"></i>\n <span>{{ 'Main-Logout' | translate }}</span>\n </button>\n </div>\n </div>\n\n <div class=\"content\">\n <framework-tabs>\n <router-outlet></router-outlet>\n </framework-tabs>\n </div>\n</div>\n\n<lib-modal #logoutModal position=\"top\" size=\"xl\" [closeButtonText]=\"'Main-Logout-Modal-Message-Cancel' | translate\">\n <div body>\n <h5>{{ 'Main-Logout-Modal-Message' | translate }}</h5>\n </div>\n <div footer>\n <button type=\"button\" class=\"btn red-600\" (click)=\"onLogoutConfirm()\">{{ 'Main-Logout-Modal-Message-Confirm' | translate }}</button>\n </div>\n</lib-modal>", styles: ["@reference \"tailwindcss\";.main-container{display:grid;height:100vh;max-height:100vh;width:100vw;max-width:100%;--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1));grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);grid-template-areas:\"sidebar toolbar\" \"sidebar content\"}.main-container lib-sidebar{--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);grid-area:sidebar}.main-container .toolbar{display:flex;justify-content:space-between;border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(214 211 209 / var(--tw-border-opacity, 1));background-color:#ffffffe6;padding:.5rem;--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);grid-area:toolbar}.main-container .content{overflow-y:auto;padding:.75rem .75rem .5rem;grid-area:content}.main-container framework-tabs{height:100%}lib-modal h5{margin-bottom:.5rem;font-size:1.125rem;font-weight:500;line-height:1.25rem}\n"] }]
213
- }], propDecorators: { logoutModal: [{
347
+ ], template: "@if (isEnabled) {\n <div #dropdown class=\"notifications\">\n <button class=\"btn gray-300 notifications-toggle\" type=\"button\" (click)=\"onDropdownClick()\"\n [attr.aria-label]=\"'TopBar-Notifications-Title' | translate\">\n <i class=\"fa-solid fa-bell\"></i>\n @if (unreadCount$ | async; as count) {\n <span class=\"notifications-badge\">{{ count }}</span>\n }\n </button>\n\n @if (showDropdown) {\n <div class=\"notifications-panel\">\n @let items = (notifications$ | async) ?? [];\n\n <div class=\"notifications-header\">\n <span>{{ 'TopBar-Notifications-Title' | translate }}</span>\n @if (items.length > 0) {\n <button type=\"button\" class=\"notifications-mark-all\" (click)=\"onMarkAllAsRead()\">\n {{ 'TopBar-Notifications-MarkAllRead' | translate }}\n </button>\n }\n </div>\n\n @if (items.length > 0) {\n <ul class=\"notifications-list\">\n @for (item of items; track $index) {\n <li class=\"notifications-item\" [class.unread]=\"!item.isRead\">\n <button type=\"button\" class=\"notifications-item-btn\"\n [class.has-cta]=\"!!item.callToActionUrl\" (click)=\"onNotificationClick(item)\">\n <span class=\"notifications-item-icon\"><i class=\"{{ item.icon }}\"></i></span>\n <span class=\"notifications-item-body\">\n <span class=\"notifications-item-title\">{{ item.title }}</span>\n <span class=\"notifications-item-description\">{{ item.description }}</span>\n </span>\n @if (item.callToActionUrl) {\n <i class=\"notifications-item-cta fa-solid fa-chevron-right\"></i>\n }\n </button>\n </li>\n }\n </ul>\n } @else {\n <div class=\"notifications-empty\">{{ 'TopBar-Notifications-Empty' | translate }}</div>\n }\n </div>\n }\n </div>\n}\n", styles: ["@reference \"tailwindcss\";.notifications{position:relative}.notifications .notifications-badge{position:absolute;top:-.25rem;right:-.25rem;height:1rem;min-width:1rem;border-radius:9999px;padding-left:.25rem;padding-right:.25rem;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.notifications .notifications-empty{padding:1.5rem .75rem;text-align:center;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(120 113 108 / var(--tw-text-opacity, 1))}.notifications .notifications-header{display:flex;align-items:center;justify-content:space-between;border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(231 229 228 / var(--tw-border-opacity, 1));padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem;font-weight:600}.notifications .notifications-list{max-height:20rem;overflow-y:auto}.notifications .notifications-list .notifications-item{border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(245 245 244 / var(--tw-border-opacity, 1))}.notifications .notifications-list .notifications-item:last-child{border-bottom-width:0px}.notifications .notifications-list .notifications-item.unread{background-color:#e6f4ff99}.notifications .notifications-list .notifications-item .notifications-item-body{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column}.notifications .notifications-list .notifications-item .notifications-item-btn{display:flex;width:100%;cursor:pointer;align-items:flex-start;gap:.625rem;padding:.5rem .75rem;text-align:left}.notifications .notifications-list .notifications-item .notifications-item-btn:hover{--tw-bg-opacity: 1;background-color:rgb(250 250 249 / var(--tw-bg-opacity, 1))}.notifications .notifications-list .notifications-item .notifications-item-cta{margin-top:.25rem;flex-shrink:0;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(168 162 158 / var(--tw-text-opacity, 1))}.notifications .notifications-list .notifications-item .notifications-item-description{font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(120 113 108 / var(--tw-text-opacity, 1))}.notifications .notifications-list .notifications-item .notifications-item-icon{margin-top:.125rem;display:flex;height:1.5rem;width:1.5rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(245 245 244 / var(--tw-bg-opacity, 1));font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(87 83 78 / var(--tw-text-opacity, 1))}.notifications .notifications-list .notifications-item .notifications-item-title{font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity: 1;color:rgb(41 37 36 / var(--tw-text-opacity, 1))}.notifications .notifications-mark-all{font-size:.75rem;line-height:1rem;font-weight:500;--tw-text-opacity: 1;color:rgb(0 107 182 / var(--tw-text-opacity, 1))}.notifications .notifications-mark-all:hover{--tw-text-opacity: 1;color:rgb(0 87 148 / var(--tw-text-opacity, 1))}.notifications .notifications-panel{position:absolute;right:0;top:100%;z-index:50;margin-top:.25rem;width:20rem;max-width:90vw;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(231 229 228 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.notifications .notifications-toggle{position:relative}\n"] }]
348
+ }], propDecorators: { dropdown: [{
214
349
  type: ViewChild,
215
- args: ['logoutModal']
350
+ args: ['dropdown']
351
+ }], documentClick: [{
352
+ type: HostListener,
353
+ args: ['window:click', ['$event']]
216
354
  }] } });
217
355
 
218
356
  class LanguageSelectorComponent {
219
357
  //#region ViewChilds, Inputs, Outputs
220
358
  dropdown;
359
+ /** When true, the toggle shows the current language's flag (light-gray button) instead of text. */
360
+ showFlag = false;
221
361
  title = 'LanguageSelector-Title';
222
362
  //#endregion
223
363
  //#region Variables
@@ -258,18 +398,18 @@ class LanguageSelectorComponent {
258
398
  }
259
399
  }
260
400
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LanguageSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
261
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.6", type: LanguageSelectorComponent, isStandalone: true, selector: "shared-language-selector", inputs: { title: "title" }, host: { listeners: { "window:click": "documentClick($event)" } }, viewQueries: [{ propertyName: "dropdown", first: true, predicate: ["dropdown"], descendants: true }], ngImport: i0, template: "<div #dropdown class=\"dropdown right\" [class.show]=\"showDropdown\">\n <button class=\"btn\" type=\"button\" (click)=\"onDropdownClick()\">\n {{ title | translate }}\n </button>\n\n <ul>\n <li *ngFor=\"let lang of languages\">\n <a class=\"dropdown-item flag-{{ lang }}\"\n [ngClass]=\"{'selected': lang === selectedLanguage}\"\n (click)=\"onSelectLanguage(lang)\"\n >\n {{ 'Language-' + lang | translate }}\n </a>\n </li>\n </ul>\n</div>", styles: [".dropdown .btn{width:100%;font-weight:500}.dropdown ul{width:100%;max-width:188px}.dropdown ul li{margin-top:.25rem;margin-bottom:.25rem}.dropdown ul li:first-child{margin-top:0}.dropdown ul li:last-child{margin-bottom:0}.dropdown ul li{--tw-shadow: 0 0 3px 0 rgba(0,0,0,.3),0 0 10px 0 rgba(0,0,0,.3);--tw-shadow-colored: 0 0 3px 0 var(--tw-shadow-color), 0 0 10px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dropdown ul .dropdown-item{position:relative;font-weight:700;--tw-contrast: contrast(1.25);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.dropdown ul .dropdown-item:before{position:absolute;inset:0;z-index:-10;--tw-content: \"\";content:var(--tw-content);background-image:linear-gradient(to right,var(--tw-gradient-stops));--tw-gradient-from: rgb(255 255 255 / .8) var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: rgb(255 255 255 / .3) var(--tw-gradient-to-position) }.dropdown ul .dropdown-item:hover:before,.dropdown ul .dropdown-item:focus:before{background-image:linear-gradient(to right,var(--tw-gradient-stops));--tw-gradient-from: rgb(229 229 229 / .8) var(--tw-gradient-from-position);--tw-gradient-to: rgb(229 229 229 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: rgb(229 229 229 / .3) var(--tw-gradient-to-position) }.dropdown ul .dropdown-item:active:before{background-image:linear-gradient(to right,var(--tw-gradient-stops));--tw-gradient-from: rgb(0 107 182 / .8) var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 107 182 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: rgb(0 107 182 / .3) var(--tw-gradient-to-position) }.dropdown ul .dropdown-item:not(.selected):before{--tw-backdrop-grayscale: grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.dropdown ul .dropdown-item.flag-en{background-image:url(/flags/en.png)}.dropdown ul .dropdown-item.flag-pt{background-image:url(/flags/pt.png)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
401
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: LanguageSelectorComponent, isStandalone: true, selector: "shared-language-selector", inputs: { showFlag: "showFlag", title: "title" }, host: { listeners: { "window:click": "documentClick($event)" } }, viewQueries: [{ propertyName: "dropdown", first: true, predicate: ["dropdown"], descendants: true }], ngImport: i0, template: "<div #dropdown class=\"dropdown right\" [class.show]=\"showDropdown\">\n <button class=\"btn\" type=\"button\" (click)=\"onDropdownClick()\"\n [class.gray-300]=\"showFlag\" [class.lang-flag-toggle]=\"showFlag\"\n [attr.aria-label]=\"showFlag ? (title | translate) : null\">\n @if (showFlag) {\n <span class=\"flag flag-{{ selectedLanguage }}\"></span>\n } @else {\n {{ title | translate }}\n }\n </button>\n\n <ul>\n @for (lang of languages; track lang) {\n <li>\n <button type=\"button\" class=\"dropdown-item\" [class.selected]=\"lang === selectedLanguage\"\n (click)=\"onSelectLanguage(lang)\">\n <span class=\"flag flag-{{ lang }}\"></span>\n <span class=\"dropdown-label\">{{ 'Language-' + lang | translate }}</span>\n </button>\n </li>\n }\n </ul>\n</div>\n", styles: [".dropdown .btn{width:100%;font-weight:500}.dropdown .flag{display:inline-block;height:1rem;width:1.5rem;flex-shrink:0;border-radius:.125rem;background-size:cover;background-position:center;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-color: rgb(0 0 0 / .1)}.dropdown .flag.flag-en{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 76 40\"%3E%3Crect width=\"76\" height=\"40\" fill=\"%23b22234\"/%3E%3Cg fill=\"%23fff\"%3E%3Crect y=\"3\" width=\"76\" height=\"3\"/%3E%3Crect y=\"9\" width=\"76\" height=\"3\"/%3E%3Crect y=\"15\" width=\"76\" height=\"3\"/%3E%3Crect y=\"22\" width=\"76\" height=\"3\"/%3E%3Crect y=\"28\" width=\"76\" height=\"3\"/%3E%3Crect y=\"34\" width=\"76\" height=\"3\"/%3E%3C/g%3E%3Crect width=\"30\" height=\"22\" fill=\"%233c3b6e\"/%3E%3C/svg%3E')}.dropdown .flag.flag-pt{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 70 49\"%3E%3Crect width=\"70\" height=\"49\" fill=\"%23009c3b\"/%3E%3Cpath d=\"M35 6L64 24.5L35 43L6 24.5Z\" fill=\"%23ffdf00\"/%3E%3Ccircle cx=\"35\" cy=\"24.5\" r=\"9.8\" fill=\"%23002776\"/%3E%3C/svg%3E')}.dropdown .lang-flag-toggle{display:flex;height:2.25rem;width:auto;align-items:center;justify-content:center;border-width:0px;padding-left:.625rem;padding-right:.625rem}.dropdown .lang-flag-toggle:after{border-top-color:currentColor;margin-left:.325rem}.dropdown ul{z-index:50;margin-top:.25rem;min-width:11rem;padding:.25rem;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(231 229 228 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dropdown ul .dropdown-item{display:flex;width:100%;cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:.5rem;border-radius:.25rem;padding:.5rem .75rem;text-align:left;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(68 64 60 / var(--tw-text-opacity, 1))}.dropdown ul .dropdown-item:hover{--tw-bg-opacity: 1;background-color:rgb(245 245 244 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(68 64 60 / var(--tw-text-opacity, 1))}.dropdown ul .dropdown-item:focus{--tw-bg-opacity: 1;background-color:rgb(245 245 244 / var(--tw-bg-opacity, 1));outline:2px solid transparent;outline-offset:2px}.dropdown ul .dropdown-item:active{--tw-bg-opacity: 1;background-color:rgb(231 229 228 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(68 64 60 / var(--tw-text-opacity, 1))}.dropdown ul .dropdown-item.selected{--tw-bg-opacity: 1;background-color:rgb(230 244 255 / var(--tw-bg-opacity, 1));font-weight:600;--tw-text-opacity: 1;color:rgb(0 47 80 / var(--tw-text-opacity, 1))}.dropdown ul .dropdown-item.selected:hover{--tw-bg-opacity: 1;background-color:rgb(154 202 237 / var(--tw-bg-opacity, 1))}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }] });
262
402
  }
263
403
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LanguageSelectorComponent, decorators: [{
264
404
  type: Component,
265
405
  args: [{ selector: 'shared-language-selector', imports: [
266
- NgClass,
267
- NgFor,
268
406
  TranslatePipe,
269
- ], template: "<div #dropdown class=\"dropdown right\" [class.show]=\"showDropdown\">\n <button class=\"btn\" type=\"button\" (click)=\"onDropdownClick()\">\n {{ title | translate }}\n </button>\n\n <ul>\n <li *ngFor=\"let lang of languages\">\n <a class=\"dropdown-item flag-{{ lang }}\"\n [ngClass]=\"{'selected': lang === selectedLanguage}\"\n (click)=\"onSelectLanguage(lang)\"\n >\n {{ 'Language-' + lang | translate }}\n </a>\n </li>\n </ul>\n</div>", styles: [".dropdown .btn{width:100%;font-weight:500}.dropdown ul{width:100%;max-width:188px}.dropdown ul li{margin-top:.25rem;margin-bottom:.25rem}.dropdown ul li:first-child{margin-top:0}.dropdown ul li:last-child{margin-bottom:0}.dropdown ul li{--tw-shadow: 0 0 3px 0 rgba(0,0,0,.3),0 0 10px 0 rgba(0,0,0,.3);--tw-shadow-colored: 0 0 3px 0 var(--tw-shadow-color), 0 0 10px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dropdown ul .dropdown-item{position:relative;font-weight:700;--tw-contrast: contrast(1.25);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.dropdown ul .dropdown-item:before{position:absolute;inset:0;z-index:-10;--tw-content: \"\";content:var(--tw-content);background-image:linear-gradient(to right,var(--tw-gradient-stops));--tw-gradient-from: rgb(255 255 255 / .8) var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: rgb(255 255 255 / .3) var(--tw-gradient-to-position) }.dropdown ul .dropdown-item:hover:before,.dropdown ul .dropdown-item:focus:before{background-image:linear-gradient(to right,var(--tw-gradient-stops));--tw-gradient-from: rgb(229 229 229 / .8) var(--tw-gradient-from-position);--tw-gradient-to: rgb(229 229 229 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: rgb(229 229 229 / .3) var(--tw-gradient-to-position) }.dropdown ul .dropdown-item:active:before{background-image:linear-gradient(to right,var(--tw-gradient-stops));--tw-gradient-from: rgb(0 107 182 / .8) var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 107 182 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: rgb(0 107 182 / .3) var(--tw-gradient-to-position) }.dropdown ul .dropdown-item:not(.selected):before{--tw-backdrop-grayscale: grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.dropdown ul .dropdown-item.flag-en{background-image:url(/flags/en.png)}.dropdown ul .dropdown-item.flag-pt{background-image:url(/flags/pt.png)}\n"] }]
407
+ ], template: "<div #dropdown class=\"dropdown right\" [class.show]=\"showDropdown\">\n <button class=\"btn\" type=\"button\" (click)=\"onDropdownClick()\"\n [class.gray-300]=\"showFlag\" [class.lang-flag-toggle]=\"showFlag\"\n [attr.aria-label]=\"showFlag ? (title | translate) : null\">\n @if (showFlag) {\n <span class=\"flag flag-{{ selectedLanguage }}\"></span>\n } @else {\n {{ title | translate }}\n }\n </button>\n\n <ul>\n @for (lang of languages; track lang) {\n <li>\n <button type=\"button\" class=\"dropdown-item\" [class.selected]=\"lang === selectedLanguage\"\n (click)=\"onSelectLanguage(lang)\">\n <span class=\"flag flag-{{ lang }}\"></span>\n <span class=\"dropdown-label\">{{ 'Language-' + lang | translate }}</span>\n </button>\n </li>\n }\n </ul>\n</div>\n", styles: [".dropdown .btn{width:100%;font-weight:500}.dropdown .flag{display:inline-block;height:1rem;width:1.5rem;flex-shrink:0;border-radius:.125rem;background-size:cover;background-position:center;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-color: rgb(0 0 0 / .1)}.dropdown .flag.flag-en{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 76 40\"%3E%3Crect width=\"76\" height=\"40\" fill=\"%23b22234\"/%3E%3Cg fill=\"%23fff\"%3E%3Crect y=\"3\" width=\"76\" height=\"3\"/%3E%3Crect y=\"9\" width=\"76\" height=\"3\"/%3E%3Crect y=\"15\" width=\"76\" height=\"3\"/%3E%3Crect y=\"22\" width=\"76\" height=\"3\"/%3E%3Crect y=\"28\" width=\"76\" height=\"3\"/%3E%3Crect y=\"34\" width=\"76\" height=\"3\"/%3E%3C/g%3E%3Crect width=\"30\" height=\"22\" fill=\"%233c3b6e\"/%3E%3C/svg%3E')}.dropdown .flag.flag-pt{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 70 49\"%3E%3Crect width=\"70\" height=\"49\" fill=\"%23009c3b\"/%3E%3Cpath d=\"M35 6L64 24.5L35 43L6 24.5Z\" fill=\"%23ffdf00\"/%3E%3Ccircle cx=\"35\" cy=\"24.5\" r=\"9.8\" fill=\"%23002776\"/%3E%3C/svg%3E')}.dropdown .lang-flag-toggle{display:flex;height:2.25rem;width:auto;align-items:center;justify-content:center;border-width:0px;padding-left:.625rem;padding-right:.625rem}.dropdown .lang-flag-toggle:after{border-top-color:currentColor;margin-left:.325rem}.dropdown ul{z-index:50;margin-top:.25rem;min-width:11rem;padding:.25rem;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(231 229 228 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dropdown ul .dropdown-item{display:flex;width:100%;cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:.5rem;border-radius:.25rem;padding:.5rem .75rem;text-align:left;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(68 64 60 / var(--tw-text-opacity, 1))}.dropdown ul .dropdown-item:hover{--tw-bg-opacity: 1;background-color:rgb(245 245 244 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(68 64 60 / var(--tw-text-opacity, 1))}.dropdown ul .dropdown-item:focus{--tw-bg-opacity: 1;background-color:rgb(245 245 244 / var(--tw-bg-opacity, 1));outline:2px solid transparent;outline-offset:2px}.dropdown ul .dropdown-item:active{--tw-bg-opacity: 1;background-color:rgb(231 229 228 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(68 64 60 / var(--tw-text-opacity, 1))}.dropdown ul .dropdown-item.selected{--tw-bg-opacity: 1;background-color:rgb(230 244 255 / var(--tw-bg-opacity, 1));font-weight:600;--tw-text-opacity: 1;color:rgb(0 47 80 / var(--tw-text-opacity, 1))}.dropdown ul .dropdown-item.selected:hover{--tw-bg-opacity: 1;background-color:rgb(154 202 237 / var(--tw-bg-opacity, 1))}\n"] }]
270
408
  }], propDecorators: { dropdown: [{
271
409
  type: ViewChild,
272
410
  args: ['dropdown']
411
+ }], showFlag: [{
412
+ type: Input
273
413
  }], title: [{
274
414
  type: Input
275
415
  }], documentClick: [{
@@ -277,6 +417,140 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
277
417
  args: ['window:click', ['$event']]
278
418
  }] } });
279
419
 
420
+ class UserProfileComponent {
421
+ //#region Variables
422
+ authenticationService = inject(AuthenticationService);
423
+ user = null;
424
+ //#endregion
425
+ //#region Properties
426
+ /** Up to two uppercase initials derived from the user's name, used when no picture is set. */
427
+ get initials() {
428
+ const parts = this.name.trim().split(/\s+/).filter((p) => p.length > 0);
429
+ if (parts.length === 0) {
430
+ return '';
431
+ }
432
+ const first = parts[0].charAt(0);
433
+ const last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : '';
434
+ return (first + last).toUpperCase();
435
+ }
436
+ get name() {
437
+ return this.user?.name ?? '';
438
+ }
439
+ get pictureUrl() {
440
+ return this.user?.pictureUrl;
441
+ }
442
+ get position() {
443
+ return this.user?.position ?? '';
444
+ }
445
+ //#endregion
446
+ //#region Constructor and Angular life cycle methods
447
+ ngOnInit() {
448
+ this.user = this.authenticationService.getUserInfo();
449
+ }
450
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
451
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: UserProfileComponent, isStandalone: true, selector: "shared-user-profile", ngImport: i0, template: "<div class=\"user-profile\">\n <div class=\"user-text\">\n <span class=\"user-name\">{{ name }}</span>\n @if (position) {\n <span class=\"user-position\">{{ position }}</span>\n }\n </div>\n\n <div class=\"user-avatar\">\n @if (pictureUrl) {\n <img class=\"user-avatar-img\" [src]=\"pictureUrl\" [alt]=\"name\" />\n } @else {\n <span class=\"user-avatar-initials\">{{ initials }}</span>\n }\n </div>\n</div>\n", styles: ["@reference \"tailwindcss\";@media (max-width: 767px){:host{display:none}}.user-profile{display:flex;height:2.25rem;min-width:0px;-webkit-user-select:none;user-select:none;align-items:center;gap:.5rem;border-radius:.25rem;padding-left:.5rem;padding-right:.5rem;background-color:#0000000d;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.user-profile:hover{background-color:#0000001a}.user-profile .user-avatar{display:flex;height:1.75rem;width:1.75rem;flex-shrink:0;align-items:center;justify-content:center;overflow:hidden;border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(0 87 148 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.user-profile .user-avatar-img{height:100%;width:100%;object-fit:cover}.user-profile .user-avatar-initials{font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase}.user-profile .user-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;font-weight:500;line-height:1rem;--tw-text-opacity: 1;color:rgb(41 37 36 / var(--tw-text-opacity, 1))}.user-profile .user-position{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(120 113 108 / var(--tw-text-opacity, 1))}.user-profile .user-text{display:flex;min-width:0px;flex-direction:column;text-align:right}\n"] });
452
+ }
453
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: UserProfileComponent, decorators: [{
454
+ type: Component,
455
+ args: [{ selector: 'shared-user-profile', template: "<div class=\"user-profile\">\n <div class=\"user-text\">\n <span class=\"user-name\">{{ name }}</span>\n @if (position) {\n <span class=\"user-position\">{{ position }}</span>\n }\n </div>\n\n <div class=\"user-avatar\">\n @if (pictureUrl) {\n <img class=\"user-avatar-img\" [src]=\"pictureUrl\" [alt]=\"name\" />\n } @else {\n <span class=\"user-avatar-initials\">{{ initials }}</span>\n }\n </div>\n</div>\n", styles: ["@reference \"tailwindcss\";@media (max-width: 767px){:host{display:none}}.user-profile{display:flex;height:2.25rem;min-width:0px;-webkit-user-select:none;user-select:none;align-items:center;gap:.5rem;border-radius:.25rem;padding-left:.5rem;padding-right:.5rem;background-color:#0000000d;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.user-profile:hover{background-color:#0000001a}.user-profile .user-avatar{display:flex;height:1.75rem;width:1.75rem;flex-shrink:0;align-items:center;justify-content:center;overflow:hidden;border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(0 87 148 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.user-profile .user-avatar-img{height:100%;width:100%;object-fit:cover}.user-profile .user-avatar-initials{font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase}.user-profile .user-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;font-weight:500;line-height:1rem;--tw-text-opacity: 1;color:rgb(41 37 36 / var(--tw-text-opacity, 1))}.user-profile .user-position{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(120 113 108 / var(--tw-text-opacity, 1))}.user-profile .user-text{display:flex;min-width:0px;flex-direction:column;text-align:right}\n"] }]
456
+ }] });
457
+
458
+ class TopBarComponent {
459
+ //#region ViewChilds, Inputs, Outputs
460
+ /** Emitted when the logout button is clicked. */
461
+ logout = new EventEmitter();
462
+ //#endregion
463
+ //#region Event handlers
464
+ onLogoutClick() {
465
+ this.logout.emit();
466
+ }
467
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: TopBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
468
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.6", type: TopBarComponent, isStandalone: true, selector: "shared-top-bar", outputs: { logout: "logout" }, ngImport: i0, template: "<div class=\"top-bar\">\n <div class=\"top-bar-section top-bar-start\">\n <shared-brand></shared-brand>\n <shared-environment-badge></shared-environment-badge>\n </div>\n\n <div class=\"top-bar-section top-bar-end\">\n <shared-notifications></shared-notifications>\n <shared-language-selector [showFlag]=\"true\"></shared-language-selector>\n\n <!-- Theme switch slot (deferred \u2014 see 2026-07-23-top-bar-update-design.md) -->\n\n <shared-user-profile></shared-user-profile>\n\n <button class=\"btn gray-300 top-bar-logout\" type=\"button\" (click)=\"onLogoutClick()\"\n [attr.aria-label]=\"'Main-Logout' | translate\">\n <i class=\"fa-solid fa-arrow-right-from-bracket\"></i>\n </button>\n </div>\n</div>\n", styles: ["@reference \"tailwindcss\";.top-bar{display:flex;width:100%;align-items:center;justify-content:space-between;gap:.75rem}.top-bar .top-bar-end{flex-shrink:0}.top-bar .top-bar-section{display:flex;min-width:0px;align-items:center;gap:.75rem}.top-bar .top-bar-start{min-width:0px}\n"], dependencies: [{ kind: "component", type: BrandComponent, selector: "shared-brand" }, { kind: "component", type: EnvironmentBadgeComponent, selector: "shared-environment-badge" }, { kind: "component", type: LanguageSelectorComponent, selector: "shared-language-selector", inputs: ["showFlag", "title"] }, { kind: "component", type: NotificationsComponent, selector: "shared-notifications" }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "component", type: UserProfileComponent, selector: "shared-user-profile" }] });
469
+ }
470
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: TopBarComponent, decorators: [{
471
+ type: Component,
472
+ args: [{ selector: 'shared-top-bar', imports: [
473
+ BrandComponent,
474
+ EnvironmentBadgeComponent,
475
+ LanguageSelectorComponent,
476
+ NotificationsComponent,
477
+ TranslatePipe,
478
+ UserProfileComponent,
479
+ ], template: "<div class=\"top-bar\">\n <div class=\"top-bar-section top-bar-start\">\n <shared-brand></shared-brand>\n <shared-environment-badge></shared-environment-badge>\n </div>\n\n <div class=\"top-bar-section top-bar-end\">\n <shared-notifications></shared-notifications>\n <shared-language-selector [showFlag]=\"true\"></shared-language-selector>\n\n <!-- Theme switch slot (deferred \u2014 see 2026-07-23-top-bar-update-design.md) -->\n\n <shared-user-profile></shared-user-profile>\n\n <button class=\"btn gray-300 top-bar-logout\" type=\"button\" (click)=\"onLogoutClick()\"\n [attr.aria-label]=\"'Main-Logout' | translate\">\n <i class=\"fa-solid fa-arrow-right-from-bracket\"></i>\n </button>\n </div>\n</div>\n", styles: ["@reference \"tailwindcss\";.top-bar{display:flex;width:100%;align-items:center;justify-content:space-between;gap:.75rem}.top-bar .top-bar-end{flex-shrink:0}.top-bar .top-bar-section{display:flex;min-width:0px;align-items:center;gap:.75rem}.top-bar .top-bar-start{min-width:0px}\n"] }]
480
+ }], propDecorators: { logout: [{
481
+ type: Output
482
+ }] } });
483
+
484
+ class MainLayoutComponent {
485
+ //#region ViewChilds, Inputs, Outputs
486
+ logoutModal;
487
+ //#endregion
488
+ //#region Variables
489
+ authenticationService = inject(AuthenticationService);
490
+ destroy$ = new Subject();
491
+ router = inject(Router);
492
+ sidebarService = inject(SidebarService);
493
+ tabService = inject(TabService);
494
+ /** Application version (from AppConfig) projected into the sidebar footer. */
495
+ appVersion = inject(APP_CONFIG).version;
496
+ //#endregion
497
+ //#region Properties
498
+ //#endregion
499
+ //#region Constructor and Angular life cycle methods
500
+ ngOnDestroy() {
501
+ this.destroy$.next(true);
502
+ this.destroy$.complete();
503
+ }
504
+ ngOnInit() {
505
+ const url = this.router.url;
506
+ if (url !== '/' && !this.tabService.isUrlOpen(url)) {
507
+ this.sidebarService.getMenuFromUrl(url)
508
+ .pipe(take(1))
509
+ .subscribe((item) => {
510
+ if (!!item) {
511
+ this.tabService.updateTabTitle(url, item.label);
512
+ }
513
+ });
514
+ }
515
+ this.sidebarService.menuUrlSelected
516
+ .pipe(takeUntil(this.destroy$))
517
+ .subscribe((item) => {
518
+ const tab = new Tab({
519
+ title: item.label,
520
+ url: item.url,
521
+ });
522
+ this.tabService.openTab(tab);
523
+ });
524
+ }
525
+ //#endregion
526
+ //#region Event handlers
527
+ //#endregion
528
+ //#region Public methods
529
+ onLogoutClick() {
530
+ this.logoutModal.toggleModal();
531
+ }
532
+ onLogoutConfirm() {
533
+ this.authenticationService.signOut();
534
+ this.router.navigate(['/login']);
535
+ }
536
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: MainLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
537
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: MainLayoutComponent, isStandalone: true, selector: "shared-main-layout", viewQueries: [{ propertyName: "logoutModal", first: true, predicate: ["logoutModal"], descendants: true }], ngImport: i0, template: "<div class=\"main-container\">\n <lib-sidebar>\n @if (appVersion) {\n <span>v{{ appVersion }}</span>\n }\n </lib-sidebar>\n\n <shared-top-bar class=\"toolbar\" (logout)=\"onLogoutClick()\"></shared-top-bar>\n\n <div class=\"content\">\n <framework-tabs>\n <router-outlet></router-outlet>\n </framework-tabs>\n </div>\n</div>\n\n<lib-modal #logoutModal position=\"top\" size=\"xl\" [closeButtonText]=\"'Main-Logout-Modal-Message-Cancel' | translate\">\n <div body>\n <h5>{{ 'Main-Logout-Modal-Message' | translate }}</h5>\n </div>\n <div footer>\n <button type=\"button\" class=\"btn red-600\" (click)=\"onLogoutConfirm()\">{{ 'Main-Logout-Modal-Message-Confirm' | translate }}</button>\n </div>\n</lib-modal>", styles: ["@reference \"tailwindcss\";.main-container{display:grid;height:100vh;max-height:100vh;width:100vw;max-width:100%;--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1));grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);grid-template-areas:\"toolbar toolbar\" \"sidebar content\"}.main-container lib-sidebar{--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);grid-area:sidebar}.main-container .toolbar{display:flex;justify-content:space-between;border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(214 211 209 / var(--tw-border-opacity, 1));background-color:#ffffffe6;padding:.5rem;--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);grid-area:toolbar}.main-container .content{overflow-y:auto;padding:.75rem .75rem .5rem;grid-area:content}.main-container framework-tabs{height:100%}lib-modal h5{margin-bottom:.5rem;font-size:1.125rem;font-weight:500;line-height:1.25rem}\n"], dependencies: [{ kind: "component", type: ModalComponent, selector: "lib-modal", inputs: ["closeButtonText", "dialog", "modalProcessing", "position", "size", "title"], outputs: ["closed"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: SidebarComponent, selector: "lib-sidebar" }, { kind: "component", type: TabsComponent, selector: "framework-tabs" }, { kind: "component", type: TopBarComponent, selector: "shared-top-bar", outputs: ["logout"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
538
+ }
539
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: MainLayoutComponent, decorators: [{
540
+ type: Component,
541
+ args: [{ selector: 'shared-main-layout', imports: [
542
+ ModalComponent,
543
+ RouterModule,
544
+ SidebarComponent,
545
+ TabsComponent,
546
+ TopBarComponent,
547
+ TranslatePipe,
548
+ ], template: "<div class=\"main-container\">\n <lib-sidebar>\n @if (appVersion) {\n <span>v{{ appVersion }}</span>\n }\n </lib-sidebar>\n\n <shared-top-bar class=\"toolbar\" (logout)=\"onLogoutClick()\"></shared-top-bar>\n\n <div class=\"content\">\n <framework-tabs>\n <router-outlet></router-outlet>\n </framework-tabs>\n </div>\n</div>\n\n<lib-modal #logoutModal position=\"top\" size=\"xl\" [closeButtonText]=\"'Main-Logout-Modal-Message-Cancel' | translate\">\n <div body>\n <h5>{{ 'Main-Logout-Modal-Message' | translate }}</h5>\n </div>\n <div footer>\n <button type=\"button\" class=\"btn red-600\" (click)=\"onLogoutConfirm()\">{{ 'Main-Logout-Modal-Message-Confirm' | translate }}</button>\n </div>\n</lib-modal>", styles: ["@reference \"tailwindcss\";.main-container{display:grid;height:100vh;max-height:100vh;width:100vw;max-width:100%;--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1));grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);grid-template-areas:\"toolbar toolbar\" \"sidebar content\"}.main-container lib-sidebar{--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);grid-area:sidebar}.main-container .toolbar{display:flex;justify-content:space-between;border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(214 211 209 / var(--tw-border-opacity, 1));background-color:#ffffffe6;padding:.5rem;--tw-shadow: 0 0 4px 0 rgba(0,0,0,.2), 0 3px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 0 4px 0 var(--tw-shadow-color), 0 3px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);grid-area:toolbar}.main-container .content{overflow-y:auto;padding:.75rem .75rem .5rem;grid-area:content}.main-container framework-tabs{height:100%}lib-modal h5{margin-bottom:.5rem;font-size:1.125rem;font-weight:500;line-height:1.25rem}\n"] }]
549
+ }], propDecorators: { logoutModal: [{
550
+ type: ViewChild,
551
+ args: ['logoutModal']
552
+ }] } });
553
+
280
554
  class LoginComponent {
281
555
  //#region ViewChilds, Inputs, Outputs
282
556
  username;
@@ -341,7 +615,7 @@ class LoginComponent {
341
615
  }
342
616
  }
343
617
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
344
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.6", type: LoginComponent, isStandalone: true, selector: "shared-login", viewQueries: [{ propertyName: "username", first: true, predicate: ["username"], descendants: true }], ngImport: i0, template: "<div class=\"content\">\n <div class=\"login\">\n <div class=\"logo-container\">\n <div class=\"logo\">\n <img src=\"/logo-login.png\" alt=\"Logo\" />\n </div>\n </div>\n \n <h3 class=\"title\">{{ 'Login-Title' | translate }}</h3>\n\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n\n <div class=\"validations\" [class.hidden]=\"!formState.error\">\n \n <div *ngIf=\"formState.error === 'invalid'\">\n {{'Login-Errors-Invalid' | translate}}\n </div>\n \n <div *ngIf=\"formState.error === 'internalServerError'\">{{'Login-Errors-InternalServerError' | translate}}</div>\n </div>\n\n <div class=\"input-group inline\">\n <input type=\"text\" #username id=\"username\" formControlName=\"username\" class=\"form-input peer\" [placeholder]=\"'Login-Username' | translate\" autofocus />\n\n <div class=\"input-group-text\">\n <label for=\"username\">\n <i class=\"fas fa-user fa-fw\"></i>\n </label>\n </div>\n\n <div class=\"validations\">\n <span *ngIf=\"form.get('username')?.hasError('required')\">{{ 'Login-Username-Required' | translate }}</span>\n </div>\n </div>\n \n <div class=\"input-group inline\">\n <input type=\"password\" id=\"password\" formControlName=\"password\" class=\"form-input\" [placeholder]=\"'Login-Password' | translate\" />\n\n <div class=\"input-group-text\">\n <label for=\"password\">\n <i class=\"fas fa-key fa-fw\"></i>\n </label>\n </div>\n \n <div class=\"validations\">\n <span *ngIf=\"form.get('password')?.hasError('required')\">{{ 'Login-Password-Required' | translate }}</span>\n </div>\n </div>\n \n <div class=\"options\">\n <div>\n <input class=\"form-input\" id=\"rememberMe\" formControlName=\"rememberMe\" type=\"checkbox\">\n <label for=\"rememberMe\">{{ 'Login-RememberMe' | translate }}</label>\n </div>\n \n <shared-language-selector></shared-language-selector>\n </div>\n \n <div class=\"buttons\">\n <button type=\"submit\" class=\"btn\"\n [class.loading]=\"formState.loading && !formState.success && !formState.error\"\n [class.error]=\"formState.error\"\n [class.success]=\"formState.success\">\n <i *ngIf=\"!formState.loading\" class=\"fas fa-sign-in-alt fa-fw\"></i>\n {{ 'Login-SignIn' | translate }}\n </button>\n </div>\n </form>\n </div>\n\n <div class=\"footer\">\n <span [innerHTML]=\"'Login-Copyright' | translate\"></span>\n <span class=\"design\">{{ 'Login-DesignBy' | translate }}</span>\n </div>\n</div>", styles: [":host{display:block}.content{margin-left:auto;margin-right:auto;width:83.333333%;border-radius:.5rem}@media (min-width: 768px){.content{width:66.666667%}}@media (min-width: 1280px){.content{width:41.666667%}}.content>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse));--tw-divide-opacity: 1;border-color:rgb(161 161 170 / var(--tw-divide-opacity, 1))}.content{--tw-shadow: 0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 4px 8px 0 var(--tw-shadow-color), 0 6px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.content .login{position:relative;z-index:10;margin-top:5rem;border-top-left-radius:.5rem;border-top-right-radius:.5rem;background-color:#fff9;padding:5rem 1.5rem 1.5rem;--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.content .login .logo-container{margin-top:-10rem;margin-left:auto;margin-right:auto;margin-bottom:1rem;width:fit-content;border-radius:1.5rem;padding:.5rem;--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-shadow: 0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 4px 8px 0 var(--tw-shadow-color), 0 6px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.content .login .logo-container .logo{width:fit-content;border-radius:1.5rem;padding:.5rem;border-width:2px;--tw-border-opacity: 1;border-color:rgb(252 211 77 / var(--tw-border-opacity, 1))}.content .login .logo-container .logo img{height:10rem;min-width:fit-content}.content .login .title{padding-bottom:1.25rem;text-align:center;font-size:1.875rem;line-height:2.25rem;font-weight:700}.content .login form>.validations{text-align:center;font-weight:700;--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.content .login form>.validations:after{margin-left:auto;margin-right:auto;margin-bottom:.5rem;display:block;border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1));--tw-content: \"\";content:var(--tw-content);animation:separator 1s ease-in-out forwards}.content .login form .options{margin-bottom:.75rem;display:flex;flex-direction:row;flex-wrap:wrap-reverse;justify-content:space-between}.content .login form .options shared-language-selector{margin-bottom:.75rem;width:100%}@media (min-width: 640px){.content .login form .options shared-language-selector{margin-bottom:0;width:auto}}.content .login form .buttons{display:flex;justify-content:center;gap:1rem}.content .footer{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem;padding:.125rem .5rem;background-color:#e2e8f099;--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);font-size:.875rem;line-height:1.25rem;font-weight:600;letter-spacing:-.025em;display:flex;flex-wrap:wrap;justify-content:space-between}@keyframes separator{0%{width:0}to{width:100%}}\n"], dependencies: [{ kind: "component", type: LanguageSelectorComponent, selector: "shared-language-selector", inputs: ["title"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: RouterModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
618
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.6", type: LoginComponent, isStandalone: true, selector: "shared-login", viewQueries: [{ propertyName: "username", first: true, predicate: ["username"], descendants: true }], ngImport: i0, template: "<div class=\"content\">\n <div class=\"login\">\n <div class=\"logo-container\">\n <div class=\"logo\">\n <img src=\"/logo-login.png\" alt=\"Logo\" />\n </div>\n </div>\n \n <h3 class=\"title\">{{ 'Login-Title' | translate }}</h3>\n\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n\n <div class=\"validations\" [class.hidden]=\"!formState.error\">\n \n <div *ngIf=\"formState.error === 'invalid'\">\n {{'Login-Errors-Invalid' | translate}}\n </div>\n \n <div *ngIf=\"formState.error === 'internalServerError'\">{{'Login-Errors-InternalServerError' | translate}}</div>\n </div>\n\n <div class=\"input-group inline\">\n <input type=\"text\" #username id=\"username\" formControlName=\"username\" class=\"form-input peer\" [placeholder]=\"'Login-Username' | translate\" autofocus />\n\n <div class=\"input-group-text\">\n <label for=\"username\">\n <i class=\"fas fa-user fa-fw\"></i>\n </label>\n </div>\n\n <div class=\"validations\">\n <span *ngIf=\"form.get('username')?.hasError('required')\">{{ 'Login-Username-Required' | translate }}</span>\n </div>\n </div>\n \n <div class=\"input-group inline\">\n <input type=\"password\" id=\"password\" formControlName=\"password\" class=\"form-input\" [placeholder]=\"'Login-Password' | translate\" />\n\n <div class=\"input-group-text\">\n <label for=\"password\">\n <i class=\"fas fa-key fa-fw\"></i>\n </label>\n </div>\n \n <div class=\"validations\">\n <span *ngIf=\"form.get('password')?.hasError('required')\">{{ 'Login-Password-Required' | translate }}</span>\n </div>\n </div>\n \n <div class=\"options\">\n <div>\n <input class=\"form-input\" id=\"rememberMe\" formControlName=\"rememberMe\" type=\"checkbox\">\n <label for=\"rememberMe\">{{ 'Login-RememberMe' | translate }}</label>\n </div>\n \n <shared-language-selector></shared-language-selector>\n </div>\n \n <div class=\"buttons\">\n <button type=\"submit\" class=\"btn\"\n [class.loading]=\"formState.loading && !formState.success && !formState.error\"\n [class.error]=\"formState.error\"\n [class.success]=\"formState.success\">\n <i *ngIf=\"!formState.loading\" class=\"fas fa-sign-in-alt fa-fw\"></i>\n {{ 'Login-SignIn' | translate }}\n </button>\n </div>\n </form>\n </div>\n\n <div class=\"footer\">\n <span [innerHTML]=\"'Login-Copyright' | translate\"></span>\n <span class=\"design\">{{ 'Login-DesignBy' | translate }}</span>\n </div>\n</div>", styles: [":host{display:block}.content{margin-left:auto;margin-right:auto;width:83.333333%;border-radius:.5rem}@media (min-width: 768px){.content{width:66.666667%}}@media (min-width: 1280px){.content{width:41.666667%}}.content>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse));--tw-divide-opacity: 1;border-color:rgb(161 161 170 / var(--tw-divide-opacity, 1))}.content{--tw-shadow: 0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 4px 8px 0 var(--tw-shadow-color), 0 6px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.content .login{position:relative;z-index:10;margin-top:5rem;border-top-left-radius:.5rem;border-top-right-radius:.5rem;background-color:#fff9;padding:5rem 1.5rem 1.5rem;--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.content .login .logo-container{margin-top:-10rem;margin-left:auto;margin-right:auto;margin-bottom:1rem;width:fit-content;border-radius:1.5rem;padding:.5rem;--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-shadow: 0 4px 8px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19);--tw-shadow-colored: 0 4px 8px 0 var(--tw-shadow-color), 0 6px 20px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.content .login .logo-container .logo{width:fit-content;border-radius:1.5rem;padding:.5rem;border-width:2px;--tw-border-opacity: 1;border-color:rgb(252 211 77 / var(--tw-border-opacity, 1))}.content .login .logo-container .logo img{height:10rem;min-width:fit-content}.content .login .title{padding-bottom:1.25rem;text-align:center;font-size:1.875rem;line-height:2.25rem;font-weight:700}.content .login form>.validations{text-align:center;font-weight:700;--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.content .login form>.validations:after{margin-left:auto;margin-right:auto;margin-bottom:.5rem;display:block;border-bottom-width:1px;--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1));--tw-content: \"\";content:var(--tw-content);animation:separator 1s ease-in-out forwards}.content .login form .options{margin-bottom:.75rem;display:flex;flex-direction:row;flex-wrap:wrap-reverse;justify-content:space-between}.content .login form .options shared-language-selector{margin-bottom:.75rem;width:100%}@media (min-width: 640px){.content .login form .options shared-language-selector{margin-bottom:0;width:auto}}.content .login form .buttons{display:flex;justify-content:center;gap:1rem}.content .footer{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem;padding:.125rem .5rem;background-color:#e2e8f099;--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);font-size:.875rem;line-height:1.25rem;font-weight:600;letter-spacing:-.025em;display:flex;flex-wrap:wrap;justify-content:space-between}@keyframes separator{0%{width:0}to{width:100%}}\n"], dependencies: [{ kind: "component", type: LanguageSelectorComponent, selector: "shared-language-selector", inputs: ["showFlag", "title"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: RouterModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
345
619
  }
346
620
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LoginComponent, decorators: [{
347
621
  type: Component,
@@ -860,6 +1134,7 @@ const ZAMBON_SHARED_I18N_RESOURCES = [
860
1134
  { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/language-selector/`, suffix: '.json' },
861
1135
  { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/login/`, suffix: '.json' },
862
1136
  { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/operations-history/`, suffix: '.json' },
1137
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/top-bar/`, suffix: '.json' },
863
1138
  ];
864
1139
  function createZambonSharedTranslateLoader(httpBackend, appResources = []) {
865
1140
  return new MultiTranslateHttpLoader(httpBackend, [
@@ -885,5 +1160,5 @@ function provideZambonSharedTranslateService(config = {}) {
885
1160
  * Generated bundle index. Do not edit.
886
1161
  */
887
1162
 
888
- export { AuthGuard, AuthInterceptor, AuthenticationService, BypassHtmlSanitizerPipe, EnumLabelPipe, FiltersBase, LoginLayoutComponent, MainLayoutComponent, OperationsHistoryChildListComponent, OperationsHistoryModalComponent, OperationsHistoryService, ServicesHistoryChildListComponent, ServicesHistoryService, ServicesHistoryViewComponent, SharedAuthModule, UtcDatePipe, ZAMBON_SHARED_I18N_ASSET, ZAMBON_SHARED_I18N_ASSET_PATH, ZAMBON_SHARED_I18N_RESOURCES, createZambonSharedTranslateLoader, provideZambonSharedTranslateLoader, provideZambonSharedTranslateService };
1163
+ export { AuthGuard, AuthInterceptor, AuthenticationService, BrandComponent, BypassHtmlSanitizerPipe, EnumLabelPipe, EnvironmentBadgeComponent, FiltersBase, LoginLayoutComponent, MainLayoutComponent, NotificationsComponent, NotificationsService, OperationsHistoryChildListComponent, OperationsHistoryModalComponent, OperationsHistoryService, ServicesHistoryChildListComponent, ServicesHistoryService, ServicesHistoryViewComponent, SharedAuthModule, TopBarComponent, UserProfileComponent, UtcDatePipe, ZAMBON_SHARED_I18N_ASSET, ZAMBON_SHARED_I18N_ASSET_PATH, ZAMBON_SHARED_I18N_RESOURCES, createZambonSharedTranslateLoader, provideZambonSharedTranslateLoader, provideZambonSharedTranslateService };
889
1164
  //# sourceMappingURL=zambon-dev-shared.mjs.map