@zambon-dev/shared 1.0.1 → 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,17 +1,19 @@
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
- import { TranslatePipe, TranslateService } from '@ngx-translate/core';
8
- import { map, catchError, interval, mergeMap, tap, Subject, take, takeUntil, finalize, shareReplay, switchMap, of } from 'rxjs';
9
- import { HttpClient } from '@angular/common/http';
7
+ import { TranslatePipe, TranslateService, TranslateLoader, provideTranslateService } from '@ngx-translate/core';
8
+ import { map, catchError, interval, mergeMap, tap, BehaviorSubject, Subject, take, takeUntil, finalize, shareReplay, switchMap, of } from 'rxjs';
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';
16
+ import { MultiTranslateHttpLoader } from 'ngx-translate-multi-http-loader';
15
17
 
16
18
  class LoginLayoutComponent {
17
19
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LoginLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -93,6 +95,84 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
93
95
  }]
94
96
  }], ctorParameters: () => [] });
95
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
+
96
176
  class OperationsHistoryService {
97
177
  //#region ViewChilds, Inputs, Outputs
98
178
  //#endregion
@@ -147,76 +227,137 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
147
227
  }]
148
228
  }] });
149
229
 
150
- 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 {
151
283
  //#region ViewChilds, Inputs, Outputs
152
- logoutModal;
284
+ dropdown;
153
285
  //#endregion
154
286
  //#region Variables
155
- authenticationService = inject(AuthenticationService);
156
- destroy$ = new Subject();
287
+ notificationsService = inject(NotificationsService);
157
288
  router = inject(Router);
158
- sidebarService = inject(SidebarService);
159
- tabService = inject(TabService);
289
+ notifications$ = this.notificationsService.getNotifications();
290
+ showDropdown = false;
291
+ unreadCount$ = this.notificationsService.getUnreadCount();
160
292
  //#endregion
161
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
+ }
162
298
  //#endregion
163
299
  //#region Constructor and Angular life cycle methods
164
- ngOnDestroy() {
165
- this.destroy$.next(true);
166
- this.destroy$.complete();
167
- }
168
300
  ngOnInit() {
169
- const url = this.router.url;
170
- if (url !== '/' && !this.tabService.isUrlOpen(url)) {
171
- this.sidebarService.getMenuFromUrl(url)
172
- .pipe(take(1))
173
- .subscribe((item) => {
174
- if (!!item) {
175
- this.tabService.updateTabTitle(url, item.label);
176
- }
177
- });
178
- }
179
- this.sidebarService.menuUrlSelected
180
- .pipe(takeUntil(this.destroy$))
181
- .subscribe((item) => {
182
- const tab = new Tab({
183
- title: item.label,
184
- url: item.url,
185
- });
186
- this.tabService.openTab(tab);
187
- });
301
+ this.notificationsService.start();
188
302
  }
189
303
  //#endregion
190
304
  //#region Event handlers
191
- //#endregion
192
- //#region Public methods
193
- onLogoutClick() {
194
- this.logoutModal.toggleModal();
305
+ onDropdownClick() {
306
+ this.showDropdown = !this.showDropdown;
195
307
  }
196
- onLogoutConfirm() {
197
- this.authenticationService.signOut();
198
- this.router.navigate(['/login']);
308
+ onMarkAllAsRead() {
309
+ this.notificationsService.markAllAsRead();
199
310
  }
200
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: MainLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
201
- 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" }] });
202
341
  }
203
- 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: [{
204
343
  type: Component,
205
- args: [{ selector: 'shared-main-layout', imports: [
206
- ModalComponent,
207
- SidebarComponent,
208
- RouterModule,
209
- TabsComponent,
344
+ args: [{ selector: 'shared-notifications', imports: [
345
+ AsyncPipe,
210
346
  TranslatePipe,
211
- ], 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"] }]
212
- }], 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: [{
213
349
  type: ViewChild,
214
- args: ['logoutModal']
350
+ args: ['dropdown']
351
+ }], documentClick: [{
352
+ type: HostListener,
353
+ args: ['window:click', ['$event']]
215
354
  }] } });
216
355
 
217
356
  class LanguageSelectorComponent {
218
357
  //#region ViewChilds, Inputs, Outputs
219
358
  dropdown;
359
+ /** When true, the toggle shows the current language's flag (light-gray button) instead of text. */
360
+ showFlag = false;
220
361
  title = 'LanguageSelector-Title';
221
362
  //#endregion
222
363
  //#region Variables
@@ -257,18 +398,18 @@ class LanguageSelectorComponent {
257
398
  }
258
399
  }
259
400
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LanguageSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
260
- 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" }] });
261
402
  }
262
403
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LanguageSelectorComponent, decorators: [{
263
404
  type: Component,
264
405
  args: [{ selector: 'shared-language-selector', imports: [
265
- NgClass,
266
- NgFor,
267
406
  TranslatePipe,
268
- ], 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"] }]
269
408
  }], propDecorators: { dropdown: [{
270
409
  type: ViewChild,
271
410
  args: ['dropdown']
411
+ }], showFlag: [{
412
+ type: Input
272
413
  }], title: [{
273
414
  type: Input
274
415
  }], documentClick: [{
@@ -276,6 +417,140 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
276
417
  args: ['window:click', ['$event']]
277
418
  }] } });
278
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
+
279
554
  class LoginComponent {
280
555
  //#region ViewChilds, Inputs, Outputs
281
556
  username;
@@ -340,7 +615,7 @@ class LoginComponent {
340
615
  }
341
616
  }
342
617
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
343
- 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" }] });
344
619
  }
345
620
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: LoginComponent, decorators: [{
346
621
  type: Component,
@@ -849,9 +1124,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
849
1124
  type: Input
850
1125
  }] } });
851
1126
 
1127
+ const ZAMBON_SHARED_I18N_ASSET_PATH = 'assets/i18n/zambon-dev/shared';
1128
+ const ZAMBON_SHARED_I18N_ASSET = {
1129
+ glob: '**/*.json',
1130
+ input: 'node_modules/@zambon-dev/shared/i18n',
1131
+ output: ZAMBON_SHARED_I18N_ASSET_PATH,
1132
+ };
1133
+ const ZAMBON_SHARED_I18N_RESOURCES = [
1134
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/language-selector/`, suffix: '.json' },
1135
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/login/`, suffix: '.json' },
1136
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/operations-history/`, suffix: '.json' },
1137
+ { prefix: `/${ZAMBON_SHARED_I18N_ASSET_PATH}/top-bar/`, suffix: '.json' },
1138
+ ];
1139
+ function createZambonSharedTranslateLoader(httpBackend, appResources = []) {
1140
+ return new MultiTranslateHttpLoader(httpBackend, [
1141
+ ...ZAMBON_SHARED_I18N_RESOURCES,
1142
+ ...appResources,
1143
+ ]);
1144
+ }
1145
+ function provideZambonSharedTranslateLoader(appResources = []) {
1146
+ return {
1147
+ provide: TranslateLoader,
1148
+ useFactory: (httpBackend) => createZambonSharedTranslateLoader(httpBackend, appResources),
1149
+ deps: [HttpBackend],
1150
+ };
1151
+ }
1152
+ function provideZambonSharedTranslateService(config = {}) {
1153
+ return provideTranslateService({
1154
+ defaultLanguage: config.defaultLanguage,
1155
+ loader: provideZambonSharedTranslateLoader(config.appResources),
1156
+ });
1157
+ }
1158
+
852
1159
  /**
853
1160
  * Generated bundle index. Do not edit.
854
1161
  */
855
1162
 
856
- export { AuthGuard, AuthInterceptor, AuthenticationService, BypassHtmlSanitizerPipe, EnumLabelPipe, FiltersBase, LoginLayoutComponent, MainLayoutComponent, OperationsHistoryChildListComponent, OperationsHistoryModalComponent, OperationsHistoryService, ServicesHistoryChildListComponent, ServicesHistoryService, ServicesHistoryViewComponent, SharedAuthModule, UtcDatePipe };
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 };
857
1164
  //# sourceMappingURL=zambon-dev-shared.mjs.map