@flusys/ng-layout 1.1.1-beta → 1.2.0-rc.1

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,34 +1,19 @@
1
- import * as i1 from '@angular/common';
2
- import { isPlatformBrowser, CommonModule, DOCUMENT as DOCUMENT$1 } from '@angular/common';
3
1
  import * as i0 from '@angular/core';
4
- import { inject, PLATFORM_ID, Injectable, DOCUMENT, signal, computed, effect, Component, ChangeDetectionStrategy, InjectionToken, DestroyRef, ElementRef, viewChild, input, Renderer2, ViewChild } from '@angular/core';
2
+ import { inject, PLATFORM_ID, Injectable, DOCUMENT, signal, DestroyRef, computed, effect, Component, InjectionToken, input, viewChild, ElementRef, Renderer2 } from '@angular/core';
3
+ import * as i3 from '@flusys/ng-shared';
4
+ import { evaluateLogicNode, PermissionValidatorService, InlineSvgComponent, TranslatePipe, TranslateService, IconComponent, AngularModule } from '@flusys/ng-shared';
5
+ import { isPlatformBrowser, NgComponentOutlet, DOCUMENT as DOCUMENT$1 } from '@angular/common';
6
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
+ import { APP_CONFIG, DEFAULT_APP_NAME, DEFAULT_AUTHOR, isCompanyFeatureEnabled, isSignUpEnabled } from '@flusys/ng-core';
8
+ import { Subject, fromEvent, filter as filter$1 } from 'rxjs';
5
9
  import * as i2 from '@angular/forms';
6
10
  import { FormsModule } from '@angular/forms';
7
- import * as i1$2 from '@angular/router';
8
- import { Router, RouterModule, NavigationEnd } from '@angular/router';
9
- import { updatePreset, updateSurfacePalette, $t, definePreset } from '@primeuix/themes';
10
- import Aura from '@primeuix/themes/aura';
11
- import Lara from '@primeuix/themes/lara';
12
- import Nora from '@primeuix/themes/nora';
13
- import { PrimeNG } from 'primeng/config';
14
- import * as i3 from 'primeng/selectbutton';
15
- import { SelectButtonModule } from 'primeng/selectbutton';
16
- import { APP_CONFIG, DEFAULT_APP_NAME, DEFAULT_AUTHOR, isCompanyFeatureEnabled } from '@flusys/ng-core';
17
- import * as i2$2 from '@flusys/ng-shared';
18
- import { evaluateLogicNode, PermissionValidatorService, PlatformService, AngularModule, IconComponent } from '@flusys/ng-shared';
19
- import { Subject, fromEvent, filter as filter$1 } from 'rxjs';
20
- import * as i1$1 from 'primeng/button';
21
- import { ButtonModule } from 'primeng/button';
22
- import * as i2$1 from 'primeng/styleclass';
23
- import { StyleClassModule } from 'primeng/styleclass';
24
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
11
+ import * as i2$1 from '@angular/router';
12
+ import { Router, NavigationEnd, isActive, RouterModule } from '@angular/router';
13
+ import * as i1 from '@flusys/ng-ui';
14
+ import { PRESET_NAMES, applyTheme, SURFACE_PALETTES, PRIMARY_PALETTES, applyPrimaryPalette, applySurfacePalette, UiModule, MessageService } from '@flusys/ng-ui';
15
+ export { applyTheme, buildBootstrapPreset } from '@flusys/ng-ui';
25
16
  import { filter } from 'rxjs/operators';
26
- import { MessageService } from 'primeng/api';
27
- import * as i4 from 'primeng/select';
28
- import { SelectModule } from 'primeng/select';
29
- import * as i2$3 from 'primeng/ripple';
30
- import { RippleModule } from 'primeng/ripple';
31
- import Material from '@primeuix/themes/material';
32
17
 
33
18
  /** Filter launcher apps based on user permission codes */
34
19
  function filterAppsByPermissions(apps, permissionCodes) {
@@ -91,36 +76,11 @@ function filterMenuItem(item, permissionCode) {
91
76
 
92
77
  const STORAGE_KEY = 'flusys.layout.config';
93
78
  const STORAGE_VERSION = 1;
94
- /**
95
- * Service for persisting layout configuration to localStorage.
96
- * Handles loading, saving, and validation of layout preferences with SSR-safe checks.
97
- *
98
- * @example
99
- * ```typescript
100
- * const persistence = inject(LayoutPersistenceService);
101
- *
102
- * // Load saved config
103
- * const config = persistence.load();
104
- *
105
- * // Save config
106
- * persistence.save({ darkTheme: true, preset: 'Aura' });
107
- *
108
- * // Clear saved config
109
- * persistence.clear();
110
- * ```
111
- */
112
79
  class LayoutPersistenceService {
113
80
  platformId = inject(PLATFORM_ID);
114
81
  isBrowser = isPlatformBrowser(this.platformId);
115
82
  validPresets = ['Aura', 'Lara', 'Nora'];
116
- validMenuModes = ['static', 'overlay'];
117
- /**
118
- * Load configuration from localStorage.
119
- * Returns null if no saved config or invalid data.
120
- * SSR-safe (returns null on server).
121
- *
122
- * @returns Validated layout configuration or null
123
- */
83
+ validMenuModes = ['static', 'overlay', 'topbar'];
124
84
  load() {
125
85
  if (!this.isBrowser)
126
86
  return null;
@@ -136,12 +96,6 @@ class LayoutPersistenceService {
136
96
  return null;
137
97
  }
138
98
  }
139
- /**
140
- * Save configuration to localStorage.
141
- * SSR-safe (no-op on server).
142
- *
143
- * @param config Layout configuration to save
144
- */
145
99
  save(config) {
146
100
  if (!this.isBrowser)
147
101
  return;
@@ -156,925 +110,1079 @@ class LayoutPersistenceService {
156
110
  // Silent fail - localStorage may be full or disabled
157
111
  }
158
112
  }
159
- /**
160
- * Clear stored configuration.
161
- * SSR-safe (no-op on server).
162
- */
163
113
  clear() {
164
114
  if (!this.isBrowser)
165
115
  return;
166
116
  localStorage.removeItem(STORAGE_KEY);
167
117
  }
168
- /**
169
- * Validate and sanitize configuration.
170
- * Returns null if version mismatch.
171
- *
172
- * @param config Configuration to validate
173
- * @returns Validated configuration or null
174
- */
175
118
  validateConfig(config) {
176
- // Version mismatch - clear and return null
177
119
  if (config._version && config._version !== STORAGE_VERSION) {
178
120
  this.clear();
179
121
  return null;
180
122
  }
181
- // Validate preset - silently fix invalid values
182
123
  if (config.preset && !this.validPresets.includes(config.preset)) {
183
124
  config.preset = 'Aura';
184
125
  }
185
- // Validate menuMode - silently fix invalid values
186
126
  if (config.menuMode && !this.validMenuModes.includes(config.menuMode)) {
187
127
  config.menuMode = 'static';
188
128
  }
189
- // Validate darkTheme
190
129
  if (typeof config.darkTheme !== 'boolean') {
191
130
  config.darkTheme = false;
192
131
  }
193
- // Remove version field from returned config
194
132
  const { _version, ...cleanConfig } = config;
195
133
  return cleanConfig;
196
134
  }
197
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LayoutPersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
198
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LayoutPersistenceService, providedIn: 'root' });
135
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutPersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
136
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutPersistenceService, providedIn: 'root' });
199
137
  }
200
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LayoutPersistenceService, decorators: [{
138
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutPersistenceService, decorators: [{
201
139
  type: Injectable,
202
- args: [{
203
- providedIn: 'root',
204
- }]
140
+ args: [{ providedIn: 'root' }]
205
141
  }] });
206
142
 
207
- /**
208
- * Service managing layout configuration and state.
209
- * Provides signals for reactive layout updates.
210
- */
143
+ const DESKTOP_BREAKPOINT = 991;
211
144
  class LayoutService {
212
145
  document = inject(DOCUMENT);
213
146
  platformId = inject(PLATFORM_ID);
214
147
  isBrowser = isPlatformBrowser(this.platformId);
215
148
  persistence = inject(LayoutPersistenceService);
216
149
  appConfig = inject(APP_CONFIG, { optional: true });
217
- defaultConfig = {
150
+ DEFAULT_CONFIG = this.appConfig?.layoutConfig ?? {
218
151
  preset: 'Aura',
219
- primary: 'emerald',
152
+ primary: 'violet',
220
153
  surface: null,
221
154
  darkTheme: false,
222
155
  menuMode: 'static',
223
156
  };
224
- // Load persisted config merged with defaults
225
- initialConfig = (() => {
226
- const persisted = this.persistence.load();
227
- return persisted
228
- ? { ...this.defaultConfig, ...persisted }
229
- : this.defaultConfig;
230
- })();
231
- defaultState = {
157
+ DEFAULT_STATE = {
232
158
  staticMenuDesktopInactive: false,
233
159
  overlayMenuActive: false,
234
160
  configSidebarVisible: false,
235
161
  staticMenuMobileActive: false,
236
162
  menuHoverActive: false,
163
+ topbarMenuVisible: true,
237
164
  };
238
- // Signals
239
- layoutConfig = signal(this.initialConfig, ...(ngDevMode ? [{ debugName: "layoutConfig" }] : []));
240
- layoutState = signal(this.defaultState, ...(ngDevMode ? [{ debugName: "layoutState" }] : []));
241
- transitionComplete = signal(false, ...(ngDevMode ? [{ debugName: "transitionComplete" }] : []));
242
- // User Profile Signals
243
- userProfile = signal(null, ...(ngDevMode ? [{ debugName: "userProfile" }] : []));
244
- companyProfile = signal(null, ...(ngDevMode ? [{ debugName: "companyProfile" }] : []));
245
- appName = signal(this.appConfig?.appName ?? DEFAULT_APP_NAME, ...(ngDevMode ? [{ debugName: "appName" }] : []));
246
- // Author/Brand Signals
247
- authorName = signal(this.appConfig?.author?.name ?? DEFAULT_AUTHOR.name, ...(ngDevMode ? [{ debugName: "authorName" }] : []));
248
- authorUrl = signal(this.appConfig?.author?.url ?? DEFAULT_AUTHOR.url, ...(ngDevMode ? [{ debugName: "authorUrl" }] : []));
249
- // App Launcher Signals
250
- _rawApps = signal([], ...(ngDevMode ? [{ debugName: "_rawApps" }] : []));
251
- /**
252
- * Filtered launcher apps based on user permissions.
253
- * Automatically recomputes when raw apps or permissions change.
254
- */
255
- apps = computed(() => {
256
- const raw = this._rawApps();
257
- const permission = this.permissionValidator.permissions();
258
- return filterAppsByPermissions(raw, permission);
259
- }, ...(ngDevMode ? [{ debugName: "apps" }] : []));
260
- // Menu Signals
261
- _rawMenu = signal([], ...(ngDevMode ? [{ debugName: "_rawMenu" }] : []));
165
+ memorable = this.DEFAULT_CONFIG.isMemorable !== false;
166
+ _layoutConfig = signal({
167
+ ...this.DEFAULT_CONFIG,
168
+ ...(this.memorable ? this.persistence.load() : null),
169
+ }, /* @ts-ignore */
170
+ ...(ngDevMode ? [{ debugName: "_layoutConfig" }] : /* istanbul ignore next */ []));
171
+ _layoutState = signal(this.DEFAULT_STATE, /* @ts-ignore */
172
+ ...(ngDevMode ? [{ debugName: "_layoutState" }] : /* istanbul ignore next */ []));
173
+ _transitionComplete = signal(false, /* @ts-ignore */
174
+ ...(ngDevMode ? [{ debugName: "_transitionComplete" }] : /* istanbul ignore next */ []));
175
+ layoutConfig = this._layoutConfig.asReadonly();
176
+ layoutState = this._layoutState.asReadonly();
177
+ transitionComplete = this._transitionComplete.asReadonly();
178
+ _userProfile = signal(null, /* @ts-ignore */
179
+ ...(ngDevMode ? [{ debugName: "_userProfile" }] : /* istanbul ignore next */ []));
180
+ _companyProfile = signal(null, /* @ts-ignore */
181
+ ...(ngDevMode ? [{ debugName: "_companyProfile" }] : /* istanbul ignore next */ []));
182
+ _rawMenu = signal([], /* @ts-ignore */
183
+ ...(ngDevMode ? [{ debugName: "_rawMenu" }] : /* istanbul ignore next */ []));
184
+ _rawApps = signal([], /* @ts-ignore */
185
+ ...(ngDevMode ? [{ debugName: "_rawApps" }] : /* istanbul ignore next */ []));
262
186
  permissionValidator = inject(PermissionValidatorService);
263
- /**
264
- * Filtered menu items based on user permissions.
265
- * Automatically recomputes when raw menu or permission checker changes.
266
- * Role checker is optional - if not set, role-based permissions will be denied.
267
- */
268
- menu = computed(() => {
269
- const raw = this._rawMenu(); // Track permission changes
270
- const permission = this.permissionValidator.permissions();
271
- return filterMenuByPermissions(raw, permission);
272
- }, ...(ngDevMode ? [{ debugName: "menu" }] : []));
273
- // Computed signals - Layout
274
- isSidebarActive = computed(() => this.layoutState().overlayMenuActive ||
275
- this.layoutState().staticMenuMobileActive, ...(ngDevMode ? [{ debugName: "isSidebarActive" }] : []));
276
- isDarkTheme = computed(() => this.layoutConfig().darkTheme, ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : []));
277
- getPrimary = computed(() => this.layoutConfig().primary, ...(ngDevMode ? [{ debugName: "getPrimary" }] : []));
278
- getSurface = computed(() => this.layoutConfig().surface, ...(ngDevMode ? [{ debugName: "getSurface" }] : []));
279
- isOverlay = computed(() => this.layoutConfig().menuMode === 'overlay', ...(ngDevMode ? [{ debugName: "isOverlay" }] : []));
280
- // Computed signals - User Profile
281
- userName = computed(() => this.userProfile()?.name ?? 'User', ...(ngDevMode ? [{ debugName: "userName" }] : []));
282
- userEmail = computed(() => this.userProfile()?.email ?? '', ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
283
- userProfilePictureUrl = computed(() => this.userProfile()?.profilePictureUrl ?? null, ...(ngDevMode ? [{ debugName: "userProfilePictureUrl" }] : []));
187
+ destroyRef = inject(DestroyRef);
188
+ _windowWidth = signal(this.isBrowser ? window.innerWidth : DESKTOP_BREAKPOINT + 1, /* @ts-ignore */
189
+ ...(ngDevMode ? [{ debugName: "_windowWidth" }] : /* istanbul ignore next */ []));
190
+ userProfile = this._userProfile.asReadonly();
191
+ companyProfile = this._companyProfile.asReadonly();
192
+ appName = this.appConfig?.appName ?? DEFAULT_APP_NAME;
193
+ appLogo = this.appConfig?.appLogo ?? '';
194
+ isAppLogoSvg = this.appLogo.toLowerCase().endsWith('.svg');
195
+ authorName = this.appConfig?.author?.name ?? DEFAULT_AUTHOR.name;
196
+ authorUrl = this.appConfig?.author?.url ?? DEFAULT_AUTHOR.url;
197
+ menu = computed(() => filterMenuByPermissions(this._rawMenu(), this.permissionValidator.permissions()), /* @ts-ignore */
198
+ ...(ngDevMode ? [{ debugName: "menu" }] : /* istanbul ignore next */ []));
199
+ apps = computed(() => filterAppsByPermissions(this._rawApps(), this.permissionValidator.permissions()), /* @ts-ignore */
200
+ ...(ngDevMode ? [{ debugName: "apps" }] : /* istanbul ignore next */ []));
201
+ hasApps = computed(() => this.apps().length > 0, /* @ts-ignore */
202
+ ...(ngDevMode ? [{ debugName: "hasApps" }] : /* istanbul ignore next */ []));
203
+ isSidebarActive = computed(() => this._layoutState().overlayMenuActive ||
204
+ this._layoutState().staticMenuMobileActive, /* @ts-ignore */
205
+ ...(ngDevMode ? [{ debugName: "isSidebarActive" }] : /* istanbul ignore next */ []));
206
+ isDesktop = computed(() => this._windowWidth() > DESKTOP_BREAKPOINT, /* @ts-ignore */
207
+ ...(ngDevMode ? [{ debugName: "isDesktop" }] : /* istanbul ignore next */ []));
208
+ isMobile = computed(() => !this.isDesktop(), /* @ts-ignore */
209
+ ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
210
+ isDarkTheme = computed(() => this._layoutConfig().darkTheme, /* @ts-ignore */
211
+ ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : /* istanbul ignore next */ []));
212
+ isOverlay = computed(() => this._layoutConfig().menuMode === 'overlay', /* @ts-ignore */
213
+ ...(ngDevMode ? [{ debugName: "isOverlay" }] : /* istanbul ignore next */ []));
214
+ isTopbar = computed(() => this._layoutConfig().menuMode === 'topbar', /* @ts-ignore */
215
+ ...(ngDevMode ? [{ debugName: "isTopbar" }] : /* istanbul ignore next */ []));
216
+ userName = computed(() => this._userProfile()?.name ?? 'Guest', /* @ts-ignore */
217
+ ...(ngDevMode ? [{ debugName: "userName" }] : /* istanbul ignore next */ []));
218
+ userEmail = computed(() => this._userProfile()?.email ?? '', /* @ts-ignore */
219
+ ...(ngDevMode ? [{ debugName: "userEmail" }] : /* istanbul ignore next */ []));
220
+ userProfilePictureUrl = computed(() => this._userProfile()?.profilePictureUrl ?? null, /* @ts-ignore */
221
+ ...(ngDevMode ? [{ debugName: "userProfilePictureUrl" }] : /* istanbul ignore next */ []));
222
+ companyLogoUrl = computed(() => this._companyProfile()?.logoUrl ?? null, /* @ts-ignore */
223
+ ...(ngDevMode ? [{ debugName: "companyLogoUrl" }] : /* istanbul ignore next */ []));
224
+ isAuthenticated = computed(() => !!this._userProfile(), /* @ts-ignore */
225
+ ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ []));
284
226
  companyName = computed(() => {
285
- // If company feature is disabled, always show app name
286
- if (!this.appConfig || !isCompanyFeatureEnabled(this.appConfig)) {
287
- return this.appName();
288
- }
289
- return this.companyProfile()?.name ?? this.appName();
290
- }, ...(ngDevMode ? [{ debugName: "companyName" }] : []));
291
- companyLogoUrl = computed(() => this.companyProfile()?.logoUrl ?? null, ...(ngDevMode ? [{ debugName: "companyLogoUrl" }] : []));
292
- isAuthenticated = computed(() => !!this.userProfile(), ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : []));
293
- // Computed signals - App Launcher
294
- hasApps = computed(() => this.apps().length > 0, ...(ngDevMode ? [{ debugName: "hasApps" }] : []));
295
- // RxJS Subjects for event communication
296
- configUpdate = new Subject();
227
+ if (!this.appConfig || !isCompanyFeatureEnabled(this.appConfig))
228
+ return this.appName;
229
+ return this._companyProfile()?.name ?? this.appName;
230
+ }, /* @ts-ignore */
231
+ ...(ngDevMode ? [{ debugName: "companyName" }] : /* istanbul ignore next */ []));
232
+ displayLogo = computed(() => this.companyLogoUrl() ?? this.appLogo, /* @ts-ignore */
233
+ ...(ngDevMode ? [{ debugName: "displayLogo" }] : /* istanbul ignore next */ []));
234
+ isDisplayLogoSvg = computed(() => this.displayLogo().toLowerCase().endsWith('.svg'), /* @ts-ignore */
235
+ ...(ngDevMode ? [{ debugName: "isDisplayLogoSvg" }] : /* istanbul ignore next */ []));
297
236
  overlayOpen = new Subject();
298
237
  menuSource = new Subject();
299
238
  resetSource = new Subject();
300
239
  menuSource$ = this.menuSource.asObservable();
301
240
  resetSource$ = this.resetSource.asObservable();
302
- configUpdate$ = this.configUpdate.asObservable();
303
241
  overlayOpen$ = this.overlayOpen.asObservable();
304
242
  initialized = false;
305
243
  constructor() {
244
+ if (this.isBrowser) {
245
+ this.toggleDarkMode();
246
+ fromEvent(window, 'resize')
247
+ .pipe(takeUntilDestroyed(this.destroyRef))
248
+ .subscribe(() => {
249
+ this._windowWidth.set(window.innerWidth);
250
+ });
251
+ }
306
252
  effect(() => {
307
- const config = this.layoutConfig();
308
- if (config) {
309
- this.onConfigUpdate();
310
- }
311
- });
312
- effect(() => {
313
- const config = this.layoutConfig();
314
- if (!this.initialized || !config) {
315
- this.initialized = true;
316
- return;
317
- }
318
- this.handleDarkModeTransition(config);
319
- });
320
- // Auto-save configuration changes to localStorage
321
- effect(() => {
322
- const config = this.layoutConfig();
323
- if (config && this.initialized) {
324
- this.persistence.save(config);
253
+ const config = this._layoutConfig();
254
+ if (this.initialized) {
255
+ this.handleDarkModeTransition(config);
256
+ if (this.memorable)
257
+ this.persistence.save(config);
325
258
  }
259
+ this.initialized = true;
326
260
  });
327
261
  }
262
+ toggleDarkMode(config) {
263
+ const isDark = (config ?? this._layoutConfig()).darkTheme;
264
+ this.document.documentElement.classList.toggle('app-dark', isDark);
265
+ }
328
266
  handleDarkModeTransition(config) {
329
- if (this.document.startViewTransition) {
330
- this.startViewTransition(config);
267
+ const doc = this.document;
268
+ if ('startViewTransition' in doc &&
269
+ typeof doc.startViewTransition === 'function') {
270
+ doc
271
+ .startViewTransition(() => this.toggleDarkMode(config))
272
+ .ready.then(() => this.onTransitionEnd())
273
+ .catch(() => { });
331
274
  }
332
275
  else {
333
276
  this.toggleDarkMode(config);
334
277
  this.onTransitionEnd();
335
278
  }
336
279
  }
337
- startViewTransition(config) {
338
- const transition = this.document.startViewTransition(() => {
339
- this.toggleDarkMode(config);
340
- });
341
- transition.ready.then(() => this.onTransitionEnd()).catch(() => { });
342
- }
343
- toggleDarkMode(config) {
344
- const _config = config || this.layoutConfig();
345
- if (_config.darkTheme) {
346
- this.document.documentElement.classList.add('app-dark');
347
- }
348
- else {
349
- this.document.documentElement.classList.remove('app-dark');
350
- }
351
- }
352
280
  onTransitionEnd() {
353
- this.transitionComplete.set(true);
354
- setTimeout(() => this.transitionComplete.set(false));
281
+ this._transitionComplete.set(true);
282
+ setTimeout(() => this._transitionComplete.set(false));
355
283
  }
356
284
  onMenuToggle() {
357
- if (this.isOverlay()) {
358
- this.layoutState.update((prev) => ({
285
+ const state = this._layoutState();
286
+ // Desktop topbar mode: toggle topbar nav visibility
287
+ if (this.isDesktop() && this.isTopbar()) {
288
+ this._layoutState.update((prev) => ({
289
+ ...prev,
290
+ topbarMenuVisible: !prev.topbarMenuVisible,
291
+ }));
292
+ return;
293
+ }
294
+ // Mobile always uses staticMenuMobileActive regardless of menu mode
295
+ if (!this.isDesktop()) {
296
+ const newMobileActive = !state.staticMenuMobileActive;
297
+ this._layoutState.update((prev) => ({
359
298
  ...prev,
360
- overlayMenuActive: !this.layoutState().overlayMenuActive,
299
+ staticMenuMobileActive: newMobileActive,
361
300
  }));
362
- if (this.layoutState().overlayMenuActive) {
301
+ if (newMobileActive)
363
302
  this.overlayOpen.next();
364
- }
303
+ return;
365
304
  }
366
- if (this.isDesktop()) {
367
- this.layoutState.update((prev) => ({
305
+ // Desktop: use overlay or static mode
306
+ if (this.isOverlay()) {
307
+ const newOverlayActive = !state.overlayMenuActive;
308
+ this._layoutState.update((prev) => ({
368
309
  ...prev,
369
- staticMenuDesktopInactive: !this.layoutState().staticMenuDesktopInactive,
310
+ overlayMenuActive: newOverlayActive,
370
311
  }));
312
+ if (newOverlayActive)
313
+ this.overlayOpen.next();
371
314
  }
372
315
  else {
373
- this.layoutState.update((prev) => ({
316
+ this._layoutState.update((prev) => ({
374
317
  ...prev,
375
- staticMenuMobileActive: !this.layoutState().staticMenuMobileActive,
318
+ staticMenuDesktopInactive: !state.staticMenuDesktopInactive,
376
319
  }));
377
- if (this.layoutState().staticMenuMobileActive) {
378
- this.overlayOpen.next();
379
- }
380
320
  }
381
321
  }
382
- isDesktop() {
383
- return this.isBrowser ? window.innerWidth > 991 : true;
384
- }
385
- isMobile() {
386
- return !this.isDesktop();
387
- }
388
- onConfigUpdate() {
389
- this.configUpdate.next(this.layoutConfig());
390
- }
391
322
  onMenuStateChange(event) {
392
323
  this.menuSource.next(event);
393
324
  }
394
325
  reset() {
395
326
  this.resetSource.next(true);
396
327
  }
397
- // ==========================================================================
398
- // User Profile Methods
399
- // ==========================================================================
400
- /**
401
- * Set the current user profile for display in layout.
402
- * Called by auth integration to sync user data.
403
- */
328
+ updateLayoutConfig(config) {
329
+ this._layoutConfig.update((prev) => ({ ...prev, ...config }));
330
+ }
331
+ updateLayoutState(state) {
332
+ this._layoutState.update((prev) => ({ ...prev, ...state }));
333
+ }
404
334
  setUserProfile(profile) {
405
- this.userProfile.set(profile);
335
+ this._userProfile.set(profile);
406
336
  }
407
- /**
408
- * Set the current company profile for display in layout.
409
- * Called by auth integration to sync company data.
410
- */
411
337
  setCompanyProfile(profile) {
412
- this.companyProfile.set(profile);
338
+ this._companyProfile.set(profile);
413
339
  }
414
- // ==========================================================================
415
- // Menu Methods
416
- // ==========================================================================
417
- /**
418
- * Set the raw menu items (unfiltered).
419
- * Menu will be automatically filtered based on permission checker.
420
- * Called by app initialization to set menu from IAM or other source.
421
- */
422
340
  setMenu(items) {
423
341
  this._rawMenu.set(items);
424
342
  }
425
- /**
426
- * Clear menu and permission/role checkers.
427
- * Called on logout.
428
- */
429
343
  clearMenu() {
430
344
  this._rawMenu.set([]);
431
345
  }
432
- // ==========================================================================
433
- // App Launcher Methods
434
- // ==========================================================================
435
- /**
436
- * Set launcher apps for display in header.
437
- * Apps will be automatically filtered based on user permissions.
438
- * If empty after filtering, the app launcher button is hidden.
439
- */
440
346
  setApps(apps) {
441
347
  this._rawApps.set(apps);
442
348
  }
443
- /**
444
- * Clear launcher apps.
445
- * Called on logout.
446
- */
447
349
  clearApps() {
448
350
  this._rawApps.set([]);
449
351
  }
450
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
451
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LayoutService, providedIn: 'root' });
352
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
353
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutService, providedIn: 'root' });
452
354
  }
453
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LayoutService, decorators: [{
355
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutService, decorators: [{
454
356
  type: Injectable,
357
+ args: [{ providedIn: 'root' }]
358
+ }], ctorParameters: () => [] });
359
+
360
+ class AppFooter {
361
+ layoutService = inject(LayoutService);
362
+ appName = this.layoutService.appName;
363
+ displayLogo = this.layoutService.appLogo;
364
+ isLogoSvg = this.layoutService.isAppLogoSvg;
365
+ authorName = this.layoutService.authorName;
366
+ authorUrl = this.layoutService.authorUrl;
367
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFooter, deps: [], target: i0.ɵɵFactoryTarget.Component });
368
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppFooter, isStandalone: true, selector: "app-footer", ngImport: i0, template: `<div class="layout-footer">
369
+ @if (displayLogo) {
370
+ @if (isLogoSvg) {
371
+ <inline-svg [src]="displayLogo" class="logo-image" />
372
+ } @else {
373
+ <img [src]="displayLogo" [alt]="appName" class="logo-image" />
374
+ }
375
+ }
376
+ <span class="hidden sm:inline">
377
+ {{ appName }} {{ 'layout.footer.by' | translate }}
378
+ <a
379
+ [href]="authorUrl"
380
+ target="_blank"
381
+ rel="noopener noreferrer"
382
+ class="text-primary font-bold hover:underline"
383
+ >{{ authorName }}</a
384
+ >
385
+ </span>
386
+ </div>`, isInline: true, dependencies: [{ kind: "component", type: InlineSvgComponent, selector: "inline-svg", inputs: ["src"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
387
+ }
388
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFooter, decorators: [{
389
+ type: Component,
455
390
  args: [{
456
- providedIn: 'root',
391
+ selector: 'app-footer',
392
+ imports: [TranslatePipe, InlineSvgComponent],
393
+ template: `<div class="layout-footer">
394
+ @if (displayLogo) {
395
+ @if (isLogoSvg) {
396
+ <inline-svg [src]="displayLogo" class="logo-image" />
397
+ } @else {
398
+ <img [src]="displayLogo" [alt]="appName" class="logo-image" />
399
+ }
400
+ }
401
+ <span class="hidden sm:inline">
402
+ {{ appName }} {{ 'layout.footer.by' | translate }}
403
+ <a
404
+ [href]="authorUrl"
405
+ target="_blank"
406
+ rel="noopener noreferrer"
407
+ class="text-primary font-bold hover:underline"
408
+ >{{ authorName }}</a
409
+ >
410
+ </span>
411
+ </div>`,
457
412
  }]
458
- }], ctorParameters: () => [] });
413
+ }] });
459
414
 
460
- const presets = {
461
- Aura,
462
- Lara,
463
- Nora,
464
- };
465
415
  class AppConfigurator {
466
416
  router = inject(Router);
467
- primeng = inject(PrimeNG);
417
+ translateAdapter = inject(TranslateService, { optional: true });
418
+ platformId = inject(PLATFORM_ID);
468
419
  layoutService = inject(LayoutService);
469
- platformService = inject(PlatformService);
470
- presets = Object.keys(presets);
471
- showMenuModeButton = signal(!this.router.url.includes('auth'), ...(ngDevMode ? [{ debugName: "showMenuModeButton" }] : []));
472
- menuModeOptions = [
473
- { label: 'Static', value: 'static' },
474
- { label: 'Overlay', value: 'overlay' },
475
- ];
476
- ngOnInit() {
477
- if (!this.platformService.isServer) {
478
- this.onPresetChange(this.layoutService.layoutConfig().preset);
420
+ presets = PRESET_NAMES;
421
+ showMenuModeButton = signal(!this.router.url.includes('auth'), /* @ts-ignore */
422
+ ...(ngDevMode ? [{ debugName: "showMenuModeButton" }] : /* istanbul ignore next */ []));
423
+ menuModeOptions = computed(() => {
424
+ const options = [
425
+ { label: this.translate('layout.configurator.menu.mode.static'), value: 'static' },
426
+ { label: this.translate('layout.configurator.menu.mode.overlay'), value: 'overlay' },
427
+ ];
428
+ if (this.layoutService.isDesktop()) {
429
+ options.push({ label: this.translate('layout.configurator.menu.mode.topbar'), value: 'topbar' });
430
+ }
431
+ return options;
432
+ }, /* @ts-ignore */
433
+ ...(ngDevMode ? [{ debugName: "menuModeOptions" }] : /* istanbul ignore next */ []));
434
+ constructor() {
435
+ if (isPlatformBrowser(this.platformId)) {
436
+ effect(() => {
437
+ const config = this.layoutService.layoutConfig();
438
+ applyTheme({
439
+ preset: (config.preset ?? 'Aura'),
440
+ primary: config.primary ?? 'violet',
441
+ surface: config.surface ?? null,
442
+ darkTheme: config.darkTheme ?? false,
443
+ });
444
+ });
479
445
  }
480
446
  }
481
- surfaces = [
482
- {
483
- name: 'slate',
484
- palette: {
485
- 0: '#ffffff',
486
- 50: '#f8fafc',
487
- 100: '#f1f5f9',
488
- 200: '#e2e8f0',
489
- 300: '#cbd5e1',
490
- 400: '#94a3b8',
491
- 500: '#64748b',
492
- 600: '#475569',
493
- 700: '#334155',
494
- 800: '#1e293b',
495
- 900: '#0f172a',
496
- 950: '#020617',
497
- },
498
- },
499
- {
500
- name: 'gray',
501
- palette: {
502
- 0: '#ffffff',
503
- 50: '#f9fafb',
504
- 100: '#f3f4f6',
505
- 200: '#e5e7eb',
506
- 300: '#d1d5db',
507
- 400: '#9ca3af',
508
- 500: '#6b7280',
509
- 600: '#4b5563',
510
- 700: '#374151',
511
- 800: '#1f2937',
512
- 900: '#111827',
513
- 950: '#030712',
514
- },
515
- },
516
- {
517
- name: 'zinc',
518
- palette: {
519
- 0: '#ffffff',
520
- 50: '#fafafa',
521
- 100: '#f4f4f5',
522
- 200: '#e4e4e7',
523
- 300: '#d4d4d8',
524
- 400: '#a1a1aa',
525
- 500: '#71717a',
526
- 600: '#52525b',
527
- 700: '#3f3f46',
528
- 800: '#27272a',
529
- 900: '#18181b',
530
- 950: '#09090b',
531
- },
532
- },
533
- {
534
- name: 'neutral',
535
- palette: {
536
- 0: '#ffffff',
537
- 50: '#fafafa',
538
- 100: '#f5f5f5',
539
- 200: '#e5e5e5',
540
- 300: '#d4d4d4',
541
- 400: '#a3a3a3',
542
- 500: '#737373',
543
- 600: '#525252',
544
- 700: '#404040',
545
- 800: '#262626',
546
- 900: '#171717',
547
- 950: '#0a0a0a',
548
- },
549
- },
550
- {
551
- name: 'stone',
552
- palette: {
553
- 0: '#ffffff',
554
- 50: '#fafaf9',
555
- 100: '#f5f5f4',
556
- 200: '#e7e5e4',
557
- 300: '#d6d3d1',
558
- 400: '#a8a29e',
559
- 500: '#78716c',
560
- 600: '#57534e',
561
- 700: '#44403c',
562
- 800: '#292524',
563
- 900: '#1c1917',
564
- 950: '#0c0a09',
565
- },
566
- },
567
- {
568
- name: 'soho',
569
- palette: {
570
- 0: '#ffffff',
571
- 50: '#ececec',
572
- 100: '#dedfdf',
573
- 200: '#c4c4c6',
574
- 300: '#adaeb0',
575
- 400: '#97979b',
576
- 500: '#7f8084',
577
- 600: '#6a6b70',
578
- 700: '#55565b',
579
- 800: '#3f4046',
580
- 900: '#2c2c34',
581
- 950: '#16161d',
582
- },
583
- },
584
- {
585
- name: 'viva',
586
- palette: {
587
- 0: '#ffffff',
588
- 50: '#f3f3f3',
589
- 100: '#e7e7e8',
590
- 200: '#cfd0d0',
591
- 300: '#b7b8b9',
592
- 400: '#9fa1a1',
593
- 500: '#87898a',
594
- 600: '#6e7173',
595
- 700: '#565a5b',
596
- 800: '#3e4244',
597
- 900: '#262b2c',
598
- 950: '#0e1315',
599
- },
600
- },
601
- {
602
- name: 'ocean',
603
- palette: {
604
- 0: '#ffffff',
605
- 50: '#fbfcfc',
606
- 100: '#F7F9F8',
607
- 200: '#EFF3F2',
608
- 300: '#DADEDD',
609
- 400: '#B1B7B6',
610
- 500: '#828787',
611
- 600: '#5F7274',
612
- 700: '#415B61',
613
- 800: '#29444E',
614
- 900: '#183240',
615
- 950: '#0c1920',
616
- },
617
- },
618
- ];
619
- selectedPrimaryColor = computed(() => {
620
- return this.layoutService.layoutConfig().primary;
621
- }, ...(ngDevMode ? [{ debugName: "selectedPrimaryColor" }] : []));
622
- selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface, ...(ngDevMode ? [{ debugName: "selectedSurfaceColor" }] : []));
623
- selectedPreset = computed(() => this.layoutService.layoutConfig().preset, ...(ngDevMode ? [{ debugName: "selectedPreset" }] : []));
624
- menuMode = computed(() => this.layoutService.layoutConfig().menuMode, ...(ngDevMode ? [{ debugName: "menuMode" }] : []));
447
+ surfaces = Object.entries(SURFACE_PALETTES).map(([name, pal]) => ({
448
+ name,
449
+ palette: pal,
450
+ }));
451
+ selectedPrimaryColor = computed(() => this.layoutService.layoutConfig().primary, /* @ts-ignore */
452
+ ...(ngDevMode ? [{ debugName: "selectedPrimaryColor" }] : /* istanbul ignore next */ []));
453
+ selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface, /* @ts-ignore */
454
+ ...(ngDevMode ? [{ debugName: "selectedSurfaceColor" }] : /* istanbul ignore next */ []));
455
+ isSurfaceSelected(surfaceName) {
456
+ const sel = this.selectedSurfaceColor();
457
+ if (sel)
458
+ return sel === surfaceName;
459
+ const isDark = this.layoutService.layoutConfig().darkTheme;
460
+ return isDark ? surfaceName === 'zinc' : surfaceName === 'slate';
461
+ }
462
+ selectedPreset = computed(() => this.layoutService.layoutConfig().preset, /* @ts-ignore */
463
+ ...(ngDevMode ? [{ debugName: "selectedPreset" }] : /* istanbul ignore next */ []));
464
+ menuMode = computed(() => this.layoutService.layoutConfig().menuMode, /* @ts-ignore */
465
+ ...(ngDevMode ? [{ debugName: "menuMode" }] : /* istanbul ignore next */ []));
625
466
  primaryColors = computed(() => {
626
- const presetPalette = presets[this.layoutService.layoutConfig().preset].primitive;
627
- const colors = [
628
- 'emerald',
629
- 'green',
630
- 'lime',
631
- 'orange',
632
- 'amber',
633
- 'yellow',
634
- 'teal',
635
- 'cyan',
636
- 'sky',
637
- 'blue',
638
- 'indigo',
639
- 'violet',
640
- 'purple',
641
- 'fuchsia',
642
- 'pink',
643
- 'rose',
644
- ];
645
467
  const palettes = [{ name: 'noir', palette: {} }];
646
- colors.forEach((color) => {
647
- palettes.push({
648
- name: color,
649
- palette: presetPalette?.[color],
650
- });
651
- });
652
- return palettes;
653
- }, ...(ngDevMode ? [{ debugName: "primaryColors" }] : []));
654
- getPresetExt() {
655
- const color = this.primaryColors().find((c) => c.name === this.selectedPrimaryColor()) || {};
656
- const preset = this.layoutService.layoutConfig().preset;
657
- if (color.name === 'noir') {
658
- return {
659
- semantic: {
660
- primary: {
661
- 50: '{surface.50}',
662
- 100: '{surface.100}',
663
- 200: '{surface.200}',
664
- 300: '{surface.300}',
665
- 400: '{surface.400}',
666
- 500: '{surface.500}',
667
- 600: '{surface.600}',
668
- 700: '{surface.700}',
669
- 800: '{surface.800}',
670
- 900: '{surface.900}',
671
- 950: '{surface.950}',
672
- },
673
- colorScheme: {
674
- light: {
675
- primary: {
676
- color: '{primary.950}',
677
- contrastColor: '#ffffff',
678
- hoverColor: '{primary.800}',
679
- activeColor: '{primary.700}',
680
- },
681
- highlight: {
682
- background: '{primary.950}',
683
- focusBackground: '{primary.700}',
684
- color: '#ffffff',
685
- focusColor: '#ffffff',
686
- },
687
- },
688
- dark: {
689
- primary: {
690
- color: '{primary.50}',
691
- contrastColor: '{primary.950}',
692
- hoverColor: '{primary.200}',
693
- activeColor: '{primary.300}',
694
- },
695
- highlight: {
696
- background: '{primary.50}',
697
- focusBackground: '{primary.300}',
698
- color: '{primary.950}',
699
- focusColor: '{primary.950}',
700
- },
701
- },
702
- },
703
- },
704
- };
705
- }
706
- else {
707
- if (preset === 'Nora') {
708
- return {
709
- semantic: {
710
- primary: color.palette,
711
- colorScheme: {
712
- light: {
713
- primary: {
714
- color: '{primary.600}',
715
- contrastColor: '#ffffff',
716
- hoverColor: '{primary.700}',
717
- activeColor: '{primary.800}',
718
- },
719
- highlight: {
720
- background: '{primary.600}',
721
- focusBackground: '{primary.700}',
722
- color: '#ffffff',
723
- focusColor: '#ffffff',
724
- },
725
- },
726
- dark: {
727
- primary: {
728
- color: '{primary.500}',
729
- contrastColor: '{surface.900}',
730
- hoverColor: '{primary.400}',
731
- activeColor: '{primary.300}',
732
- },
733
- highlight: {
734
- background: '{primary.500}',
735
- focusBackground: '{primary.400}',
736
- color: '{surface.900}',
737
- focusColor: '{surface.900}',
738
- },
739
- },
740
- },
741
- },
742
- };
743
- }
744
- else {
745
- return {
746
- semantic: {
747
- primary: color.palette,
748
- colorScheme: {
749
- light: {
750
- primary: {
751
- color: '{primary.500}',
752
- contrastColor: '#ffffff',
753
- hoverColor: '{primary.600}',
754
- activeColor: '{primary.700}',
755
- },
756
- highlight: {
757
- background: '{primary.50}',
758
- focusBackground: '{primary.100}',
759
- color: '{primary.700}',
760
- focusColor: '{primary.800}',
761
- },
762
- },
763
- dark: {
764
- primary: {
765
- color: '{primary.400}',
766
- contrastColor: '{surface.900}',
767
- hoverColor: '{primary.300}',
768
- activeColor: '{primary.200}',
769
- },
770
- highlight: {
771
- background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
772
- focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
773
- color: 'rgba(255,255,255,.87)',
774
- focusColor: 'rgba(255,255,255,.87)',
775
- },
776
- },
777
- },
778
- },
779
- };
780
- }
468
+ for (const [name, pal] of Object.entries(PRIMARY_PALETTES)) {
469
+ palettes.push({ name, palette: pal });
781
470
  }
782
- }
471
+ return palettes;
472
+ }, /* @ts-ignore */
473
+ ...(ngDevMode ? [{ debugName: "primaryColors" }] : /* istanbul ignore next */ []));
783
474
  updateColors(event, type, color) {
784
475
  if (type === 'primary') {
785
- this.layoutService.layoutConfig.update((state) => ({
786
- ...state,
787
- primary: color.name,
788
- }));
476
+ this.layoutService.updateLayoutConfig({ primary: color.name });
789
477
  }
790
478
  else if (type === 'surface') {
791
- this.layoutService.layoutConfig.update((state) => ({
792
- ...state,
793
- surface: color.name,
794
- }));
479
+ this.layoutService.updateLayoutConfig({ surface: color.name });
795
480
  }
796
- this.applyTheme(type, color);
481
+ this._applyColorChange(type, color);
797
482
  event.stopPropagation();
798
483
  }
799
- applyTheme(type, color) {
484
+ _applyColorChange(type, color) {
485
+ const isDark = this.layoutService.layoutConfig().darkTheme ?? false;
800
486
  if (type === 'primary') {
801
- updatePreset(this.getPresetExt());
487
+ const primary = color.name ?? 'violet';
488
+ const pal = primary === 'noir'
489
+ ? Object.fromEntries(Object.entries(SURFACE_PALETTES['slate']).map(([k, v]) => [k, v]))
490
+ : PRIMARY_PALETTES[primary];
491
+ if (pal)
492
+ applyPrimaryPalette(pal, isDark);
802
493
  }
803
494
  else if (type === 'surface') {
804
- updateSurfacePalette(color.palette);
495
+ const surfacePal = SURFACE_PALETTES[color.name ?? 'slate'];
496
+ if (surfacePal)
497
+ applySurfacePalette(surfacePal);
805
498
  }
806
499
  }
807
500
  onPresetChange(event) {
808
- this.layoutService.layoutConfig.update((state) => ({
809
- ...state,
810
- preset: event,
811
- }));
812
- const preset = presets[event];
813
- const surfacePalette = this.surfaces.find((s) => s.name === this.selectedSurfaceColor())?.palette;
814
- $t()
815
- .preset(preset)
816
- .preset(this.getPresetExt())
817
- .surfacePalette(surfacePalette)
818
- .use({ useDefaultOptions: true });
501
+ this.layoutService.updateLayoutConfig({ preset: event });
502
+ const isDark = this.layoutService.layoutConfig().darkTheme ?? false;
503
+ const primary = this.layoutService.layoutConfig().primary ?? 'violet';
504
+ const surface = this.layoutService.layoutConfig().surface ?? null;
505
+ applyTheme({ preset: event, primary, surface, darkTheme: isDark });
819
506
  }
820
507
  onMenuModeChange(event) {
821
- this.layoutService.layoutConfig.update((prev) => ({
822
- ...prev,
823
- menuMode: event,
824
- }));
508
+ this.layoutService.updateLayoutConfig({ menuMode: event });
509
+ }
510
+ translate(key) {
511
+ return this.translateAdapter?.translate(key) ?? key;
825
512
  }
826
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
827
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AppConfigurator, isStandalone: true, selector: "app-configurator", host: { styleAttribute: "background-color: var(--surface-overlay)", classAttribute: "hidden absolute top-[3.25rem] right-0 w-72 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]" }, ngImport: i0, template: `
513
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
514
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppConfigurator, isStandalone: true, selector: "app-configurator", host: { styleAttribute: "background-color: var(--surface-overlay); inset-inline-end: 0;", classAttribute: "hidden absolute top-[3.25rem] w-[calc(100vw-2rem)] sm:w-72 max-w-72 p-4 border border-surface rounded-border origin-top shadow-xl" }, ngImport: i0, template: `
828
515
  <div class="flex flex-col gap-4">
829
516
  <div>
830
- <span class="text-sm text-muted-color font-semibold">Primary</span>
517
+ <span class="text-sm text-muted-color font-semibold">{{
518
+ 'layout.configurator.primary' | translate
519
+ }}</span>
831
520
  <div class="pt-2 flex gap-2 flex-wrap justify-start">
832
521
  @for (primaryColor of primaryColors(); track primaryColor.name) {
833
522
  <button
834
523
  type="button"
835
524
  [title]="primaryColor.name"
836
525
  (click)="updateColors($event, 'primary', primaryColor)"
837
- [ngClass]="{
838
- 'outline-primary': primaryColor.name === selectedPrimaryColor()
839
- }"
526
+ [class.outline-primary]="primaryColor.name === selectedPrimaryColor()"
840
527
  class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
841
- [style]="{
842
- 'background-color': primaryColor?.name === 'noir' ? 'var(--text-color)' : primaryColor?.palette?.['500']
843
- }"
528
+ [style.background-color]="
529
+ primaryColor?.name === 'noir'
530
+ ? 'var(--f-text-color)'
531
+ : primaryColor?.palette?.['500']
532
+ "
844
533
  ></button>
845
534
  }
846
535
  </div>
847
536
  </div>
848
537
  <div>
849
- <span class="text-sm text-muted-color font-semibold">Surface</span>
538
+ <span class="text-sm text-muted-color font-semibold">{{
539
+ 'layout.configurator.surface' | translate
540
+ }}</span>
850
541
  <div class="pt-2 flex gap-2 flex-wrap justify-start">
851
542
  @for (surface of surfaces; track surface.name) {
852
543
  <button
853
544
  type="button"
854
545
  [title]="surface.name"
855
546
  (click)="updateColors($event, 'surface', surface)"
856
- [ngClass]="{
857
- 'outline-primary': selectedSurfaceColor()
858
- ? selectedSurfaceColor() === surface.name
859
- : layoutService.layoutConfig().darkTheme
860
- ? surface.name === 'zinc'
861
- : surface.name === 'slate'
862
- }"
547
+ [class.outline-primary]="isSurfaceSelected(surface.name)"
863
548
  class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
864
- [style]="{
865
- 'background-color': surface?.name === 'noir' ? 'var(--text-color)' : surface?.palette?.['500']
866
- }"
549
+ [style.background-color]="
550
+ surface?.name === 'noir'
551
+ ? 'var(--f-text-color)'
552
+ : surface?.palette?.['500']
553
+ "
867
554
  ></button>
868
555
  }
869
556
  </div>
870
557
  </div>
871
558
  <div class="flex flex-col gap-2">
872
- <span class="text-sm text-muted-color font-semibold">Presets</span>
873
- <p-selectbutton
559
+ <span class="text-sm text-muted-color font-semibold">{{
560
+ 'layout.configurator.presets' | translate
561
+ }}</span>
562
+ <f-selectbutton
874
563
  [options]="presets"
875
564
  [ngModel]="selectedPreset()"
876
565
  (ngModelChange)="onPresetChange($event)"
877
566
  [allowEmpty]="false"
878
567
  size="small"
879
- />
568
+ />
880
569
  </div>
881
570
  @if (showMenuModeButton()) {
882
571
  <div class="flex flex-col gap-2">
883
- <span class="text-sm text-muted-color font-semibold">Menu Mode</span>
884
- <p-selectbutton
572
+ <span class="text-sm text-muted-color font-semibold">{{
573
+ 'layout.configurator.menu.mode' | translate
574
+ }}</span>
575
+ <f-selectbutton
885
576
  [ngModel]="menuMode()"
886
577
  (ngModelChange)="onMenuModeChange($event)"
887
- [options]="menuModeOptions"
578
+ [options]="menuModeOptions()"
888
579
  [allowEmpty]="false"
889
580
  size="small"
890
- />
581
+ />
891
582
  </div>
892
583
  }
893
584
  </div>
894
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i3.SelectButton, selector: "p-selectButton, p-selectbutton, p-select-button", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "unselectable", "tabindex", "multiple", "allowEmpty", "styleClass", "ariaLabelledBy", "dataKey", "autofocus", "size", "fluid"], outputs: ["onOptionClick", "onChange"] }] });
585
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "component", type: i1.SelectButtonComponent, selector: "f-selectbutton", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "multiple", "disabled", "invalid", "allowEmpty", "size", "styleClass", "style"], outputs: ["onChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
895
586
  }
896
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppConfigurator, decorators: [{
587
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppConfigurator, decorators: [{
897
588
  type: Component,
898
589
  args: [{
899
590
  selector: 'app-configurator',
900
- standalone: true,
901
- imports: [CommonModule, FormsModule, SelectButtonModule],
591
+ imports: [UiModule, FormsModule, TranslatePipe],
902
592
  template: `
903
593
  <div class="flex flex-col gap-4">
904
594
  <div>
905
- <span class="text-sm text-muted-color font-semibold">Primary</span>
595
+ <span class="text-sm text-muted-color font-semibold">{{
596
+ 'layout.configurator.primary' | translate
597
+ }}</span>
906
598
  <div class="pt-2 flex gap-2 flex-wrap justify-start">
907
599
  @for (primaryColor of primaryColors(); track primaryColor.name) {
908
600
  <button
909
601
  type="button"
910
602
  [title]="primaryColor.name"
911
603
  (click)="updateColors($event, 'primary', primaryColor)"
912
- [ngClass]="{
913
- 'outline-primary': primaryColor.name === selectedPrimaryColor()
914
- }"
604
+ [class.outline-primary]="primaryColor.name === selectedPrimaryColor()"
915
605
  class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
916
- [style]="{
917
- 'background-color': primaryColor?.name === 'noir' ? 'var(--text-color)' : primaryColor?.palette?.['500']
918
- }"
606
+ [style.background-color]="
607
+ primaryColor?.name === 'noir'
608
+ ? 'var(--f-text-color)'
609
+ : primaryColor?.palette?.['500']
610
+ "
919
611
  ></button>
920
612
  }
921
613
  </div>
922
614
  </div>
923
615
  <div>
924
- <span class="text-sm text-muted-color font-semibold">Surface</span>
616
+ <span class="text-sm text-muted-color font-semibold">{{
617
+ 'layout.configurator.surface' | translate
618
+ }}</span>
925
619
  <div class="pt-2 flex gap-2 flex-wrap justify-start">
926
620
  @for (surface of surfaces; track surface.name) {
927
621
  <button
928
622
  type="button"
929
623
  [title]="surface.name"
930
624
  (click)="updateColors($event, 'surface', surface)"
931
- [ngClass]="{
932
- 'outline-primary': selectedSurfaceColor()
933
- ? selectedSurfaceColor() === surface.name
934
- : layoutService.layoutConfig().darkTheme
935
- ? surface.name === 'zinc'
936
- : surface.name === 'slate'
937
- }"
625
+ [class.outline-primary]="isSurfaceSelected(surface.name)"
938
626
  class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
939
- [style]="{
940
- 'background-color': surface?.name === 'noir' ? 'var(--text-color)' : surface?.palette?.['500']
941
- }"
627
+ [style.background-color]="
628
+ surface?.name === 'noir'
629
+ ? 'var(--f-text-color)'
630
+ : surface?.palette?.['500']
631
+ "
942
632
  ></button>
943
633
  }
944
634
  </div>
945
635
  </div>
946
636
  <div class="flex flex-col gap-2">
947
- <span class="text-sm text-muted-color font-semibold">Presets</span>
948
- <p-selectbutton
637
+ <span class="text-sm text-muted-color font-semibold">{{
638
+ 'layout.configurator.presets' | translate
639
+ }}</span>
640
+ <f-selectbutton
949
641
  [options]="presets"
950
642
  [ngModel]="selectedPreset()"
951
643
  (ngModelChange)="onPresetChange($event)"
952
644
  [allowEmpty]="false"
953
645
  size="small"
954
- />
646
+ />
955
647
  </div>
956
648
  @if (showMenuModeButton()) {
957
649
  <div class="flex flex-col gap-2">
958
- <span class="text-sm text-muted-color font-semibold">Menu Mode</span>
959
- <p-selectbutton
650
+ <span class="text-sm text-muted-color font-semibold">{{
651
+ 'layout.configurator.menu.mode' | translate
652
+ }}</span>
653
+ <f-selectbutton
960
654
  [ngModel]="menuMode()"
961
655
  (ngModelChange)="onMenuModeChange($event)"
962
- [options]="menuModeOptions"
656
+ [options]="menuModeOptions()"
963
657
  [allowEmpty]="false"
964
658
  size="small"
965
- />
659
+ />
966
660
  </div>
967
661
  }
968
662
  </div>
969
- `,
663
+ `,
970
664
  host: {
971
- class: 'hidden absolute top-[3.25rem] right-0 w-72 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]',
972
- style: 'background-color: var(--surface-overlay)',
665
+ class: 'hidden absolute top-[3.25rem] w-[calc(100vw-2rem)] sm:w-72 max-w-72 p-4 border border-surface rounded-border origin-top shadow-xl',
666
+ style: 'background-color: var(--surface-overlay); inset-inline-end: 0;',
973
667
  },
974
668
  }]
975
- }] });
669
+ }], ctorParameters: () => [] });
670
+
671
+ const LAYOUT_AUTH_STATE = new InjectionToken('LAYOUT_AUTH_STATE');
672
+ const LAYOUT_AUTH_API = new InjectionToken('LAYOUT_AUTH_API');
673
+ const LAYOUT_NOTIFICATION_BELL = new InjectionToken('LAYOUT_NOTIFICATION_BELL');
674
+ const LAYOUT_LANGUAGE_SELECTOR = new InjectionToken('LAYOUT_LANGUAGE_SELECTOR');
675
+ const LAYOUT_LANGUAGE_SELECTOR_PANEL = new InjectionToken('LAYOUT_LANGUAGE_SELECTOR_PANEL');
676
+ const LAYOUT_SEARCHBAR = new InjectionToken('LAYOUT_SEARCHBAR');
677
+ const LAYOUT_IS_RTL = new InjectionToken('LAYOUT_IS_RTL');
678
+ const LAYOUT_SEARCH_ADAPTER = new InjectionToken('LAYOUT_SEARCH_ADAPTER');
976
679
 
977
680
  class AppFloatingConfigurator {
978
- LayoutService = inject(LayoutService);
979
- isDarkTheme = computed(() => this.LayoutService.layoutConfig().darkTheme, ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : []));
681
+ layoutService = inject(LayoutService);
682
+ languageSelectorPanel = inject(LAYOUT_LANGUAGE_SELECTOR_PANEL, {
683
+ optional: true,
684
+ });
685
+ isDarkTheme = computed(() => this.layoutService.layoutConfig().darkTheme, /* @ts-ignore */
686
+ ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : /* istanbul ignore next */ []));
980
687
  toggleDarkMode() {
981
- this.LayoutService.layoutConfig.update((state) => ({ ...state, darkTheme: !state.darkTheme }));
982
- }
983
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppFloatingConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
984
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: AppFloatingConfigurator, isStandalone: true, selector: "app-floating-configurator", ngImport: i0, template: `
985
- <div class="fixed flex flex-col md:flex-row gap-4 top-8 right-0 md:right-8 z-99">
986
- <p-button type="button" (onClick)="toggleDarkMode()" [rounded]="true" [icon]="isDarkTheme() ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary" />
987
- <div class="relative">
988
- <p-button icon="pi pi-palette" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true" type="button" rounded />
989
- <app-configurator />
990
- </div>
688
+ const currentDarkTheme = this.layoutService.layoutConfig().darkTheme;
689
+ this.layoutService.updateLayoutConfig({ darkTheme: !currentDarkTheme });
690
+ }
691
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFloatingConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
692
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppFloatingConfigurator, isStandalone: true, selector: "app-floating-configurator", ngImport: i0, template: `
693
+ <div
694
+ class="floating-configurator fixed flex flex-col md:flex-row gap-2 md:gap-4 top-4 md:top-8 z-50"
695
+ >
696
+ <button
697
+ fButton
698
+ type="button"
699
+ (click)="toggleDarkMode()"
700
+ [rounded]="true"
701
+ severity="secondary"
702
+ >
703
+ <ui-icon pButtonIcon [name]="isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
704
+ </button>
705
+ @if (languageSelectorPanel) {
706
+ <div class="relative">
707
+ <button
708
+ fButton
709
+ fStyleClass="@next"
710
+ enterFromClass="hidden"
711
+ enterActiveClass="animate-scalein"
712
+ leaveToClass="hidden"
713
+ leaveActiveClass="animate-fadeout"
714
+ [hideOnOutsideClick]="true"
715
+ type="button"
716
+ rounded
717
+ >
718
+ <ui-icon pButtonIcon name="globe"></ui-icon>
719
+ </button>
720
+ <ng-container *ngComponentOutlet="languageSelectorPanel" />
991
721
  </div>
992
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i1$1.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "component", type: AppConfigurator, selector: "app-configurator" }] });
722
+ }
723
+ <div class="relative">
724
+ <button
725
+ fButton
726
+ fStyleClass="@next"
727
+ enterFromClass="hidden"
728
+ enterActiveClass="animate-scalein"
729
+ leaveToClass="hidden"
730
+ leaveActiveClass="animate-fadeout"
731
+ [hideOnOutsideClick]="true"
732
+ type="button"
733
+ rounded
734
+ >
735
+ <ui-icon pButtonIcon name="palette"></ui-icon>
736
+ </button>
737
+ <app-configurator />
738
+ </div>
739
+ </div>
740
+ `, isInline: true, styles: [".floating-configurator{inset-inline-end:.5rem}@media(min-width:768px){.floating-configurator{inset-inline-end:2rem}}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: UiModule }, { kind: "directive", type: i1.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "directive", type: i1.StyleClassDirective, selector: "[fStyleClass]", inputs: ["fStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass"] }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "component", type: AppConfigurator, selector: "app-configurator" }] });
993
741
  }
994
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppFloatingConfigurator, decorators: [{
742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFloatingConfigurator, decorators: [{
995
743
  type: Component,
996
- args: [{
997
- selector: 'app-floating-configurator',
998
- imports: [ButtonModule, StyleClassModule, AppConfigurator],
999
- template: `
1000
- <div class="fixed flex flex-col md:flex-row gap-4 top-8 right-0 md:right-8 z-99">
1001
- <p-button type="button" (onClick)="toggleDarkMode()" [rounded]="true" [icon]="isDarkTheme() ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary" />
1002
- <div class="relative">
1003
- <p-button icon="pi pi-palette" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true" type="button" rounded />
1004
- <app-configurator />
1005
- </div>
744
+ args: [{ selector: 'app-floating-configurator', imports: [NgComponentOutlet, UiModule, AppConfigurator], template: `
745
+ <div
746
+ class="floating-configurator fixed flex flex-col md:flex-row gap-2 md:gap-4 top-4 md:top-8 z-50"
747
+ >
748
+ <button
749
+ fButton
750
+ type="button"
751
+ (click)="toggleDarkMode()"
752
+ [rounded]="true"
753
+ severity="secondary"
754
+ >
755
+ <ui-icon pButtonIcon [name]="isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
756
+ </button>
757
+ @if (languageSelectorPanel) {
758
+ <div class="relative">
759
+ <button
760
+ fButton
761
+ fStyleClass="@next"
762
+ enterFromClass="hidden"
763
+ enterActiveClass="animate-scalein"
764
+ leaveToClass="hidden"
765
+ leaveActiveClass="animate-fadeout"
766
+ [hideOnOutsideClick]="true"
767
+ type="button"
768
+ rounded
769
+ >
770
+ <ui-icon pButtonIcon name="globe"></ui-icon>
771
+ </button>
772
+ <ng-container *ngComponentOutlet="languageSelectorPanel" />
1006
773
  </div>
1007
- `
1008
- }]
774
+ }
775
+ <div class="relative">
776
+ <button
777
+ fButton
778
+ fStyleClass="@next"
779
+ enterFromClass="hidden"
780
+ enterActiveClass="animate-scalein"
781
+ leaveToClass="hidden"
782
+ leaveActiveClass="animate-fadeout"
783
+ [hideOnOutsideClick]="true"
784
+ type="button"
785
+ rounded
786
+ >
787
+ <ui-icon pButtonIcon name="palette"></ui-icon>
788
+ </button>
789
+ <app-configurator />
790
+ </div>
791
+ </div>
792
+ `, styles: [".floating-configurator{inset-inline-end:.5rem}@media(min-width:768px){.floating-configurator{inset-inline-end:2rem}}\n"] }]
1009
793
  }] });
1010
794
 
1011
- class AppFooter {
795
+ class AppMenuitem {
796
+ // Signal inputs
797
+ item = input.required(/* @ts-ignore */
798
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
799
+ index = input.required(/* @ts-ignore */
800
+ ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
801
+ parentKey = input('', /* @ts-ignore */
802
+ ...(ngDevMode ? [{ debugName: "parentKey" }] : /* istanbul ignore next */ []));
803
+ // Injected services
804
+ router = inject(Router);
1012
805
  layoutService = inject(LayoutService);
1013
- // Footer shows product branding (appName), not user's company
1014
- appName = this.layoutService.appName;
1015
- authorName = this.layoutService.authorName;
1016
- authorUrl = this.layoutService.authorUrl;
1017
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppFooter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1018
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: AppFooter, isStandalone: true, selector: "app-footer", ngImport: i0, template: `<div class="layout-footer">
1019
- {{ appName() }} by
1020
- <a [href]="authorUrl()" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">{{ authorName() }}</a>
1021
- </div>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1022
- }
1023
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppFooter, decorators: [{
1024
- type: Component,
1025
- args: [{
1026
- standalone: true,
1027
- selector: 'app-footer',
1028
- changeDetection: ChangeDetectionStrategy.OnPush,
1029
- template: `<div class="layout-footer">
1030
- {{ appName() }} by
1031
- <a [href]="authorUrl()" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">{{ authorName() }}</a>
1032
- </div>`,
1033
- }]
1034
- }] });
1035
-
1036
- const LAYOUT_AUTH_STATE = new InjectionToken('LAYOUT_AUTH_STATE');
1037
- const LAYOUT_AUTH_API = new InjectionToken('LAYOUT_AUTH_API');
1038
-
1039
- /** Company/branch switcher displayed in top bar */
1040
- class AppCompanyBranchSelector {
1041
806
  destroyRef = inject(DestroyRef);
1042
- authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1043
- authApi = inject(LAYOUT_AUTH_API, { optional: true });
1044
- messageService = inject(MessageService);
1045
- document = inject(DOCUMENT$1);
1046
- elementRef = inject(ElementRef);
1047
- isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
807
+ // View references
808
+ parentLink = viewChild('parentLink', /* @ts-ignore */
809
+ ...(ngDevMode ? [{ debugName: "parentLink" }] : /* istanbul ignore next */ []));
810
+ // State signals - private writable + public readonly pattern
811
+ _active = signal(false, /* @ts-ignore */
812
+ ...(ngDevMode ? [{ debugName: "_active" }] : /* istanbul ignore next */ []));
813
+ active = this._active.asReadonly();
814
+ // Computed signals
815
+ key = computed(() => {
816
+ const parent = this.parentKey();
817
+ return parent ? `${parent}-${this.index()}` : String(this.index());
818
+ }, /* @ts-ignore */
819
+ ...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
820
+ routerLink = computed(() => {
821
+ const menuItem = this.item();
822
+ return menuItem.routerLink ?? [];
823
+ }, /* @ts-ignore */
824
+ ...(ngDevMode ? [{ debugName: "routerLink" }] : /* istanbul ignore next */ []));
825
+ // Computed options for routerLinkActive - use 'exact' for root, 'subset' for others
826
+ routerLinkActiveOptions = computed(() => {
827
+ const link = this.routerLink();
828
+ const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
829
+ return {
830
+ paths: pathsMatch,
831
+ queryParams: 'ignored',
832
+ matrixParams: 'ignored',
833
+ fragment: 'ignored',
834
+ };
835
+ }, /* @ts-ignore */
836
+ ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : /* istanbul ignore next */ []));
1048
837
  constructor() {
1049
- fromEvent(this.document, 'click')
1050
- .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1051
- .subscribe((event) => {
1052
- const target = event.target;
1053
- if (!this.elementRef.nativeElement.contains(target)) {
1054
- this.isActive.set(false);
1055
- }
838
+ // Subscribe to menu source changes
839
+ this.layoutService.menuSource$
840
+ .pipe(takeUntilDestroyed(this.destroyRef))
841
+ .subscribe((value) => {
842
+ Promise.resolve(null).then(() => {
843
+ const currentKey = this.key();
844
+ if (value.routeEvent) {
845
+ this._active.set(value.key === currentKey ||
846
+ value.key?.startsWith(currentKey + '-'));
847
+ }
848
+ else if (value.key !== currentKey &&
849
+ !value.key?.startsWith(currentKey + '-')) {
850
+ this._active.set(false);
851
+ }
852
+ });
1056
853
  });
1057
- }
1058
- currentCompanyName = computed(() => this.authState?.currentCompanyInfo()?.name ?? 'No Company', ...(ngDevMode ? [{ debugName: "currentCompanyName" }] : []));
1059
- currentBranchName = computed(() => this.authState?.currentBranchInfo()?.name ?? null, ...(ngDevMode ? [{ debugName: "currentBranchName" }] : []));
1060
- companies = signal([], ...(ngDevMode ? [{ debugName: "companies" }] : []));
1061
- branches = signal([], ...(ngDevMode ? [{ debugName: "branches" }] : []));
1062
- selectedCompanyId = signal(null, ...(ngDevMode ? [{ debugName: "selectedCompanyId" }] : []));
1063
- selectedBranchId = signal(null, ...(ngDevMode ? [{ debugName: "selectedBranchId" }] : []));
1064
- isLoadingCompanies = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingCompanies" }] : []));
1065
- isLoadingBranches = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingBranches" }] : []));
1066
- isSwitching = signal(false, ...(ngDevMode ? [{ debugName: "isSwitching" }] : []));
1067
- canSwitch = computed(() => {
1068
- const selectedCompany = this.selectedCompanyId();
854
+ // Subscribe to menu reset
855
+ this.layoutService.resetSource$
856
+ .pipe(takeUntilDestroyed(this.destroyRef))
857
+ .subscribe(() => this._active.set(false));
858
+ // Subscribe to navigation events
859
+ this.router.events
860
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
861
+ .subscribe(() => {
862
+ if (this.item().routerLink) {
863
+ this.updateActiveStateFromRoute();
864
+ }
865
+ });
866
+ // Effect to update active state on init when item has routerLink
867
+ effect(() => {
868
+ const menuItem = this.item();
869
+ if (menuItem.routerLink) {
870
+ this.updateActiveStateFromRoute();
871
+ }
872
+ });
873
+ // Effect to update submenu position when active in topbar mode
874
+ effect(() => {
875
+ if (this.active() && this.parentLink()) {
876
+ Promise.resolve().then(() => this.updateSubmenuPosition());
877
+ }
878
+ });
879
+ }
880
+ onMouseEnter() {
881
+ if (this.layoutService.isTopbar() && this.item().children) {
882
+ this.updateSubmenuPosition();
883
+ }
884
+ }
885
+ updateSubmenuPosition() {
886
+ const linkEl = this.parentLink()?.nativeElement;
887
+ if (!linkEl || typeof linkEl.getBoundingClientRect !== 'function')
888
+ return;
889
+ const rect = linkEl.getBoundingClientRect();
890
+ const hostEl = linkEl.closest('[app-menuitem]');
891
+ if (!hostEl)
892
+ return;
893
+ // Position submenu to the left of the parent link and above it
894
+ const left = rect.left;
895
+ const top = rect.top + 30; // 8px gap above the menu item
896
+ hostEl.style.setProperty('--submenu-left', `${left}px`);
897
+ hostEl.style.setProperty('--submenu-top', `${top}px`);
898
+ }
899
+ updateActiveStateFromRoute() {
900
+ const link = this.routerLink();
901
+ if (!link.length)
902
+ return;
903
+ // Use 'exact' for root path to avoid matching all routes
904
+ // Use 'subset' for all other paths to match child routes
905
+ const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
906
+ const active = isActive(link[0], this.router, {
907
+ paths: pathsMatch,
908
+ queryParams: 'ignored',
909
+ matrixParams: 'ignored',
910
+ fragment: 'ignored',
911
+ })();
912
+ if (active) {
913
+ this.layoutService.onMenuStateChange({
914
+ key: this.key(),
915
+ routeEvent: true,
916
+ });
917
+ }
918
+ }
919
+ itemClick() {
920
+ if (this.item().children && !this.layoutService.isTopbar()) {
921
+ this._active.update((prev) => !prev);
922
+ }
923
+ this.layoutService.onMenuStateChange({ key: this.key() });
924
+ }
925
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenuitem, deps: [], target: i0.ɵɵFactoryTarget.Component });
926
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppMenuitem, isStandalone: true, selector: "[app-menuitem]", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, parentKey: { classPropertyName: "parentKey", publicName: "parentKey", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.active-menuitem": "active()" } }, viewQueries: [{ propertyName: "parentLink", first: true, predicate: ["parentLink"], descendants: true, isSignal: true }], ngImport: i0, template: `
927
+ <ng-container>
928
+ @if (item().children?.length) {
929
+ <a
930
+ (click)="itemClick()"
931
+ (mouseenter)="onMouseEnter()"
932
+ #parentLink
933
+ tabindex="0"
934
+ >
935
+ @if (item().icon) {
936
+ <lib-icon
937
+ [icon]="item().icon!"
938
+ [iconType]="item().iconType"
939
+ class="layout-menuitem-icon"
940
+ />
941
+ }
942
+ <span class="layout-menuitem-text">{{
943
+ item().labelKey ? (item().labelKey! | translate) : item().label
944
+ }}</span>
945
+ @if (item().children) {
946
+ @if (layoutService.isTopbar()) {
947
+ <ui-icon name="chevron-right" class="layout-submenu-toggler"></ui-icon>
948
+ } @else {
949
+ <ui-icon name="angle-down" class="layout-submenu-toggler"></ui-icon>
950
+ }
951
+ }
952
+ </a>
953
+ }
954
+ @if (item().routerLink && !item().children?.length) {
955
+ <a
956
+ (click)="itemClick()"
957
+ [routerLink]="routerLink()"
958
+ routerLinkActive="active-route"
959
+ [routerLinkActiveOptions]="routerLinkActiveOptions()"
960
+ tabindex="0"
961
+ >
962
+ @if (item().icon) {
963
+ <lib-icon
964
+ [icon]="item().icon!"
965
+ [iconType]="item().iconType"
966
+ class="layout-menuitem-icon"
967
+ />
968
+ }
969
+ <span class="layout-menuitem-text">{{
970
+ item().labelKey ? (item().labelKey! | translate) : item().label
971
+ }}</span>
972
+ </a>
973
+ }
974
+ @if (item().children) {
975
+ <ul
976
+ (mouseenter)="onMouseEnter()"
977
+ [class.submenu-expanded]="active() && !layoutService.isTopbar()"
978
+ [class.submenu-collapsed]="!active() && !layoutService.isTopbar()"
979
+ >
980
+ @for (
981
+ child of item().children;
982
+ track 'menu' + child.id + '' + i;
983
+ let i = $index
984
+ ) {
985
+ <li
986
+ app-menuitem
987
+ [item]="child"
988
+ [index]="i"
989
+ [parentKey]="key()"
990
+ ></li>
991
+ }
992
+ </ul>
993
+ }
994
+ </ng-container>
995
+ `, isInline: true, styles: [":host ul{overflow:hidden;transition:max-height .4s cubic-bezier(.86,0,.07,1)}:host ul.submenu-collapsed{max-height:0}:host ul.submenu-expanded{max-height:1000px}\n"], dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "ngmodule", type: UiModule }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "component", type: IconComponent, selector: "lib-icon", inputs: ["icon", "iconType"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2$1.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
996
+ }
997
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenuitem, decorators: [{
998
+ type: Component,
999
+ args: [{ selector: '[app-menuitem]', imports: [UiModule, IconComponent, RouterModule, TranslatePipe], template: `
1000
+ <ng-container>
1001
+ @if (item().children?.length) {
1002
+ <a
1003
+ (click)="itemClick()"
1004
+ (mouseenter)="onMouseEnter()"
1005
+ #parentLink
1006
+ tabindex="0"
1007
+ >
1008
+ @if (item().icon) {
1009
+ <lib-icon
1010
+ [icon]="item().icon!"
1011
+ [iconType]="item().iconType"
1012
+ class="layout-menuitem-icon"
1013
+ />
1014
+ }
1015
+ <span class="layout-menuitem-text">{{
1016
+ item().labelKey ? (item().labelKey! | translate) : item().label
1017
+ }}</span>
1018
+ @if (item().children) {
1019
+ @if (layoutService.isTopbar()) {
1020
+ <ui-icon name="chevron-right" class="layout-submenu-toggler"></ui-icon>
1021
+ } @else {
1022
+ <ui-icon name="angle-down" class="layout-submenu-toggler"></ui-icon>
1023
+ }
1024
+ }
1025
+ </a>
1026
+ }
1027
+ @if (item().routerLink && !item().children?.length) {
1028
+ <a
1029
+ (click)="itemClick()"
1030
+ [routerLink]="routerLink()"
1031
+ routerLinkActive="active-route"
1032
+ [routerLinkActiveOptions]="routerLinkActiveOptions()"
1033
+ tabindex="0"
1034
+ >
1035
+ @if (item().icon) {
1036
+ <lib-icon
1037
+ [icon]="item().icon!"
1038
+ [iconType]="item().iconType"
1039
+ class="layout-menuitem-icon"
1040
+ />
1041
+ }
1042
+ <span class="layout-menuitem-text">{{
1043
+ item().labelKey ? (item().labelKey! | translate) : item().label
1044
+ }}</span>
1045
+ </a>
1046
+ }
1047
+ @if (item().children) {
1048
+ <ul
1049
+ (mouseenter)="onMouseEnter()"
1050
+ [class.submenu-expanded]="active() && !layoutService.isTopbar()"
1051
+ [class.submenu-collapsed]="!active() && !layoutService.isTopbar()"
1052
+ >
1053
+ @for (
1054
+ child of item().children;
1055
+ track 'menu' + child.id + '' + i;
1056
+ let i = $index
1057
+ ) {
1058
+ <li
1059
+ app-menuitem
1060
+ [item]="child"
1061
+ [index]="i"
1062
+ [parentKey]="key()"
1063
+ ></li>
1064
+ }
1065
+ </ul>
1066
+ }
1067
+ </ng-container>
1068
+ `, host: {
1069
+ '[class.active-menuitem]': 'active()',
1070
+ }, styles: [":host ul{overflow:hidden;transition:max-height .4s cubic-bezier(.86,0,.07,1)}:host ul.submenu-collapsed{max-height:0}:host ul.submenu-expanded{max-height:1000px}\n"] }]
1071
+ }], ctorParameters: () => [], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }], parentKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentKey", required: false }] }], parentLink: [{ type: i0.ViewChild, args: ['parentLink', { isSignal: true }] }] } });
1072
+
1073
+ /**
1074
+ * Main menu component that displays filtered menu items.
1075
+ * Menu is automatically filtered based on user permissions using the permission checker (logicNode pattern).
1076
+ * The filtering happens internally in LayoutService via computed signal.
1077
+ */
1078
+ class AppMenu {
1079
+ layoutService = inject(LayoutService);
1080
+ /**
1081
+ * Filtered menu items from layout service.
1082
+ * This signal is computed in LayoutService and automatically updates when:
1083
+ * - Raw menu changes (setMenu called)
1084
+ * - Permission state changes (user permissions updated)
1085
+ */
1086
+ menuItems = this.layoutService.menu;
1087
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1088
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppMenu, isStandalone: true, selector: "app-menu", ngImport: i0, template: `<div class="layout-menu">
1089
+ <ul>
1090
+ @for (
1091
+ item of menuItems();
1092
+ track 'menu' + item.id + '' + i;
1093
+ let i = $index
1094
+ ) {
1095
+ <li app-menuitem [item]="item" [index]="i"></li>
1096
+ }
1097
+ </ul>
1098
+ </div>`, isInline: true, dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "ngmodule", type: RouterModule }] });
1099
+ }
1100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenu, decorators: [{
1101
+ type: Component,
1102
+ args: [{
1103
+ selector: 'app-menu',
1104
+ imports: [AppMenuitem, RouterModule],
1105
+ template: `<div class="layout-menu">
1106
+ <ul>
1107
+ @for (
1108
+ item of menuItems();
1109
+ track 'menu' + item.id + '' + i;
1110
+ let i = $index
1111
+ ) {
1112
+ <li app-menuitem [item]="item" [index]="i"></li>
1113
+ }
1114
+ </ul>
1115
+ </div>`,
1116
+ }]
1117
+ }] });
1118
+
1119
+ /**
1120
+ * View Transitions API type definitions
1121
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
1122
+ */
1123
+
1124
+ /** Company/branch switcher displayed in top bar */
1125
+ class AppCompanyBranchSelector {
1126
+ destroyRef = inject(DestroyRef);
1127
+ authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1128
+ authApi = inject(LAYOUT_AUTH_API, { optional: true });
1129
+ messageService = inject(MessageService);
1130
+ translateAdapter = inject(TranslateService, {
1131
+ optional: true,
1132
+ });
1133
+ document = inject(DOCUMENT$1);
1134
+ elementRef = inject(ElementRef);
1135
+ _isActive = signal(false, /* @ts-ignore */
1136
+ ...(ngDevMode ? [{ debugName: "_isActive" }] : /* istanbul ignore next */ []));
1137
+ isActive = this._isActive.asReadonly();
1138
+ constructor() {
1139
+ fromEvent(this.document, 'click')
1140
+ .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1141
+ .subscribe((event) => {
1142
+ const target = event.target;
1143
+ if (!this.elementRef.nativeElement.contains(target)) {
1144
+ this._isActive.set(false);
1145
+ }
1146
+ });
1147
+ }
1148
+ currentCompanyName = computed(() => this.authState?.currentCompanyInfo()?.name ??
1149
+ this.translate('layout.company.branch.selector.no.company'), /* @ts-ignore */
1150
+ ...(ngDevMode ? [{ debugName: "currentCompanyName" }] : /* istanbul ignore next */ []));
1151
+ currentBranchName = computed(() => this.authState?.currentBranchInfo()?.name ?? null, /* @ts-ignore */
1152
+ ...(ngDevMode ? [{ debugName: "currentBranchName" }] : /* istanbul ignore next */ []));
1153
+ _companies = signal([], /* @ts-ignore */
1154
+ ...(ngDevMode ? [{ debugName: "_companies" }] : /* istanbul ignore next */ []));
1155
+ companies = this._companies.asReadonly();
1156
+ _branches = signal([], /* @ts-ignore */
1157
+ ...(ngDevMode ? [{ debugName: "_branches" }] : /* istanbul ignore next */ []));
1158
+ branches = this._branches.asReadonly();
1159
+ _selectedCompanyId = signal(null, /* @ts-ignore */
1160
+ ...(ngDevMode ? [{ debugName: "_selectedCompanyId" }] : /* istanbul ignore next */ []));
1161
+ selectedCompanyId = this._selectedCompanyId.asReadonly();
1162
+ _selectedBranchId = signal(null, /* @ts-ignore */
1163
+ ...(ngDevMode ? [{ debugName: "_selectedBranchId" }] : /* istanbul ignore next */ []));
1164
+ selectedBranchId = this._selectedBranchId.asReadonly();
1165
+ _isLoadingCompanies = signal(false, /* @ts-ignore */
1166
+ ...(ngDevMode ? [{ debugName: "_isLoadingCompanies" }] : /* istanbul ignore next */ []));
1167
+ isLoadingCompanies = this._isLoadingCompanies.asReadonly();
1168
+ _isLoadingBranches = signal(false, /* @ts-ignore */
1169
+ ...(ngDevMode ? [{ debugName: "_isLoadingBranches" }] : /* istanbul ignore next */ []));
1170
+ isLoadingBranches = this._isLoadingBranches.asReadonly();
1171
+ _isSwitching = signal(false, /* @ts-ignore */
1172
+ ...(ngDevMode ? [{ debugName: "_isSwitching" }] : /* istanbul ignore next */ []));
1173
+ isSwitching = this._isSwitching.asReadonly();
1174
+ canSwitch = computed(() => {
1175
+ const selectedCompany = this.selectedCompanyId();
1069
1176
  if (!selectedCompany)
1070
1177
  return false;
1071
1178
  const currentCompanyId = this.authState?.currentCompanyInfo()?.id;
1072
1179
  const currentBranchId = this.authState?.currentBranchInfo()?.id;
1073
1180
  const selectedBranch = this.selectedBranchId();
1074
- return selectedCompany !== currentCompanyId || selectedBranch !== currentBranchId;
1075
- }, ...(ngDevMode ? [{ debugName: "canSwitch" }] : []));
1181
+ return (selectedCompany !== currentCompanyId || selectedBranch !== currentBranchId);
1182
+ }, /* @ts-ignore */
1183
+ ...(ngDevMode ? [{ debugName: "canSwitch" }] : /* istanbul ignore next */ []));
1076
1184
  onPanelToggle() {
1077
- this.isActive.update((v) => !v);
1185
+ this._isActive.update((v) => !v);
1078
1186
  if (this.isActive() && this.companies().length === 0) {
1079
1187
  this.loadCompanies();
1080
1188
  }
@@ -1082,61 +1190,53 @@ class AppCompanyBranchSelector {
1082
1190
  loadCompanies() {
1083
1191
  if (!this.authApi)
1084
1192
  return;
1085
- this.isLoadingCompanies.set(true);
1193
+ this._isLoadingCompanies.set(true);
1086
1194
  this.authApi
1087
1195
  .getUserCompanies()
1088
1196
  .pipe(takeUntilDestroyed(this.destroyRef))
1089
1197
  .subscribe({
1090
1198
  next: (companies) => {
1091
- this.companies.set(companies);
1092
- this.isLoadingCompanies.set(false);
1199
+ this._companies.set(companies);
1200
+ this._isLoadingCompanies.set(false);
1093
1201
  },
1094
- error: (err) => {
1095
- this.isLoadingCompanies.set(false);
1096
- this.messageService.add({
1097
- severity: 'error',
1098
- summary: 'Error',
1099
- detail: err?.message || 'Failed to load companies',
1100
- });
1202
+ error: () => {
1203
+ // Error toast handled by global interceptor
1204
+ this._isLoadingCompanies.set(false);
1101
1205
  },
1102
1206
  });
1103
1207
  }
1104
1208
  onCompanyChange(companyId) {
1105
- this.selectedCompanyId.set(companyId);
1209
+ this._selectedCompanyId.set(companyId);
1106
1210
  if (!companyId) {
1107
- this.branches.set([]);
1108
- this.selectedBranchId.set(null);
1211
+ this._branches.set([]);
1212
+ this._selectedBranchId.set(null);
1109
1213
  return;
1110
1214
  }
1111
1215
  this.loadBranches(companyId);
1112
1216
  }
1113
1217
  onBranchChange(branchId) {
1114
- this.selectedBranchId.set(branchId);
1218
+ this._selectedBranchId.set(branchId);
1115
1219
  }
1116
1220
  loadBranches(companyId) {
1117
1221
  if (!this.authApi)
1118
1222
  return;
1119
- this.isLoadingBranches.set(true);
1120
- this.selectedBranchId.set(null);
1223
+ this._isLoadingBranches.set(true);
1224
+ this._selectedBranchId.set(null);
1121
1225
  this.authApi
1122
1226
  .getCompanyBranches(companyId)
1123
1227
  .pipe(takeUntilDestroyed(this.destroyRef))
1124
1228
  .subscribe({
1125
1229
  next: (branches) => {
1126
- this.branches.set(branches);
1127
- this.isLoadingBranches.set(false);
1230
+ this._branches.set(branches);
1231
+ this._isLoadingBranches.set(false);
1128
1232
  // Auto-select if only one branch available
1129
1233
  if (branches.length === 1) {
1130
- this.selectedBranchId.set(branches[0].id);
1234
+ this._selectedBranchId.set(branches[0].id);
1131
1235
  }
1132
1236
  },
1133
- error: (err) => {
1134
- this.isLoadingBranches.set(false);
1135
- this.messageService.add({
1136
- severity: 'error',
1137
- summary: 'Error',
1138
- detail: err?.message || 'Failed to load branches',
1139
- });
1237
+ error: () => {
1238
+ // Error toast handled by global interceptor
1239
+ this._isLoadingBranches.set(false);
1140
1240
  },
1141
1241
  });
1142
1242
  }
@@ -1148,34 +1248,33 @@ class AppCompanyBranchSelector {
1148
1248
  if (this.branches().length > 0 && !selectedBranch) {
1149
1249
  this.messageService.add({
1150
1250
  severity: 'warn',
1151
- summary: 'Branch Required',
1152
- detail: 'Please select a branch',
1251
+ summary: this.translate('layout.company.branch.selector.branch.required'),
1252
+ detail: this.translate('layout.company.branch.selector.please.select.branch'),
1153
1253
  });
1154
1254
  return;
1155
1255
  }
1156
- this.isSwitching.set(true);
1256
+ this._isSwitching.set(true);
1157
1257
  this.authApi
1158
1258
  .switchCompany(selectedCompany, selectedBranch || '')
1159
1259
  .pipe(takeUntilDestroyed(this.destroyRef))
1160
1260
  .subscribe({
1161
1261
  next: () => { },
1162
- error: (err) => {
1163
- this.isSwitching.set(false);
1164
- this.messageService.add({
1165
- severity: 'error',
1166
- summary: 'Error',
1167
- detail: err?.message || 'Failed to switch company',
1168
- });
1262
+ error: () => {
1263
+ // Error toast handled by global interceptor
1264
+ this._isSwitching.set(false);
1169
1265
  },
1170
1266
  });
1171
1267
  }
1172
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppCompanyBranchSelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
1173
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AppCompanyBranchSelector, isStandalone: true, selector: "app-company-branch-selector", host: { classAttribute: "relative" }, ngImport: i0, template: `
1268
+ translate(key) {
1269
+ return this.translateAdapter?.translate(key) ?? key;
1270
+ }
1271
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppCompanyBranchSelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
1272
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppCompanyBranchSelector, isStandalone: true, selector: "app-company-branch-selector", host: { classAttribute: "relative" }, ngImport: i0, template: `
1174
1273
  <button
1175
1274
  type="button"
1176
1275
  class="layout-topbar-action"
1177
1276
  [class.layout-topbar-action-highlight]="isActive()"
1178
- pStyleClass="@next"
1277
+ fStyleClass="@next"
1179
1278
  enterFromClass="hidden"
1180
1279
  enterActiveClass="animate-scalein"
1181
1280
  leaveToClass="hidden"
@@ -1183,31 +1282,35 @@ class AppCompanyBranchSelector {
1183
1282
  [hideOnOutsideClick]="true"
1184
1283
  (click)="onPanelToggle()"
1185
1284
  >
1186
- <i class="pi pi-building"></i>
1285
+ <ui-icon name="building"></ui-icon>
1187
1286
  <span>{{ currentCompanyName() }}</span>
1188
1287
  @if (currentBranchName()) {
1189
- <span class="text-xs opacity-70"> | {{ currentBranchName() }}</span>
1288
+ <span class="text-xs opacity-70 ml-1">| {{ currentBranchName() }}</span>
1190
1289
  }
1191
1290
  </button>
1192
1291
  <div
1193
- class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1194
- style="background-color: var(--surface-overlay)"
1292
+ class="hidden absolute top-[3.25rem] z-50 p-4 border border-surface rounded-border origin-top shadow-xl w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[300px] max-w-[360px]"
1293
+ style="background-color: var(--surface-overlay); inset-inline-end: 0;"
1195
1294
  >
1196
- <div class="flex flex-col gap-4 min-w-[280px]">
1197
- <span class="text-sm text-muted-color font-semibold"
1198
- >Switch Company & Branch</span
1199
- >
1295
+ <div class="flex flex-col gap-4">
1296
+ <span class="text-sm text-muted-color font-semibold">{{
1297
+ 'layout.company.branch.selector.title' | translate
1298
+ }}</span>
1200
1299
 
1201
1300
  <!-- Company Selector -->
1202
1301
  <div class="flex flex-col gap-2">
1203
- <label class="text-sm text-muted-color font-semibold">Company</label>
1204
- <p-select
1302
+ <label class="text-sm text-muted-color font-semibold">{{
1303
+ 'shared.company' | translate
1304
+ }}</label>
1305
+ <f-select
1205
1306
  [options]="companies()"
1206
1307
  [ngModel]="selectedCompanyId()"
1207
1308
  (ngModelChange)="onCompanyChange($event)"
1208
1309
  optionLabel="name"
1209
1310
  optionValue="id"
1210
- placeholder="Select Company"
1311
+ [placeholder]="
1312
+ 'layout.company.branch.selector.select.company' | translate
1313
+ "
1211
1314
  class="w-full"
1212
1315
  [loading]="isLoadingCompanies()"
1213
1316
  />
@@ -1216,14 +1319,18 @@ class AppCompanyBranchSelector {
1216
1319
  <!-- Branch Selector -->
1217
1320
  @if (selectedCompanyId() && branches().length > 0) {
1218
1321
  <div class="flex flex-col gap-2">
1219
- <label class="text-sm text-muted-color font-semibold">Branch</label>
1220
- <p-select
1322
+ <label class="text-sm text-muted-color font-semibold">{{
1323
+ 'layout.company.branch.selector.branch' | translate
1324
+ }}</label>
1325
+ <f-select
1221
1326
  [options]="branches()"
1222
1327
  [ngModel]="selectedBranchId()"
1223
1328
  (ngModelChange)="onBranchChange($event)"
1224
1329
  optionLabel="name"
1225
1330
  optionValue="id"
1226
- placeholder="Select Branch"
1331
+ [placeholder]="
1332
+ 'layout.company.branch.selector.select.branch' | translate
1333
+ "
1227
1334
  class="w-full"
1228
1335
  [loading]="isLoadingBranches()"
1229
1336
  />
@@ -1232,32 +1339,37 @@ class AppCompanyBranchSelector {
1232
1339
 
1233
1340
  <!-- Actions -->
1234
1341
  <div class="flex gap-2 pt-2">
1235
- <p-button
1236
- label="Switch"
1237
- icon="pi pi-sync"
1238
- styleClass="flex-1"
1239
- [loading]="isSwitching()"
1240
- [disabled]="!canSwitch()"
1241
- (onClick)="onSwitch()"
1242
- />
1342
+ <button
1343
+ fButton
1344
+ class="flex-1"
1345
+ [disabled]="!canSwitch() || isSwitching()"
1346
+ (click)="onSwitch()"
1347
+ >
1348
+ @if (isSwitching()) {
1349
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
1350
+ } @else {
1351
+ <ui-icon pButtonIcon name="sync"></ui-icon>
1352
+ }
1353
+ <span pButtonLabel>{{ 'layout.company.branch.selector.switch.button' | translate }}</span>
1354
+ </button>
1243
1355
  </div>
1244
1356
  </div>
1245
1357
  </div>
1246
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i1$1.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i4.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }] });
1358
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "directive", type: i1.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i1.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i1.StyleClassDirective, selector: "[fStyleClass]", inputs: ["fStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass"] }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1247
1359
  }
1248
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppCompanyBranchSelector, decorators: [{
1360
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppCompanyBranchSelector, decorators: [{
1249
1361
  type: Component,
1250
1362
  args: [{
1251
1363
  selector: 'app-company-branch-selector',
1252
- standalone: true,
1253
- imports: [AngularModule, StyleClassModule, ButtonModule, SelectModule],
1364
+ imports: [UiModule, AngularModule,
1365
+ TranslatePipe,],
1254
1366
  host: { class: 'relative' },
1255
1367
  template: `
1256
1368
  <button
1257
1369
  type="button"
1258
1370
  class="layout-topbar-action"
1259
1371
  [class.layout-topbar-action-highlight]="isActive()"
1260
- pStyleClass="@next"
1372
+ fStyleClass="@next"
1261
1373
  enterFromClass="hidden"
1262
1374
  enterActiveClass="animate-scalein"
1263
1375
  leaveToClass="hidden"
@@ -1265,31 +1377,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1265
1377
  [hideOnOutsideClick]="true"
1266
1378
  (click)="onPanelToggle()"
1267
1379
  >
1268
- <i class="pi pi-building"></i>
1380
+ <ui-icon name="building"></ui-icon>
1269
1381
  <span>{{ currentCompanyName() }}</span>
1270
1382
  @if (currentBranchName()) {
1271
- <span class="text-xs opacity-70"> | {{ currentBranchName() }}</span>
1383
+ <span class="text-xs opacity-70 ml-1">| {{ currentBranchName() }}</span>
1272
1384
  }
1273
1385
  </button>
1274
1386
  <div
1275
- class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1276
- style="background-color: var(--surface-overlay)"
1387
+ class="hidden absolute top-[3.25rem] z-50 p-4 border border-surface rounded-border origin-top shadow-xl w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[300px] max-w-[360px]"
1388
+ style="background-color: var(--surface-overlay); inset-inline-end: 0;"
1277
1389
  >
1278
- <div class="flex flex-col gap-4 min-w-[280px]">
1279
- <span class="text-sm text-muted-color font-semibold"
1280
- >Switch Company & Branch</span
1281
- >
1390
+ <div class="flex flex-col gap-4">
1391
+ <span class="text-sm text-muted-color font-semibold">{{
1392
+ 'layout.company.branch.selector.title' | translate
1393
+ }}</span>
1282
1394
 
1283
1395
  <!-- Company Selector -->
1284
1396
  <div class="flex flex-col gap-2">
1285
- <label class="text-sm text-muted-color font-semibold">Company</label>
1286
- <p-select
1397
+ <label class="text-sm text-muted-color font-semibold">{{
1398
+ 'shared.company' | translate
1399
+ }}</label>
1400
+ <f-select
1287
1401
  [options]="companies()"
1288
1402
  [ngModel]="selectedCompanyId()"
1289
1403
  (ngModelChange)="onCompanyChange($event)"
1290
1404
  optionLabel="name"
1291
1405
  optionValue="id"
1292
- placeholder="Select Company"
1406
+ [placeholder]="
1407
+ 'layout.company.branch.selector.select.company' | translate
1408
+ "
1293
1409
  class="w-full"
1294
1410
  [loading]="isLoadingCompanies()"
1295
1411
  />
@@ -1298,14 +1414,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1298
1414
  <!-- Branch Selector -->
1299
1415
  @if (selectedCompanyId() && branches().length > 0) {
1300
1416
  <div class="flex flex-col gap-2">
1301
- <label class="text-sm text-muted-color font-semibold">Branch</label>
1302
- <p-select
1417
+ <label class="text-sm text-muted-color font-semibold">{{
1418
+ 'layout.company.branch.selector.branch' | translate
1419
+ }}</label>
1420
+ <f-select
1303
1421
  [options]="branches()"
1304
1422
  [ngModel]="selectedBranchId()"
1305
1423
  (ngModelChange)="onBranchChange($event)"
1306
1424
  optionLabel="name"
1307
1425
  optionValue="id"
1308
- placeholder="Select Branch"
1426
+ [placeholder]="
1427
+ 'layout.company.branch.selector.select.branch' | translate
1428
+ "
1309
1429
  class="w-full"
1310
1430
  [loading]="isLoadingBranches()"
1311
1431
  />
@@ -1314,14 +1434,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1314
1434
 
1315
1435
  <!-- Actions -->
1316
1436
  <div class="flex gap-2 pt-2">
1317
- <p-button
1318
- label="Switch"
1319
- icon="pi pi-sync"
1320
- styleClass="flex-1"
1321
- [loading]="isSwitching()"
1322
- [disabled]="!canSwitch()"
1323
- (onClick)="onSwitch()"
1324
- />
1437
+ <button
1438
+ fButton
1439
+ class="flex-1"
1440
+ [disabled]="!canSwitch() || isSwitching()"
1441
+ (click)="onSwitch()"
1442
+ >
1443
+ @if (isSwitching()) {
1444
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
1445
+ } @else {
1446
+ <ui-icon pButtonIcon name="sync"></ui-icon>
1447
+ }
1448
+ <span pButtonLabel>{{ 'layout.company.branch.selector.switch.button' | translate }}</span>
1449
+ </button>
1325
1450
  </div>
1326
1451
  </div>
1327
1452
  </div>
@@ -1330,129 +1455,146 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1330
1455
  }], ctorParameters: () => [] });
1331
1456
 
1332
1457
  class AppLauncher {
1458
+ layoutService = inject(LayoutService);
1333
1459
  destroyRef = inject(DestroyRef);
1334
1460
  document = inject(DOCUMENT$1);
1335
1461
  elementRef = inject(ElementRef);
1336
- layoutService = inject(LayoutService);
1337
- isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
1462
+ _isActive = signal(false, /* @ts-ignore */
1463
+ ...(ngDevMode ? [{ debugName: "_isActive" }] : /* istanbul ignore next */ []));
1464
+ isActive = this._isActive.asReadonly();
1338
1465
  constructor() {
1339
1466
  fromEvent(this.document, 'click')
1340
1467
  .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1341
1468
  .subscribe((event) => {
1342
1469
  const target = event.target;
1343
1470
  if (!this.elementRef.nativeElement.contains(target)) {
1344
- this.isActive.set(false);
1471
+ this._isActive.set(false);
1345
1472
  }
1346
1473
  });
1347
1474
  }
1348
- togglePanel() {
1349
- this.isActive.update((v) => !v);
1475
+ toggleActive() {
1476
+ this._isActive.update((v) => !v);
1350
1477
  }
1351
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppLauncher, deps: [], target: i0.ɵɵFactoryTarget.Component });
1352
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AppLauncher, isStandalone: true, selector: "app-launcher", host: { classAttribute: "relative" }, ngImport: i0, template: `
1478
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLauncher, deps: [], target: i0.ɵɵFactoryTarget.Component });
1479
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppLauncher, isStandalone: true, selector: "app-launcher", host: { classAttribute: "relative" }, ngImport: i0, template: `
1353
1480
  <button
1354
1481
  type="button"
1355
1482
  class="layout-topbar-action"
1356
1483
  [class.layout-topbar-action-highlight]="isActive()"
1357
- pStyleClass="@next"
1484
+ fStyleClass="@next"
1358
1485
  enterFromClass="hidden"
1359
1486
  enterActiveClass="animate-scalein"
1360
1487
  leaveToClass="hidden"
1361
1488
  leaveActiveClass="animate-fadeout"
1362
1489
  [hideOnOutsideClick]="true"
1363
- (click)="togglePanel()"
1490
+ (click)="toggleActive()"
1364
1491
  >
1365
- <i class="pi pi-th-large"></i>
1366
- <span>Apps</span>
1492
+ <ui-icon name="th-large"></ui-icon>
1493
+ <span>{{ 'layout.launcher.apps' | translate }}</span>
1367
1494
  </button>
1368
1495
  <div
1369
- class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1370
- style="background-color: var(--surface-overlay)"
1496
+ class="hidden absolute top-[3.25rem] z-50 border border-surface rounded-border origin-top surface-overlay"
1497
+ style="inset-inline-end: 0; min-width: 16rem; max-width: 22rem; width: calc(100vw - 2rem); box-shadow: var(--f-shadow-xl);"
1371
1498
  >
1372
- <div class="flex flex-col gap-2 min-w-[240px]">
1373
- <span class="text-sm text-muted-color font-semibold">Applications</span>
1374
- <div class="grid grid-cols-3 gap-2">
1499
+ <div class="px-4 py-3 border-b border-surface">
1500
+ <span class="text-xs font-semibold tracking-wide text-muted-color">{{
1501
+ 'layout.launcher.applications' | translate
1502
+ }}</span>
1503
+ </div>
1504
+ @if (layoutService.apps().length === 0) {
1505
+ <div class="px-4 py-6 text-center text-muted-color text-sm">
1506
+ {{ 'layout.launcher.no.apps' | translate }}
1507
+ </div>
1508
+ } @else {
1509
+ <div class="p-3 grid grid-cols-3 gap-1">
1375
1510
  @for (app of layoutService.apps(); track app.id) {
1376
1511
  <a
1377
- class="group flex flex-col items-center w-20 p-2 rounded-border cursor-pointer no-underline transition-all duration-200 hover:bg-emphasis"
1512
+ class="group flex flex-col items-center gap-2 p-3 rounded-border cursor-pointer no-underline transition-colors duration-150 hover:bg-emphasis"
1378
1513
  [href]="app.url"
1379
1514
  target="_blank"
1380
1515
  rel="noopener noreferrer"
1381
1516
  >
1382
1517
  <div
1383
- class="flex items-center justify-center w-10 h-10 rounded-border bg-emphasis group-hover:bg-primary transition-all duration-200"
1518
+ class="flex items-center justify-center w-9 h-9 rounded-border bg-primary/10 group-hover:bg-primary transition-colors duration-150"
1384
1519
  >
1385
- <lib-icon
1386
- [icon]="app.icon"
1387
- [iconType]="app.iconType"
1388
- class="text-xl text-primary group-hover:text-primary-contrast transition-colors duration-200"
1389
- />
1520
+ <ui-icon
1521
+ [name]="app.icon"
1522
+ class="text-base text-primary group-hover:text-primary-contrast transition-colors duration-150"
1523
+ ></ui-icon>
1390
1524
  </div>
1391
1525
  <span
1392
- class="text-xs mt-2 text-center text-muted-color group-hover:text-color transition-colors duration-200"
1393
- >{{ app.name }}</span
1526
+ class="text-xs text-center text-muted-color group-hover:text-color transition-colors duration-150 truncate w-full text-center leading-tight"
1394
1527
  >
1528
+ {{ app.nameKey ? (app.nameKey | translate) : app.name }}
1529
+ </span>
1395
1530
  </a>
1396
1531
  }
1397
1532
  </div>
1398
- </div>
1533
+ }
1399
1534
  </div>
1400
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "component", type: IconComponent, selector: "lib-icon", inputs: ["icon", "iconType"] }] });
1535
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "directive", type: i1.StyleClassDirective, selector: "[fStyleClass]", inputs: ["fStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass"] }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1401
1536
  }
1402
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppLauncher, decorators: [{
1537
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLauncher, decorators: [{
1403
1538
  type: Component,
1404
1539
  args: [{
1405
1540
  selector: 'app-launcher',
1406
- standalone: true,
1407
- imports: [StyleClassModule, IconComponent],
1541
+ imports: [UiModule, TranslatePipe],
1408
1542
  host: { class: 'relative' },
1409
1543
  template: `
1410
1544
  <button
1411
1545
  type="button"
1412
1546
  class="layout-topbar-action"
1413
1547
  [class.layout-topbar-action-highlight]="isActive()"
1414
- pStyleClass="@next"
1548
+ fStyleClass="@next"
1415
1549
  enterFromClass="hidden"
1416
1550
  enterActiveClass="animate-scalein"
1417
1551
  leaveToClass="hidden"
1418
1552
  leaveActiveClass="animate-fadeout"
1419
1553
  [hideOnOutsideClick]="true"
1420
- (click)="togglePanel()"
1554
+ (click)="toggleActive()"
1421
1555
  >
1422
- <i class="pi pi-th-large"></i>
1423
- <span>Apps</span>
1556
+ <ui-icon name="th-large"></ui-icon>
1557
+ <span>{{ 'layout.launcher.apps' | translate }}</span>
1424
1558
  </button>
1425
1559
  <div
1426
- class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1427
- style="background-color: var(--surface-overlay)"
1560
+ class="hidden absolute top-[3.25rem] z-50 border border-surface rounded-border origin-top surface-overlay"
1561
+ style="inset-inline-end: 0; min-width: 16rem; max-width: 22rem; width: calc(100vw - 2rem); box-shadow: var(--f-shadow-xl);"
1428
1562
  >
1429
- <div class="flex flex-col gap-2 min-w-[240px]">
1430
- <span class="text-sm text-muted-color font-semibold">Applications</span>
1431
- <div class="grid grid-cols-3 gap-2">
1563
+ <div class="px-4 py-3 border-b border-surface">
1564
+ <span class="text-xs font-semibold tracking-wide text-muted-color">{{
1565
+ 'layout.launcher.applications' | translate
1566
+ }}</span>
1567
+ </div>
1568
+ @if (layoutService.apps().length === 0) {
1569
+ <div class="px-4 py-6 text-center text-muted-color text-sm">
1570
+ {{ 'layout.launcher.no.apps' | translate }}
1571
+ </div>
1572
+ } @else {
1573
+ <div class="p-3 grid grid-cols-3 gap-1">
1432
1574
  @for (app of layoutService.apps(); track app.id) {
1433
1575
  <a
1434
- class="group flex flex-col items-center w-20 p-2 rounded-border cursor-pointer no-underline transition-all duration-200 hover:bg-emphasis"
1576
+ class="group flex flex-col items-center gap-2 p-3 rounded-border cursor-pointer no-underline transition-colors duration-150 hover:bg-emphasis"
1435
1577
  [href]="app.url"
1436
1578
  target="_blank"
1437
1579
  rel="noopener noreferrer"
1438
1580
  >
1439
1581
  <div
1440
- class="flex items-center justify-center w-10 h-10 rounded-border bg-emphasis group-hover:bg-primary transition-all duration-200"
1582
+ class="flex items-center justify-center w-9 h-9 rounded-border bg-primary/10 group-hover:bg-primary transition-colors duration-150"
1441
1583
  >
1442
- <lib-icon
1443
- [icon]="app.icon"
1444
- [iconType]="app.iconType"
1445
- class="text-xl text-primary group-hover:text-primary-contrast transition-colors duration-200"
1446
- />
1584
+ <ui-icon
1585
+ [name]="app.icon"
1586
+ class="text-base text-primary group-hover:text-primary-contrast transition-colors duration-150"
1587
+ ></ui-icon>
1447
1588
  </div>
1448
1589
  <span
1449
- class="text-xs mt-2 text-center text-muted-color group-hover:text-color transition-colors duration-200"
1450
- >{{ app.name }}</span
1590
+ class="text-xs text-center text-muted-color group-hover:text-color transition-colors duration-150 truncate w-full text-center leading-tight"
1451
1591
  >
1592
+ {{ app.nameKey ? (app.nameKey | translate) : app.name }}
1593
+ </span>
1452
1594
  </a>
1453
1595
  }
1454
1596
  </div>
1455
- </div>
1597
+ }
1456
1598
  </div>
1457
1599
  `,
1458
1600
  }]
@@ -1464,26 +1606,37 @@ class AppProfile {
1464
1606
  authApi = inject(LAYOUT_AUTH_API, { optional: true });
1465
1607
  layoutService = inject(LayoutService);
1466
1608
  messageService = inject(MessageService);
1609
+ translateAdapter = inject(TranslateService, {
1610
+ optional: true,
1611
+ });
1612
+ appConfig = inject(APP_CONFIG, { optional: true });
1467
1613
  document = inject(DOCUMENT$1);
1468
1614
  elementRef = inject(ElementRef);
1469
- isActive = signal(false, ...(ngDevMode ? [{ debugName: "isActive" }] : []));
1615
+ _isActive = signal(false, /* @ts-ignore */
1616
+ ...(ngDevMode ? [{ debugName: "_isActive" }] : /* istanbul ignore next */ []));
1617
+ isActive = this._isActive.asReadonly();
1470
1618
  constructor() {
1471
1619
  fromEvent(this.document, 'click')
1472
1620
  .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1473
1621
  .subscribe((event) => {
1474
1622
  const target = event.target;
1475
1623
  if (!this.elementRef.nativeElement.contains(target)) {
1476
- this.isActive.set(false);
1624
+ this._isActive.set(false);
1477
1625
  }
1478
1626
  });
1479
1627
  }
1480
1628
  togglePanel() {
1481
- this.isActive.update((v) => !v);
1629
+ this._isActive.update((v) => !v);
1482
1630
  }
1483
- userName = computed(() => this.authState?.loginUserData()?.name ?? 'Guest', ...(ngDevMode ? [{ debugName: "userName" }] : []));
1484
- userEmail = computed(() => this.authState?.loginUserData()?.email ?? '', ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
1485
- // Use LayoutService for profile picture URL (synced by AuthLayoutSyncService)
1486
- profilePicture = computed(() => this.layoutService.userProfilePictureUrl() ?? '', ...(ngDevMode ? [{ debugName: "profilePicture" }] : []));
1631
+ userName = computed(() => this.authState?.loginUserData()?.name ??
1632
+ this.translate('layout.profile.guest'), /* @ts-ignore */
1633
+ ...(ngDevMode ? [{ debugName: "userName" }] : /* istanbul ignore next */ []));
1634
+ userEmail = computed(() => this.authState?.loginUserData()?.email ?? '', /* @ts-ignore */
1635
+ ...(ngDevMode ? [{ debugName: "userEmail" }] : /* istanbul ignore next */ []));
1636
+ profilePicture = computed(() => this.layoutService.userProfilePictureUrl() ?? '', /* @ts-ignore */
1637
+ ...(ngDevMode ? [{ debugName: "profilePicture" }] : /* istanbul ignore next */ []));
1638
+ signUpEnabled = computed(() => this.appConfig ? isSignUpEnabled(this.appConfig) : false, /* @ts-ignore */
1639
+ ...(ngDevMode ? [{ debugName: "signUpEnabled" }] : /* istanbul ignore next */ []));
1487
1640
  logout() {
1488
1641
  if (!this.authApi)
1489
1642
  return;
@@ -1492,19 +1645,18 @@ class AppProfile {
1492
1645
  .pipe(takeUntilDestroyed(this.destroyRef))
1493
1646
  .subscribe({
1494
1647
  next: (result) => {
1648
+ const r = result;
1495
1649
  this.messageService.add({
1496
1650
  severity: 'success',
1497
- summary: 'Success!',
1498
- detail: result.message,
1651
+ summary: this.translate('shared.success'),
1652
+ detail: r?.messageKey
1653
+ ? this.translate(r.messageKey, r.messageVariables)
1654
+ : (r?.message ?? ''),
1499
1655
  });
1500
1656
  this.authApi?.navigateLogin(false);
1501
1657
  },
1502
- error: (err) => {
1503
- this.messageService.add({
1504
- severity: 'error',
1505
- summary: 'Error',
1506
- detail: err?.message || 'Failed to logout',
1507
- });
1658
+ error: () => {
1659
+ // Error toast handled by global interceptor
1508
1660
  },
1509
1661
  });
1510
1662
  }
@@ -1516,25 +1668,28 @@ class AppProfile {
1516
1668
  .then(() => {
1517
1669
  this.messageService.add({
1518
1670
  severity: 'success',
1519
- summary: 'Link Copied',
1520
- detail: 'Sign Up Link copied.',
1671
+ summary: this.translate('layout.profile.link.copied'),
1672
+ detail: this.translate('layout.profile.sign.up.link.copied'),
1521
1673
  });
1522
1674
  })
1523
1675
  .catch(() => {
1524
1676
  this.messageService.add({
1525
1677
  severity: 'error',
1526
- summary: 'Sorry',
1527
- detail: 'Failed to copy signup link.',
1678
+ summary: this.translate('shared.error'),
1679
+ detail: this.translate('layout.profile.failed.copy.sign.up.link'),
1528
1680
  });
1529
1681
  });
1530
1682
  }
1531
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppProfile, deps: [], target: i0.ɵɵFactoryTarget.Component });
1532
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AppProfile, isStandalone: true, selector: "app-profile", host: { classAttribute: "relative" }, ngImport: i0, template: `
1683
+ translate(key, vars) {
1684
+ return this.translateAdapter?.translate(key, vars) ?? key;
1685
+ }
1686
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppProfile, deps: [], target: i0.ɵɵFactoryTarget.Component });
1687
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppProfile, isStandalone: true, selector: "app-profile", host: { classAttribute: "relative" }, ngImport: i0, template: `
1533
1688
  <button
1534
1689
  type="button"
1535
1690
  class="layout-topbar-action"
1536
1691
  [class.layout-topbar-action-highlight]="isActive()"
1537
- pStyleClass="@next"
1692
+ fStyleClass="@next"
1538
1693
  enterFromClass="hidden"
1539
1694
  enterActiveClass="animate-scalein"
1540
1695
  leaveToClass="hidden"
@@ -1542,14 +1697,14 @@ class AppProfile {
1542
1697
  [hideOnOutsideClick]="true"
1543
1698
  (click)="togglePanel()"
1544
1699
  >
1545
- <i class="pi pi-user"></i>
1546
- <span>Profile</span>
1700
+ <ui-icon name="user"></ui-icon>
1701
+ <span>{{ 'layout.profile.title' | translate }}</span>
1547
1702
  </button>
1548
1703
  <div
1549
- class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1550
- style="background-color: var(--surface-overlay)"
1704
+ class="hidden absolute top-[3.25rem] z-50 p-4 border border-surface rounded-border origin-top shadow-xl w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]"
1705
+ style="background-color: var(--surface-overlay); inset-inline-end: 0;"
1551
1706
  >
1552
- <div class="flex flex-col gap-3 min-w-[240px]">
1707
+ <div class="flex flex-col gap-3">
1553
1708
  <!-- User Info -->
1554
1709
  <a
1555
1710
  class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline hover:bg-emphasis transition-all duration-200"
@@ -1559,18 +1714,22 @@ class AppProfile {
1559
1714
  <img
1560
1715
  [src]="profilePicture()"
1561
1716
  class="w-12 h-12 rounded-border object-cover"
1562
- alt="Profile"
1717
+ [alt]="'layout.profile.profile.picture.alt' | translate"
1563
1718
  />
1564
1719
  } @else {
1565
1720
  <div
1566
1721
  class="w-12 h-12 rounded-border bg-primary flex items-center justify-center"
1567
1722
  >
1568
- <i class="pi pi-user text-xl text-primary-contrast"></i>
1723
+ <ui-icon name="user" class="text-xl text-primary-contrast"></ui-icon>
1569
1724
  </div>
1570
1725
  }
1571
- <div class="flex flex-col">
1572
- <span class="font-semibold text-color">{{ userName() }}</span>
1573
- <span class="text-sm text-muted-color">{{ userEmail() }}</span>
1726
+ <div class="flex flex-col min-w-0 flex-1">
1727
+ <span class="font-semibold text-color truncate">{{
1728
+ userName()
1729
+ }}</span>
1730
+ <span class="text-sm text-muted-color truncate">{{
1731
+ userEmail()
1732
+ }}</span>
1574
1733
  </div>
1575
1734
  </a>
1576
1735
 
@@ -1579,38 +1738,39 @@ class AppProfile {
1579
1738
 
1580
1739
  <!-- Menu Items -->
1581
1740
  <div class="flex flex-col gap-1">
1582
- <a
1583
- class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1584
- (click)="copySignUpLink()"
1585
- >
1586
- <i class="pi pi-link text-muted-color"></i>
1587
- <span>Copy SignUp Link</span>
1588
- </a>
1741
+ @if (signUpEnabled()) {
1742
+ <a
1743
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1744
+ (click)="copySignUpLink()"
1745
+ >
1746
+ <ui-icon name="link" class="text-muted-color"></ui-icon>
1747
+ <span>{{ 'layout.profile.copy.sign.up.link' | translate }}</span>
1748
+ </a>
1749
+ }
1589
1750
  <a
1590
1751
  class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1591
1752
  (click)="logout()"
1592
1753
  >
1593
- <i class="pi pi-sign-out text-muted-color"></i>
1594
- <span>Logout</span>
1754
+ <ui-icon name="sign-out" class="text-muted-color"></ui-icon>
1755
+ <span>{{ 'layout.profile.logout' | translate }}</span>
1595
1756
  </a>
1596
1757
  </div>
1597
1758
  </div>
1598
1759
  </div>
1599
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i2$2.IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }] });
1760
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "directive", type: i1.StyleClassDirective, selector: "[fStyleClass]", inputs: ["fStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass"] }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i3.IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1600
1761
  }
1601
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppProfile, decorators: [{
1762
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppProfile, decorators: [{
1602
1763
  type: Component,
1603
1764
  args: [{
1604
1765
  selector: 'app-profile',
1605
- standalone: true,
1606
- imports: [AngularModule, StyleClassModule],
1766
+ imports: [UiModule, AngularModule, TranslatePipe],
1607
1767
  host: { class: 'relative' },
1608
1768
  template: `
1609
1769
  <button
1610
1770
  type="button"
1611
1771
  class="layout-topbar-action"
1612
1772
  [class.layout-topbar-action-highlight]="isActive()"
1613
- pStyleClass="@next"
1773
+ fStyleClass="@next"
1614
1774
  enterFromClass="hidden"
1615
1775
  enterActiveClass="animate-scalein"
1616
1776
  leaveToClass="hidden"
@@ -1618,14 +1778,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1618
1778
  [hideOnOutsideClick]="true"
1619
1779
  (click)="togglePanel()"
1620
1780
  >
1621
- <i class="pi pi-user"></i>
1622
- <span>Profile</span>
1781
+ <ui-icon name="user"></ui-icon>
1782
+ <span>{{ 'layout.profile.title' | translate }}</span>
1623
1783
  </button>
1624
1784
  <div
1625
- class="hidden absolute top-[3.25rem] right-0 z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)]"
1626
- style="background-color: var(--surface-overlay)"
1785
+ class="hidden absolute top-[3.25rem] z-50 p-4 border border-surface rounded-border origin-top shadow-xl w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]"
1786
+ style="background-color: var(--surface-overlay); inset-inline-end: 0;"
1627
1787
  >
1628
- <div class="flex flex-col gap-3 min-w-[240px]">
1788
+ <div class="flex flex-col gap-3">
1629
1789
  <!-- User Info -->
1630
1790
  <a
1631
1791
  class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline hover:bg-emphasis transition-all duration-200"
@@ -1635,18 +1795,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1635
1795
  <img
1636
1796
  [src]="profilePicture()"
1637
1797
  class="w-12 h-12 rounded-border object-cover"
1638
- alt="Profile"
1798
+ [alt]="'layout.profile.profile.picture.alt' | translate"
1639
1799
  />
1640
1800
  } @else {
1641
1801
  <div
1642
1802
  class="w-12 h-12 rounded-border bg-primary flex items-center justify-center"
1643
1803
  >
1644
- <i class="pi pi-user text-xl text-primary-contrast"></i>
1804
+ <ui-icon name="user" class="text-xl text-primary-contrast"></ui-icon>
1645
1805
  </div>
1646
1806
  }
1647
- <div class="flex flex-col">
1648
- <span class="font-semibold text-color">{{ userName() }}</span>
1649
- <span class="text-sm text-muted-color">{{ userEmail() }}</span>
1807
+ <div class="flex flex-col min-w-0 flex-1">
1808
+ <span class="font-semibold text-color truncate">{{
1809
+ userName()
1810
+ }}</span>
1811
+ <span class="text-sm text-muted-color truncate">{{
1812
+ userEmail()
1813
+ }}</span>
1650
1814
  </div>
1651
1815
  </a>
1652
1816
 
@@ -1655,19 +1819,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1655
1819
 
1656
1820
  <!-- Menu Items -->
1657
1821
  <div class="flex flex-col gap-1">
1658
- <a
1659
- class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1660
- (click)="copySignUpLink()"
1661
- >
1662
- <i class="pi pi-link text-muted-color"></i>
1663
- <span>Copy SignUp Link</span>
1664
- </a>
1822
+ @if (signUpEnabled()) {
1823
+ <a
1824
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1825
+ (click)="copySignUpLink()"
1826
+ >
1827
+ <ui-icon name="link" class="text-muted-color"></ui-icon>
1828
+ <span>{{ 'layout.profile.copy.sign.up.link' | translate }}</span>
1829
+ </a>
1830
+ }
1665
1831
  <a
1666
1832
  class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1667
1833
  (click)="logout()"
1668
1834
  >
1669
- <i class="pi pi-sign-out text-muted-color"></i>
1670
- <span>Logout</span>
1835
+ <ui-icon name="sign-out" class="text-muted-color"></ui-icon>
1836
+ <span>{{ 'layout.profile.logout' | translate }}</span>
1671
1837
  </a>
1672
1838
  </div>
1673
1839
  </div>
@@ -1676,30 +1842,404 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1676
1842
  }]
1677
1843
  }], ctorParameters: () => [] });
1678
1844
 
1845
+ class AppSearchbar {
1846
+ layoutService = inject(LayoutService);
1847
+ router = inject(Router);
1848
+ searchAdapter = inject(LAYOUT_SEARCH_ADAPTER, {
1849
+ optional: true,
1850
+ });
1851
+ destroyRef = inject(DestroyRef);
1852
+ document = inject(DOCUMENT$1);
1853
+ searchTerm = signal('', /* @ts-ignore */
1854
+ ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
1855
+ suggestionsOpen = signal(false, /* @ts-ignore */
1856
+ ...(ngDevMode ? [{ debugName: "suggestionsOpen" }] : /* istanbul ignore next */ []));
1857
+ suggestions = signal([], /* @ts-ignore */
1858
+ ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
1859
+ focusedIndex = signal(-1, /* @ts-ignore */
1860
+ ...(ngDevMode ? [{ debugName: "focusedIndex" }] : /* istanbul ignore next */ []));
1861
+ isSearchExpanded = signal(false, /* @ts-ignore */
1862
+ ...(ngDevMode ? [{ debugName: "isSearchExpanded" }] : /* istanbul ignore next */ []));
1863
+ isResponsive = signal(typeof window !== 'undefined' ? window.innerWidth < 768 : false, /* @ts-ignore */
1864
+ ...(ngDevMode ? [{ debugName: "isResponsive" }] : /* istanbul ignore next */ []));
1865
+ searchWrapper = viewChild('searchWrapper', /* @ts-ignore */
1866
+ ...(ngDevMode ? [{ debugName: "searchWrapper" }] : /* istanbul ignore next */ []));
1867
+ searchInput = viewChild('searchInput', /* @ts-ignore */
1868
+ ...(ngDevMode ? [{ debugName: "searchInput" }] : /* istanbul ignore next */ []));
1869
+ searchTimeout;
1870
+ constructor() {
1871
+ // Track responsive breakpoint (mobile < 768px)
1872
+ if (typeof window !== 'undefined') {
1873
+ fromEvent(window, 'resize')
1874
+ .pipe(takeUntilDestroyed(this.destroyRef))
1875
+ .subscribe(() => {
1876
+ this.isResponsive.set(window.innerWidth < 768);
1877
+ });
1878
+ }
1879
+ // Close dropdown/search on outside click
1880
+ fromEvent(this.document, 'click')
1881
+ .pipe(takeUntilDestroyed(this.destroyRef))
1882
+ .subscribe((event) => {
1883
+ const wrapper = this.searchWrapper()?.nativeElement;
1884
+ const target = event.target;
1885
+ if (wrapper && !wrapper.contains(target)) {
1886
+ if (this.suggestionsOpen()) {
1887
+ this.closeSuggestions();
1888
+ }
1889
+ if (this.isSearchExpanded() &&
1890
+ (this.layoutService.isTopbar() || this.isResponsive())) {
1891
+ this.closeSearch();
1892
+ }
1893
+ }
1894
+ });
1895
+ // Debounced search for suggestions
1896
+ effect(() => {
1897
+ const query = this.searchTerm();
1898
+ clearTimeout(this.searchTimeout);
1899
+ if (!this.searchAdapter || !query.trim()) {
1900
+ this.suggestions.set([]);
1901
+ this.focusedIndex.set(-1);
1902
+ return;
1903
+ }
1904
+ this.searchTimeout = setTimeout(() => {
1905
+ this.searchAdapter.getSuggestions(query)
1906
+ .pipe(takeUntilDestroyed(this.destroyRef))
1907
+ .subscribe((results) => {
1908
+ this.suggestions.set(results);
1909
+ this.focusedIndex.set(-1);
1910
+ this.suggestionsOpen.set(results.length > 0);
1911
+ });
1912
+ }, 300);
1913
+ });
1914
+ }
1915
+ onFocus() {
1916
+ if (this.suggestions().length > 0) {
1917
+ this.suggestionsOpen.set(true);
1918
+ }
1919
+ }
1920
+ handleKeydown(event) {
1921
+ const items = this.suggestions();
1922
+ const current = this.focusedIndex();
1923
+ switch (event.key) {
1924
+ case 'ArrowDown':
1925
+ event.preventDefault();
1926
+ this.focusedIndex.set(current < items.length - 1 ? current + 1 : 0);
1927
+ this.suggestionsOpen.set(items.length > 0);
1928
+ break;
1929
+ case 'ArrowUp':
1930
+ event.preventDefault();
1931
+ this.focusedIndex.set(current > 0 ? current - 1 : items.length - 1);
1932
+ break;
1933
+ case 'Escape':
1934
+ event.preventDefault();
1935
+ this.closeSuggestions();
1936
+ break;
1937
+ }
1938
+ }
1939
+ handleEnter() {
1940
+ const current = this.focusedIndex();
1941
+ const items = this.suggestions();
1942
+ if (current >= 0 && current < items.length) {
1943
+ this.selectSuggestion(items[current]);
1944
+ }
1945
+ else if (items.length > 0) {
1946
+ this.selectSuggestion(items[0]);
1947
+ }
1948
+ else {
1949
+ this.onSearch();
1950
+ }
1951
+ }
1952
+ selectSuggestion(item) {
1953
+ if (item.routerLink) {
1954
+ this.router.navigate(item.routerLink);
1955
+ }
1956
+ this.searchTerm.set('');
1957
+ this.closeSuggestions();
1958
+ if (this.layoutService.isTopbar() || this.isResponsive()) {
1959
+ this.closeSearch();
1960
+ }
1961
+ }
1962
+ closeSuggestions() {
1963
+ this.suggestionsOpen.set(false);
1964
+ this.focusedIndex.set(-1);
1965
+ }
1966
+ toggleSearch() {
1967
+ this.isSearchExpanded.set(!this.isSearchExpanded());
1968
+ if (this.isSearchExpanded()) {
1969
+ setTimeout(() => {
1970
+ this.searchInput()?.nativeElement?.focus();
1971
+ }, 100);
1972
+ }
1973
+ }
1974
+ closeSearch() {
1975
+ this.isSearchExpanded.set(false);
1976
+ this.closeSuggestions();
1977
+ }
1978
+ onSearch() {
1979
+ const query = this.searchTerm();
1980
+ if (this.searchAdapter?.onSearch && query.trim()) {
1981
+ this.searchAdapter.onSearch(query);
1982
+ }
1983
+ }
1984
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSearchbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1985
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppSearchbar, isStandalone: true, selector: "app-searchbar", viewQueries: [{ propertyName: "searchWrapper", first: true, predicate: ["searchWrapper"], descendants: true, isSignal: true }, { propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
1986
+ <div
1987
+ class="search-container h-full"
1988
+ #searchWrapper
1989
+ [class.topbar-mode]="layoutService.isTopbar() || isResponsive()"
1990
+ >
1991
+ @if (layoutService.isTopbar() || isResponsive()) {
1992
+ <!-- Topbar/Mobile Icon Mode -->
1993
+ <button
1994
+ type="button"
1995
+ class="search-toggle-btn layout-topbar-action"
1996
+ (click)="toggleSearch()"
1997
+ [attr.aria-label]="'layout.topbar.search' | translate"
1998
+ >
1999
+ <ui-icon name="search"></ui-icon>
2000
+ </button>
2001
+
2002
+ @if (isSearchExpanded()) {
2003
+ <div class="search-dropdown-panel">
2004
+ <f-iconfield class="search-iconfield">
2005
+ <f-inputicon class="search-icon"><ui-icon name="search" /></f-inputicon>
2006
+ <input
2007
+ type="text"
2008
+ fInputText
2009
+ class="search-input w-full"
2010
+ [placeholder]="'layout.topbar.search.placeholder' | translate"
2011
+ [(ngModel)]="searchTerm"
2012
+ (keyup.enter)="handleEnter()"
2013
+ (keydown)="handleKeydown($event)"
2014
+ (focus)="onFocus()"
2015
+ [attr.aria-label]="'layout.topbar.search' | translate"
2016
+ #searchInput
2017
+ autocomplete="off"
2018
+ />
2019
+ </f-iconfield>
2020
+
2021
+ @if (suggestionsOpen() && suggestions().length > 0) {
2022
+ <div class="search-suggestions">
2023
+ <ul class="suggestions-list">
2024
+ @for (
2025
+ item of suggestions();
2026
+ track item.label;
2027
+ let idx = $index
2028
+ ) {
2029
+ <li
2030
+ class="suggestion-item"
2031
+ [class.focused]="idx === focusedIndex()"
2032
+ (click)="selectSuggestion(item)"
2033
+ (mouseenter)="focusedIndex.set(idx)"
2034
+ role="option"
2035
+ [attr.aria-selected]="idx === focusedIndex()"
2036
+ >
2037
+ @if (item.icon) {
2038
+ <ui-icon [name]="item.icon" class="suggestion-icon"></ui-icon>
2039
+ }
2040
+ <span class="suggestion-label">{{ item.label }}</span>
2041
+ </li>
2042
+ }
2043
+ </ul>
2044
+ </div>
2045
+ }
2046
+ </div>
2047
+ }
2048
+ } @else {
2049
+ <!-- Desktop Full Input Mode -->
2050
+ <f-iconfield class="search-iconfield">
2051
+ <f-inputicon class="search-icon cursor-pointer" (click)="onSearch()"><ui-icon name="search" /></f-inputicon>
2052
+ <input
2053
+ type="text"
2054
+ fInputText
2055
+ class="search-input w-full"
2056
+ [placeholder]="'layout.topbar.search.placeholder' | translate"
2057
+ [(ngModel)]="searchTerm"
2058
+ (keyup.enter)="handleEnter()"
2059
+ (keydown)="handleKeydown($event)"
2060
+ (focus)="onFocus()"
2061
+ [attr.aria-label]="'layout.topbar.search' | translate"
2062
+ #searchInput
2063
+ />
2064
+ </f-iconfield>
2065
+
2066
+ @if (suggestionsOpen() && suggestions().length > 0) {
2067
+ <div class="search-dropdown">
2068
+ <ul class="suggestions-list">
2069
+ @for (item of suggestions(); track item.label; let idx = $index) {
2070
+ <li
2071
+ class="suggestion-item"
2072
+ [class.focused]="idx === focusedIndex()"
2073
+ (click)="selectSuggestion(item)"
2074
+ (mouseenter)="focusedIndex.set(idx)"
2075
+ role="option"
2076
+ [attr.aria-selected]="idx === focusedIndex()"
2077
+ >
2078
+ @if (item.icon) {
2079
+ <ui-icon [name]="item.icon" class="suggestion-icon"></ui-icon>
2080
+ }
2081
+ <span class="suggestion-label">{{ item.label }}</span>
2082
+ </li>
2083
+ }
2084
+ </ul>
2085
+ </div>
2086
+ }
2087
+ }
2088
+ </div>
2089
+ `, isInline: true, styles: [".search-container{display:flex;align-items:center;position:relative;width:100%;flex:0 1 auto}.search-container.topbar-mode{width:auto}.search-dropdown-panel{position:absolute;top:100%;right:0;margin-top:.5rem;background-color:var(--surface-card);border:1px solid var(--surface-border);border-radius:var(--f-content-border-radius);box-shadow:var(--f-shadow-xl);z-index:1100;padding:1rem .75rem .75rem;min-width:300px;max-width:450px;animation:slideDown .2s ease-out}.search-suggestions{margin-top:.5rem;border-top:1px solid var(--surface-border);padding-top:.5rem;background-color:inherit}.suggestions-list{list-style:none;margin:0;padding:0;max-height:360px;overflow-y:auto;background-color:inherit}.suggestion-item{padding:.75rem 1rem;cursor:pointer;display:flex;align-items:center;gap:.75rem;color:var(--text-color);border-bottom:1px solid var(--surface-border);transition:background-color .15s ease,color .15s ease}.suggestion-item:last-child{border-bottom:none}.suggestion-item:hover,.suggestion-item.focused{background-color:var(--surface-50);color:var(--primary-color)}.suggestion-icon{color:var(--text-color-secondary);font-size:1rem;flex-shrink:0;transition:color .15s ease}.suggestion-item:hover .suggestion-icon,.suggestion-item.focused .suggestion-icon{color:var(--primary-color)}.suggestion-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.search-dropdown{position:absolute;top:calc(100% + .5rem);left:0;right:0;background-color:var(--surface-card);border:1px solid var(--surface-border);border-radius:var(--f-content-border-radius);box-shadow:var(--f-shadow-xl);z-index:1000;overflow:hidden}.search-dropdown .suggestions-list{background-color:inherit}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@media(max-width:768px){.search-dropdown-panel{min-width:280px;max-width:400px}}@media(max-width:576px){.search-dropdown-panel{position:fixed;top:4rem;left:0;right:0;bottom:auto;width:100vw;min-width:100vw;max-width:100vw;margin-top:0;padding:1rem;border-radius:0;border-bottom:1px solid var(--surface-border);z-index:1200}.search-dropdown-panel .search-iconfield{margin-bottom:1rem}.search-dropdown{max-width:100%;left:0!important;right:0!important;transform:none;position:static;border-radius:var(--f-content-border-radius);margin-top:0}.suggestion-item{padding:.75rem}}\n"], dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "component", type: i1.IconFieldComponent, selector: "f-iconfield", inputs: ["iconPosition", "styleClass"] }, { kind: "component", type: i1.InputIconComponent, selector: "f-inputicon", inputs: ["icon", "position", "styleClass", "style"] }, { kind: "directive", type: i1.InputTextDirective, selector: "[fInputText]", inputs: ["invalid", "touched", "variant", "size", "fluid"] }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2090
+ }
2091
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSearchbar, decorators: [{
2092
+ type: Component,
2093
+ args: [{ selector: 'app-searchbar', imports: [UiModule, AngularModule, TranslatePipe], template: `
2094
+ <div
2095
+ class="search-container h-full"
2096
+ #searchWrapper
2097
+ [class.topbar-mode]="layoutService.isTopbar() || isResponsive()"
2098
+ >
2099
+ @if (layoutService.isTopbar() || isResponsive()) {
2100
+ <!-- Topbar/Mobile Icon Mode -->
2101
+ <button
2102
+ type="button"
2103
+ class="search-toggle-btn layout-topbar-action"
2104
+ (click)="toggleSearch()"
2105
+ [attr.aria-label]="'layout.topbar.search' | translate"
2106
+ >
2107
+ <ui-icon name="search"></ui-icon>
2108
+ </button>
2109
+
2110
+ @if (isSearchExpanded()) {
2111
+ <div class="search-dropdown-panel">
2112
+ <f-iconfield class="search-iconfield">
2113
+ <f-inputicon class="search-icon"><ui-icon name="search" /></f-inputicon>
2114
+ <input
2115
+ type="text"
2116
+ fInputText
2117
+ class="search-input w-full"
2118
+ [placeholder]="'layout.topbar.search.placeholder' | translate"
2119
+ [(ngModel)]="searchTerm"
2120
+ (keyup.enter)="handleEnter()"
2121
+ (keydown)="handleKeydown($event)"
2122
+ (focus)="onFocus()"
2123
+ [attr.aria-label]="'layout.topbar.search' | translate"
2124
+ #searchInput
2125
+ autocomplete="off"
2126
+ />
2127
+ </f-iconfield>
2128
+
2129
+ @if (suggestionsOpen() && suggestions().length > 0) {
2130
+ <div class="search-suggestions">
2131
+ <ul class="suggestions-list">
2132
+ @for (
2133
+ item of suggestions();
2134
+ track item.label;
2135
+ let idx = $index
2136
+ ) {
2137
+ <li
2138
+ class="suggestion-item"
2139
+ [class.focused]="idx === focusedIndex()"
2140
+ (click)="selectSuggestion(item)"
2141
+ (mouseenter)="focusedIndex.set(idx)"
2142
+ role="option"
2143
+ [attr.aria-selected]="idx === focusedIndex()"
2144
+ >
2145
+ @if (item.icon) {
2146
+ <ui-icon [name]="item.icon" class="suggestion-icon"></ui-icon>
2147
+ }
2148
+ <span class="suggestion-label">{{ item.label }}</span>
2149
+ </li>
2150
+ }
2151
+ </ul>
2152
+ </div>
2153
+ }
2154
+ </div>
2155
+ }
2156
+ } @else {
2157
+ <!-- Desktop Full Input Mode -->
2158
+ <f-iconfield class="search-iconfield">
2159
+ <f-inputicon class="search-icon cursor-pointer" (click)="onSearch()"><ui-icon name="search" /></f-inputicon>
2160
+ <input
2161
+ type="text"
2162
+ fInputText
2163
+ class="search-input w-full"
2164
+ [placeholder]="'layout.topbar.search.placeholder' | translate"
2165
+ [(ngModel)]="searchTerm"
2166
+ (keyup.enter)="handleEnter()"
2167
+ (keydown)="handleKeydown($event)"
2168
+ (focus)="onFocus()"
2169
+ [attr.aria-label]="'layout.topbar.search' | translate"
2170
+ #searchInput
2171
+ />
2172
+ </f-iconfield>
2173
+
2174
+ @if (suggestionsOpen() && suggestions().length > 0) {
2175
+ <div class="search-dropdown">
2176
+ <ul class="suggestions-list">
2177
+ @for (item of suggestions(); track item.label; let idx = $index) {
2178
+ <li
2179
+ class="suggestion-item"
2180
+ [class.focused]="idx === focusedIndex()"
2181
+ (click)="selectSuggestion(item)"
2182
+ (mouseenter)="focusedIndex.set(idx)"
2183
+ role="option"
2184
+ [attr.aria-selected]="idx === focusedIndex()"
2185
+ >
2186
+ @if (item.icon) {
2187
+ <ui-icon [name]="item.icon" class="suggestion-icon"></ui-icon>
2188
+ }
2189
+ <span class="suggestion-label">{{ item.label }}</span>
2190
+ </li>
2191
+ }
2192
+ </ul>
2193
+ </div>
2194
+ }
2195
+ }
2196
+ </div>
2197
+ `, styles: [".search-container{display:flex;align-items:center;position:relative;width:100%;flex:0 1 auto}.search-container.topbar-mode{width:auto}.search-dropdown-panel{position:absolute;top:100%;right:0;margin-top:.5rem;background-color:var(--surface-card);border:1px solid var(--surface-border);border-radius:var(--f-content-border-radius);box-shadow:var(--f-shadow-xl);z-index:1100;padding:1rem .75rem .75rem;min-width:300px;max-width:450px;animation:slideDown .2s ease-out}.search-suggestions{margin-top:.5rem;border-top:1px solid var(--surface-border);padding-top:.5rem;background-color:inherit}.suggestions-list{list-style:none;margin:0;padding:0;max-height:360px;overflow-y:auto;background-color:inherit}.suggestion-item{padding:.75rem 1rem;cursor:pointer;display:flex;align-items:center;gap:.75rem;color:var(--text-color);border-bottom:1px solid var(--surface-border);transition:background-color .15s ease,color .15s ease}.suggestion-item:last-child{border-bottom:none}.suggestion-item:hover,.suggestion-item.focused{background-color:var(--surface-50);color:var(--primary-color)}.suggestion-icon{color:var(--text-color-secondary);font-size:1rem;flex-shrink:0;transition:color .15s ease}.suggestion-item:hover .suggestion-icon,.suggestion-item.focused .suggestion-icon{color:var(--primary-color)}.suggestion-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.search-dropdown{position:absolute;top:calc(100% + .5rem);left:0;right:0;background-color:var(--surface-card);border:1px solid var(--surface-border);border-radius:var(--f-content-border-radius);box-shadow:var(--f-shadow-xl);z-index:1000;overflow:hidden}.search-dropdown .suggestions-list{background-color:inherit}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@media(max-width:768px){.search-dropdown-panel{min-width:280px;max-width:400px}}@media(max-width:576px){.search-dropdown-panel{position:fixed;top:4rem;left:0;right:0;bottom:auto;width:100vw;min-width:100vw;max-width:100vw;margin-top:0;padding:1rem;border-radius:0;border-bottom:1px solid var(--surface-border);z-index:1200}.search-dropdown-panel .search-iconfield{margin-bottom:1rem}.search-dropdown{max-width:100%;left:0!important;right:0!important;transform:none;position:static;border-radius:var(--f-content-border-radius);margin-top:0}.suggestion-item{padding:.75rem}}\n"] }]
2198
+ }], ctorParameters: () => [], propDecorators: { searchWrapper: [{ type: i0.ViewChild, args: ['searchWrapper', { isSignal: true }] }], searchInput: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
2199
+
1679
2200
  class AppTopbar {
1680
2201
  appConfig = inject(APP_CONFIG);
1681
2202
  document = inject(DOCUMENT$1);
1682
2203
  destroyRef = inject(DestroyRef);
1683
2204
  layoutService = inject(LayoutService);
1684
- configContainer = viewChild('configContainer', ...(ngDevMode ? [{ debugName: "configContainer" }] : []));
1685
- activePanel = signal(null, ...(ngDevMode ? [{ debugName: "activePanel" }] : []));
2205
+ notificationBellComponent = inject(LAYOUT_NOTIFICATION_BELL, {
2206
+ optional: true,
2207
+ });
2208
+ languageSelectorComponent = inject(LAYOUT_LANGUAGE_SELECTOR, {
2209
+ optional: true,
2210
+ });
2211
+ searchAdapter = inject(LAYOUT_SEARCH_ADAPTER, {
2212
+ optional: true,
2213
+ });
2214
+ hasSearchAdapter = computed(() => !!this.searchAdapter, /* @ts-ignore */
2215
+ ...(ngDevMode ? [{ debugName: "hasSearchAdapter" }] : /* istanbul ignore next */ []));
2216
+ configContainer = viewChild('configContainer', /* @ts-ignore */
2217
+ ...(ngDevMode ? [{ debugName: "configContainer" }] : /* istanbul ignore next */ []));
2218
+ navContainer = viewChild('navContainer', /* @ts-ignore */
2219
+ ...(ngDevMode ? [{ debugName: "navContainer" }] : /* istanbul ignore next */ []));
2220
+ activePanel = signal(null, /* @ts-ignore */
2221
+ ...(ngDevMode ? [{ debugName: "activePanel" }] : /* istanbul ignore next */ []));
1686
2222
  constructor() {
1687
2223
  fromEvent(this.document, 'click')
1688
2224
  .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.activePanel() !== null))
1689
2225
  .subscribe((event) => {
1690
2226
  this.handleOutsideClick(event);
1691
2227
  });
2228
+ effect(() => {
2229
+ const nav = this.navContainer();
2230
+ if (nav && this.layoutService.isTopbar()) {
2231
+ fromEvent(nav.nativeElement, 'scroll')
2232
+ .pipe(takeUntilDestroyed(this.destroyRef))
2233
+ .subscribe(() => this.recalculateHoveredSubmenu(nav.nativeElement));
2234
+ }
2235
+ });
1692
2236
  }
1693
- // Use layoutService.companyName which falls back to appName
1694
2237
  companyName = this.layoutService.companyName;
1695
- enableCompanyFeature = computed(() => {
1696
- return isCompanyFeatureEnabled(this.appConfig);
1697
- }, ...(ngDevMode ? [{ debugName: "enableCompanyFeature" }] : []));
2238
+ displayLogo = this.layoutService.displayLogo;
2239
+ enableCompanyFeature = isCompanyFeatureEnabled(this.appConfig);
1698
2240
  toggleDarkMode() {
1699
- this.layoutService.layoutConfig.update((state) => ({
1700
- ...state,
1701
- darkTheme: !state.darkTheme,
1702
- }));
2241
+ const currentDarkTheme = this.layoutService.layoutConfig().darkTheme;
2242
+ this.layoutService.updateLayoutConfig({ darkTheme: !currentDarkTheme });
1703
2243
  }
1704
2244
  togglePanel(panel) {
1705
2245
  this.activePanel.update((current) => (current === panel ? null : panel));
@@ -1711,40 +2251,90 @@ class AppTopbar {
1711
2251
  this.activePanel.set(null);
1712
2252
  }
1713
2253
  }
1714
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1715
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AppTopbar, isStandalone: true, selector: "app-topbar", viewQueries: [{ propertyName: "configContainer", first: true, predicate: ["configContainer"], descendants: true, isSignal: true }], ngImport: i0, template: ` <div class="layout-topbar">
1716
- <div class="layout-topbar-logo-container">
1717
- <button
1718
- class="layout-menu-button layout-topbar-action"
1719
- (click)="layoutService.onMenuToggle()"
1720
- >
1721
- <i class="pi pi-bars"></i>
1722
- </button>
2254
+ recalculateHoveredSubmenu(navEl) {
2255
+ const hoveredItems = navEl.querySelectorAll('li:hover');
2256
+ hoveredItems.forEach((li) => {
2257
+ const link = li.querySelector(':scope > a');
2258
+ if (!link)
2259
+ return;
2260
+ const rect = link.getBoundingClientRect();
2261
+ const top = rect.bottom + 12;
2262
+ const left = rect.left - 12;
2263
+ li.style.setProperty('--submenu-left', `${left}px`);
2264
+ li.style.setProperty('--submenu-top', `${top}px`);
2265
+ });
2266
+ }
2267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppTopbar, isStandalone: true, selector: "app-topbar", viewQueries: [{ propertyName: "configContainer", first: true, predicate: ["configContainer"], descendants: true, isSignal: true }, { propertyName: "navContainer", first: true, predicate: ["navContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `<div class="layout-topbar">
2269
+ <div
2270
+ class="layout-topbar-logo-container"
2271
+ [class.layout-topbar-logo-container-desktop]="layoutService.isDesktop()"
2272
+ >
2273
+ @if (!layoutService.isTopbar() && layoutService.isMobile()) {
2274
+ <button
2275
+ class="layout-menu-button layout-topbar-action"
2276
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2277
+ (click)="layoutService.onMenuToggle()"
2278
+ >
2279
+ <ui-icon name="bars"></ui-icon>
2280
+ </button>
2281
+ }
1723
2282
  <a class="layout-topbar-logo" routerLink="/">
1724
- <span>{{ companyName() }}</span>
2283
+ @if (displayLogo()) {
2284
+ @if (layoutService.isDisplayLogoSvg()) {
2285
+ <inline-svg [src]="displayLogo()" class="logo-image" />
2286
+ } @else {
2287
+ <img
2288
+ [src]="displayLogo()"
2289
+ [alt]="companyName()"
2290
+ class="logo-image"
2291
+ />
2292
+ }
2293
+ }
2294
+ <span class="hidden sm:inline">{{ companyName() }}</span>
1725
2295
  </a>
2296
+ @if (!layoutService.isTopbar() && layoutService.isDesktop()) {
2297
+ <button
2298
+ class="layout-menu-button layout-topbar-action layout-topbar-action-right"
2299
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2300
+ (click)="layoutService.onMenuToggle()"
2301
+ >
2302
+ <ui-icon name="bars"></ui-icon>
2303
+ </button>
2304
+ }
1726
2305
  </div>
1727
2306
 
2307
+ @if (layoutService.isTopbar() && layoutService.isDesktop()) {
2308
+ <div
2309
+ #navContainer
2310
+ class="layout-topbar-nav-horizontal"
2311
+ [class.layout-topbar-nav-hidden]="
2312
+ !layoutService.layoutState().topbarMenuVisible
2313
+ "
2314
+ >
2315
+ <app-menu />
2316
+ </div>
2317
+ }
2318
+
1728
2319
  <div class="layout-topbar-actions">
2320
+ @if (hasSearchAdapter()) {
2321
+ <app-searchbar />
2322
+ }
1729
2323
  <div class="layout-config-menu">
1730
2324
  <button
1731
2325
  type="button"
1732
2326
  class="layout-topbar-action"
2327
+ [attr.aria-label]="'layout.topbar.toggle.dark.mode' | translate"
1733
2328
  (click)="toggleDarkMode()"
1734
2329
  >
1735
- <i
1736
- [ngClass]="{
1737
- 'pi ': true,
1738
- 'pi-moon': layoutService.isDarkTheme(),
1739
- 'pi-sun': !layoutService.isDarkTheme(),
1740
- }"
1741
- ></i>
2330
+ <ui-icon [name]="layoutService.isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
1742
2331
  </button>
1743
2332
  <div class="relative" #configContainer>
1744
2333
  <button
1745
2334
  class="layout-topbar-action"
1746
2335
  [class.layout-topbar-action-highlight]="activePanel() === 'config'"
1747
- pStyleClass="@next"
2336
+ [attr.aria-label]="'layout.topbar.open.theme.settings' | translate"
2337
+ fStyleClass="@next"
1748
2338
  enterFromClass="hidden"
1749
2339
  enterActiveClass="animate-scalein"
1750
2340
  leaveToClass="hidden"
@@ -1752,7 +2342,7 @@ class AppTopbar {
1752
2342
  [hideOnOutsideClick]="true"
1753
2343
  (click)="togglePanel('config')"
1754
2344
  >
1755
- <i class="pi pi-palette"></i>
2345
+ <ui-icon name="palette"></ui-icon>
1756
2346
  </button>
1757
2347
  <app-configurator />
1758
2348
  </div>
@@ -1760,14 +2350,15 @@ class AppTopbar {
1760
2350
 
1761
2351
  <button
1762
2352
  class="layout-topbar-menu-button layout-topbar-action"
1763
- pStyleClass="@next"
2353
+ [attr.aria-label]="'layout.topbar.more.options' | translate"
2354
+ fStyleClass="@next"
1764
2355
  enterFromClass="hidden"
1765
2356
  enterActiveClass="animate-scalein"
1766
2357
  leaveToClass="hidden"
1767
2358
  leaveActiveClass="animate-fadeout"
1768
2359
  [hideOnOutsideClick]="true"
1769
2360
  >
1770
- <i class="pi pi-ellipsis-v"></i>
2361
+ <ui-icon name="ellipsis-v"></ui-icon>
1771
2362
  </button>
1772
2363
 
1773
2364
  <div class="layout-topbar-menu hidden lg:block">
@@ -1775,62 +2366,105 @@ class AppTopbar {
1775
2366
  @if (layoutService.hasApps()) {
1776
2367
  <app-launcher />
1777
2368
  }
1778
- @if (enableCompanyFeature()) {
2369
+ @if (languageSelectorComponent) {
2370
+ <ng-container *ngComponentOutlet="languageSelectorComponent" />
2371
+ }
2372
+ @if (enableCompanyFeature) {
1779
2373
  <app-company-branch-selector />
1780
2374
  }
2375
+ @if (notificationBellComponent) {
2376
+ <ng-container *ngComponentOutlet="notificationBellComponent" />
2377
+ }
1781
2378
  <app-profile />
1782
2379
  </div>
1783
2380
  </div>
1784
2381
  </div>
1785
- </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "component", type: AppConfigurator, selector: "app-configurator" }, { kind: "component", type: AppProfile, selector: "app-profile" }, { kind: "component", type: AppCompanyBranchSelector, selector: "app-company-branch-selector" }, { kind: "component", type: AppLauncher, selector: "app-launcher" }] });
2382
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "directive", type: i1.StyleClassDirective, selector: "[fStyleClass]", inputs: ["fStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass"] }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: InlineSvgComponent, selector: "inline-svg", inputs: ["src"] }, { kind: "component", type: AppConfigurator, selector: "app-configurator" }, { kind: "component", type: AppMenu, selector: "app-menu" }, { kind: "component", type: AppProfile, selector: "app-profile" }, { kind: "component", type: AppCompanyBranchSelector, selector: "app-company-branch-selector" }, { kind: "component", type: AppLauncher, selector: "app-launcher" }, { kind: "component", type: AppSearchbar, selector: "app-searchbar" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1786
2383
  }
1787
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppTopbar, decorators: [{
2384
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppTopbar, decorators: [{
1788
2385
  type: Component,
1789
2386
  args: [{
1790
2387
  selector: 'app-topbar',
1791
- standalone: true,
1792
- imports: [
1793
- RouterModule,
1794
- CommonModule,
1795
- StyleClassModule,
2388
+ imports: [UiModule, RouterModule,
2389
+ NgComponentOutlet,
2390
+ TranslatePipe,
2391
+ InlineSvgComponent,
1796
2392
  AppConfigurator,
2393
+ AppMenu,
1797
2394
  AppProfile,
1798
2395
  AppCompanyBranchSelector,
1799
2396
  AppLauncher,
1800
- ],
1801
- template: ` <div class="layout-topbar">
1802
- <div class="layout-topbar-logo-container">
1803
- <button
1804
- class="layout-menu-button layout-topbar-action"
1805
- (click)="layoutService.onMenuToggle()"
1806
- >
1807
- <i class="pi pi-bars"></i>
1808
- </button>
2397
+ AppSearchbar,],
2398
+ template: `<div class="layout-topbar">
2399
+ <div
2400
+ class="layout-topbar-logo-container"
2401
+ [class.layout-topbar-logo-container-desktop]="layoutService.isDesktop()"
2402
+ >
2403
+ @if (!layoutService.isTopbar() && layoutService.isMobile()) {
2404
+ <button
2405
+ class="layout-menu-button layout-topbar-action"
2406
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2407
+ (click)="layoutService.onMenuToggle()"
2408
+ >
2409
+ <ui-icon name="bars"></ui-icon>
2410
+ </button>
2411
+ }
1809
2412
  <a class="layout-topbar-logo" routerLink="/">
1810
- <span>{{ companyName() }}</span>
2413
+ @if (displayLogo()) {
2414
+ @if (layoutService.isDisplayLogoSvg()) {
2415
+ <inline-svg [src]="displayLogo()" class="logo-image" />
2416
+ } @else {
2417
+ <img
2418
+ [src]="displayLogo()"
2419
+ [alt]="companyName()"
2420
+ class="logo-image"
2421
+ />
2422
+ }
2423
+ }
2424
+ <span class="hidden sm:inline">{{ companyName() }}</span>
1811
2425
  </a>
2426
+ @if (!layoutService.isTopbar() && layoutService.isDesktop()) {
2427
+ <button
2428
+ class="layout-menu-button layout-topbar-action layout-topbar-action-right"
2429
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2430
+ (click)="layoutService.onMenuToggle()"
2431
+ >
2432
+ <ui-icon name="bars"></ui-icon>
2433
+ </button>
2434
+ }
1812
2435
  </div>
1813
2436
 
2437
+ @if (layoutService.isTopbar() && layoutService.isDesktop()) {
2438
+ <div
2439
+ #navContainer
2440
+ class="layout-topbar-nav-horizontal"
2441
+ [class.layout-topbar-nav-hidden]="
2442
+ !layoutService.layoutState().topbarMenuVisible
2443
+ "
2444
+ >
2445
+ <app-menu />
2446
+ </div>
2447
+ }
2448
+
1814
2449
  <div class="layout-topbar-actions">
2450
+ @if (hasSearchAdapter()) {
2451
+ <app-searchbar />
2452
+ }
1815
2453
  <div class="layout-config-menu">
1816
2454
  <button
1817
2455
  type="button"
1818
2456
  class="layout-topbar-action"
2457
+ [attr.aria-label]="'layout.topbar.toggle.dark.mode' | translate"
1819
2458
  (click)="toggleDarkMode()"
1820
2459
  >
1821
- <i
1822
- [ngClass]="{
1823
- 'pi ': true,
1824
- 'pi-moon': layoutService.isDarkTheme(),
1825
- 'pi-sun': !layoutService.isDarkTheme(),
1826
- }"
1827
- ></i>
2460
+ <ui-icon [name]="layoutService.isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
1828
2461
  </button>
1829
2462
  <div class="relative" #configContainer>
1830
2463
  <button
1831
2464
  class="layout-topbar-action"
1832
2465
  [class.layout-topbar-action-highlight]="activePanel() === 'config'"
1833
- pStyleClass="@next"
2466
+ [attr.aria-label]="'layout.topbar.open.theme.settings' | translate"
2467
+ fStyleClass="@next"
1834
2468
  enterFromClass="hidden"
1835
2469
  enterActiveClass="animate-scalein"
1836
2470
  leaveToClass="hidden"
@@ -1838,7 +2472,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1838
2472
  [hideOnOutsideClick]="true"
1839
2473
  (click)="togglePanel('config')"
1840
2474
  >
1841
- <i class="pi pi-palette"></i>
2475
+ <ui-icon name="palette"></ui-icon>
1842
2476
  </button>
1843
2477
  <app-configurator />
1844
2478
  </div>
@@ -1846,14 +2480,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1846
2480
 
1847
2481
  <button
1848
2482
  class="layout-topbar-menu-button layout-topbar-action"
1849
- pStyleClass="@next"
2483
+ [attr.aria-label]="'layout.topbar.more.options' | translate"
2484
+ fStyleClass="@next"
1850
2485
  enterFromClass="hidden"
1851
2486
  enterActiveClass="animate-scalein"
1852
2487
  leaveToClass="hidden"
1853
2488
  leaveActiveClass="animate-fadeout"
1854
2489
  [hideOnOutsideClick]="true"
1855
2490
  >
1856
- <i class="pi pi-ellipsis-v"></i>
2491
+ <ui-icon name="ellipsis-v"></ui-icon>
1857
2492
  </button>
1858
2493
 
1859
2494
  <div class="layout-topbar-menu hidden lg:block">
@@ -1861,294 +2496,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1861
2496
  @if (layoutService.hasApps()) {
1862
2497
  <app-launcher />
1863
2498
  }
1864
- @if (enableCompanyFeature()) {
2499
+ @if (languageSelectorComponent) {
2500
+ <ng-container *ngComponentOutlet="languageSelectorComponent" />
2501
+ }
2502
+ @if (enableCompanyFeature) {
1865
2503
  <app-company-branch-selector />
1866
2504
  }
2505
+ @if (notificationBellComponent) {
2506
+ <ng-container *ngComponentOutlet="notificationBellComponent" />
2507
+ }
1867
2508
  <app-profile />
1868
2509
  </div>
1869
2510
  </div>
1870
2511
  </div>
1871
2512
  </div>`,
1872
2513
  }]
1873
- }], ctorParameters: () => [], propDecorators: { configContainer: [{ type: i0.ViewChild, args: ['configContainer', { isSignal: true }] }] } });
1874
-
1875
- class AppMenuitem {
1876
- // Signal inputs
1877
- item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
1878
- index = input.required(...(ngDevMode ? [{ debugName: "index" }] : []));
1879
- parentKey = input('', ...(ngDevMode ? [{ debugName: "parentKey" }] : []));
1880
- // Injected services
1881
- router = inject(Router);
1882
- layoutService = inject(LayoutService);
1883
- authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1884
- destroyRef = inject(DestroyRef);
1885
- // State signals
1886
- active = signal(false, ...(ngDevMode ? [{ debugName: "active" }] : []));
1887
- // Computed signals
1888
- key = computed(() => {
1889
- const parent = this.parentKey();
1890
- return parent ? `${parent}-${this.index()}` : String(this.index());
1891
- }, ...(ngDevMode ? [{ debugName: "key" }] : []));
1892
- routerLink = computed(() => {
1893
- const menuItem = this.item();
1894
- return menuItem.routerLink ?? [];
1895
- }, ...(ngDevMode ? [{ debugName: "routerLink" }] : []));
1896
- // Computed options for routerLinkActive - use 'exact' for root, 'subset' for others
1897
- routerLinkActiveOptions = computed(() => {
1898
- const link = this.routerLink();
1899
- const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1900
- return {
1901
- paths: pathsMatch,
1902
- queryParams: 'ignored',
1903
- matrixParams: 'ignored',
1904
- fragment: 'ignored',
1905
- };
1906
- }, ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : []));
1907
- constructor() {
1908
- // Subscribe to menu source changes
1909
- this.layoutService.menuSource$
1910
- .pipe(takeUntilDestroyed(this.destroyRef))
1911
- .subscribe((value) => {
1912
- Promise.resolve(null).then(() => {
1913
- const currentKey = this.key();
1914
- if (value.routeEvent) {
1915
- this.active.set(value.key === currentKey ||
1916
- value.key?.startsWith(currentKey + '-'));
1917
- }
1918
- else if (value.key !== currentKey &&
1919
- !value.key?.startsWith(currentKey + '-')) {
1920
- this.active.set(false);
1921
- }
1922
- });
1923
- });
1924
- // Subscribe to menu reset
1925
- this.layoutService.resetSource$
1926
- .pipe(takeUntilDestroyed(this.destroyRef))
1927
- .subscribe(() => this.active.set(false));
1928
- // Subscribe to navigation events
1929
- this.router.events
1930
- .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1931
- .subscribe(() => {
1932
- if (this.item().routerLink) {
1933
- this.updateActiveStateFromRoute();
1934
- }
1935
- });
1936
- // Effect to update active state on init when item has routerLink
1937
- effect(() => {
1938
- const menuItem = this.item();
1939
- if (menuItem.routerLink) {
1940
- this.updateActiveStateFromRoute();
1941
- }
1942
- });
1943
- }
1944
- updateActiveStateFromRoute() {
1945
- const link = this.routerLink();
1946
- if (!link.length)
1947
- return;
1948
- // Use 'exact' for root path to avoid matching all routes
1949
- // Use 'subset' for all other paths to match child routes
1950
- const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1951
- const isActive = this.router.isActive(link[0], {
1952
- paths: pathsMatch,
1953
- queryParams: 'ignored',
1954
- matrixParams: 'ignored',
1955
- fragment: 'ignored',
1956
- });
1957
- if (isActive) {
1958
- this.layoutService.onMenuStateChange({
1959
- key: this.key(),
1960
- routeEvent: true,
1961
- });
1962
- }
1963
- }
1964
- itemClick() {
1965
- if (this.item().children) {
1966
- this.active.update((prev) => !prev);
1967
- }
1968
- this.layoutService.onMenuStateChange({ key: this.key() });
1969
- }
1970
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppMenuitem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1971
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AppMenuitem, isStandalone: true, selector: "[app-menuitem]", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, parentKey: { classPropertyName: "parentKey", publicName: "parentKey", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.active-menuitem": "active()" } }, ngImport: i0, template: `
1972
- <ng-container>
1973
- @if (item().children?.length) {
1974
- <a (click)="itemClick()" tabindex="0" pRipple>
1975
- @if (item().icon) {
1976
- <lib-icon
1977
- [icon]="item().icon!"
1978
- [iconType]="item().iconType"
1979
- class="layout-menuitem-icon"
1980
- />
1981
- }
1982
- <span class="layout-menuitem-text">{{ item().label }}</span>
1983
- @if (item().children) {
1984
- <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
1985
- }
1986
- </a>
1987
- }
1988
- @if (item().routerLink && !item().children?.length) {
1989
- <a
1990
- (click)="itemClick()"
1991
- [routerLink]="routerLink()"
1992
- routerLinkActive="active-route"
1993
- [routerLinkActiveOptions]="routerLinkActiveOptions()"
1994
- tabindex="0"
1995
- pRipple
1996
- >
1997
- @if (item().icon) {
1998
- <lib-icon
1999
- [icon]="item().icon!"
2000
- [iconType]="item().iconType"
2001
- class="layout-menuitem-icon"
2002
- />
2003
- }
2004
- <span class="layout-menuitem-text">{{ item().label }}</span>
2005
- </a>
2006
- }
2007
- @if (item().children) {
2008
- <ul
2009
- [class.submenu-expanded]="active()"
2010
- [class.submenu-collapsed]="!active()"
2011
- >
2012
- @for (
2013
- child of item().children;
2014
- track 'menu' + child.id + '' + i;
2015
- let i = $index
2016
- ) {
2017
- <li
2018
- app-menuitem
2019
- [item]="child"
2020
- [index]="i"
2021
- [parentKey]="key()"
2022
- ></li>
2023
- }
2024
- </ul>
2025
- }
2026
- </ng-container>
2027
- `, isInline: true, styles: [":host ul{overflow:hidden;transition:max-height .4s cubic-bezier(.86,0,.07,1)}:host ul.submenu-collapsed{max-height:0}:host ul.submenu-expanded{max-height:1000px}\n"], dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "component", type: IconComponent, selector: "lib-icon", inputs: ["icon", "iconType"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1$2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "ngmodule", type: RippleModule }, { kind: "directive", type: i2$3.Ripple, selector: "[pRipple]" }] });
2028
- }
2029
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppMenuitem, decorators: [{
2030
- type: Component,
2031
- args: [{ selector: '[app-menuitem]', standalone: true, imports: [IconComponent, RouterModule, RippleModule], template: `
2032
- <ng-container>
2033
- @if (item().children?.length) {
2034
- <a (click)="itemClick()" tabindex="0" pRipple>
2035
- @if (item().icon) {
2036
- <lib-icon
2037
- [icon]="item().icon!"
2038
- [iconType]="item().iconType"
2039
- class="layout-menuitem-icon"
2040
- />
2041
- }
2042
- <span class="layout-menuitem-text">{{ item().label }}</span>
2043
- @if (item().children) {
2044
- <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
2045
- }
2046
- </a>
2047
- }
2048
- @if (item().routerLink && !item().children?.length) {
2049
- <a
2050
- (click)="itemClick()"
2051
- [routerLink]="routerLink()"
2052
- routerLinkActive="active-route"
2053
- [routerLinkActiveOptions]="routerLinkActiveOptions()"
2054
- tabindex="0"
2055
- pRipple
2056
- >
2057
- @if (item().icon) {
2058
- <lib-icon
2059
- [icon]="item().icon!"
2060
- [iconType]="item().iconType"
2061
- class="layout-menuitem-icon"
2062
- />
2063
- }
2064
- <span class="layout-menuitem-text">{{ item().label }}</span>
2065
- </a>
2066
- }
2067
- @if (item().children) {
2068
- <ul
2069
- [class.submenu-expanded]="active()"
2070
- [class.submenu-collapsed]="!active()"
2071
- >
2072
- @for (
2073
- child of item().children;
2074
- track 'menu' + child.id + '' + i;
2075
- let i = $index
2076
- ) {
2077
- <li
2078
- app-menuitem
2079
- [item]="child"
2080
- [index]="i"
2081
- [parentKey]="key()"
2082
- ></li>
2083
- }
2084
- </ul>
2085
- }
2086
- </ng-container>
2087
- `, host: {
2088
- '[class.active-menuitem]': 'active()',
2089
- }, styles: [":host ul{overflow:hidden;transition:max-height .4s cubic-bezier(.86,0,.07,1)}:host ul.submenu-collapsed{max-height:0}:host ul.submenu-expanded{max-height:1000px}\n"] }]
2090
- }], ctorParameters: () => [], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }], parentKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentKey", required: false }] }] } });
2091
-
2092
- /**
2093
- * Main menu component that displays filtered menu items.
2094
- * Menu is automatically filtered based on user permissions using the permission checker (logicNode pattern).
2095
- * The filtering happens internally in LayoutService via computed signal.
2096
- */
2097
- class AppMenu {
2098
- layoutService = inject(LayoutService);
2099
- /**
2100
- * Filtered menu items from layout service.
2101
- * This signal is computed in LayoutService and automatically updates when:
2102
- * - Raw menu changes (setMenu called)
2103
- * - Permission state changes (user permissions updated)
2104
- */
2105
- menuItems = this.layoutService.menu;
2106
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
2107
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AppMenu, isStandalone: true, selector: "app-menu", ngImport: i0, template: `<div class="layout-menu">
2108
- <ul>
2109
- @for (
2110
- item of menuItems();
2111
- track 'menu' + item.id + '' + i;
2112
- let i = $index
2113
- ) {
2114
- <li app-menuitem [item]="item" [index]="i"></li>
2115
- }
2116
- </ul>
2117
- </div>`, isInline: true, dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "ngmodule", type: RouterModule }] });
2118
- }
2119
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppMenu, decorators: [{
2120
- type: Component,
2121
- args: [{
2122
- selector: 'app-menu',
2123
- standalone: true,
2124
- imports: [AppMenuitem, RouterModule],
2125
- template: `<div class="layout-menu">
2126
- <ul>
2127
- @for (
2128
- item of menuItems();
2129
- track 'menu' + item.id + '' + i;
2130
- let i = $index
2131
- ) {
2132
- <li app-menuitem [item]="item" [index]="i"></li>
2133
- }
2134
- </ul>
2135
- </div>`,
2136
- }]
2137
- }] });
2514
+ }], ctorParameters: () => [], propDecorators: { configContainer: [{ type: i0.ViewChild, args: ['configContainer', { isSignal: true }] }], navContainer: [{ type: i0.ViewChild, args: ['navContainer', { isSignal: true }] }] } });
2138
2515
 
2139
2516
  class AppSidebar {
2140
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2141
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: AppSidebar, isStandalone: true, selector: "app-sidebar", ngImport: i0, template: `
2517
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2518
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.6", type: AppSidebar, isStandalone: true, selector: "app-sidebar", ngImport: i0, template: `
2142
2519
  <div class="layout-sidebar">
2143
2520
  <app-menu />
2144
2521
  </div>
2145
2522
  `, isInline: true, dependencies: [{ kind: "component", type: AppMenu, selector: "app-menu" }] });
2146
2523
  }
2147
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppSidebar, decorators: [{
2524
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSidebar, decorators: [{
2148
2525
  type: Component,
2149
2526
  args: [{
2150
2527
  selector: 'app-sidebar',
2151
- standalone: true,
2152
2528
  imports: [AppMenu],
2153
2529
  template: `
2154
2530
  <div class="layout-sidebar">
@@ -2165,8 +2541,6 @@ class AppLayout {
2165
2541
  renderer = inject(Renderer2);
2166
2542
  router = inject(Router);
2167
2543
  menuOutsideClickListener = null;
2168
- appSidebar;
2169
- appTopBar;
2170
2544
  constructor() {
2171
2545
  this.layoutService.overlayOpen$
2172
2546
  .pipe(takeUntilDestroyed(this.destroyRef))
@@ -2189,19 +2563,21 @@ class AppLayout {
2189
2563
  isOutsideClicked(event) {
2190
2564
  const sidebarEl = this.document.querySelector('.layout-sidebar');
2191
2565
  const topbarEl = this.document.querySelector('.layout-menu-button');
2566
+ const topbarNavEl = this.document.querySelector('.layout-topbar-nav');
2192
2567
  const eventTarget = event.target;
2193
2568
  return !(sidebarEl?.isSameNode(eventTarget) ||
2194
2569
  sidebarEl?.contains(eventTarget) ||
2195
2570
  topbarEl?.isSameNode(eventTarget) ||
2196
- topbarEl?.contains(eventTarget));
2571
+ topbarEl?.contains(eventTarget) ||
2572
+ topbarNavEl?.isSameNode(eventTarget) ||
2573
+ topbarNavEl?.contains(eventTarget));
2197
2574
  }
2198
2575
  hideMenu() {
2199
- this.layoutService.layoutState.update((prev) => ({
2200
- ...prev,
2576
+ this.layoutService.updateLayoutState({
2201
2577
  overlayMenuActive: false,
2202
2578
  staticMenuMobileActive: false,
2203
2579
  menuHoverActive: false,
2204
- }));
2580
+ });
2205
2581
  if (this.menuOutsideClickListener) {
2206
2582
  this.menuOutsideClickListener();
2207
2583
  this.menuOutsideClickListener = null;
@@ -2214,20 +2590,32 @@ class AppLayout {
2214
2590
  unblockBodyScroll() {
2215
2591
  this.document.body.classList.remove('blocked-scroll');
2216
2592
  }
2217
- get containerClass() {
2218
- const config = this.layoutService.layoutConfig();
2219
- const state = this.layoutService.layoutState();
2220
- return {
2221
- 'layout-overlay': config.menuMode === 'overlay',
2222
- 'layout-static': config.menuMode === 'static',
2223
- 'layout-static-inactive': state.staticMenuDesktopInactive && config.menuMode === 'static',
2224
- 'layout-overlay-active': state.overlayMenuActive,
2225
- 'layout-mobile-active': state.staticMenuMobileActive,
2226
- };
2227
- }
2228
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
2229
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: AppLayout, isStandalone: true, selector: "app-layout", viewQueries: [{ propertyName: "appSidebar", first: true, predicate: AppSidebar, descendants: true }, { propertyName: "appTopBar", first: true, predicate: AppTopbar, descendants: true }], ngImport: i0, template: `
2230
- <div class="layout-wrapper" [ngClass]="containerClass">
2593
+ isOverlay = computed(() => this.layoutService.layoutConfig().menuMode === 'overlay', /* @ts-ignore */
2594
+ ...(ngDevMode ? [{ debugName: "isOverlay" }] : /* istanbul ignore next */ []));
2595
+ isStatic = computed(() => this.layoutService.layoutConfig().menuMode === 'static', /* @ts-ignore */
2596
+ ...(ngDevMode ? [{ debugName: "isStatic" }] : /* istanbul ignore next */ []));
2597
+ isTopbarMode = computed(() => this.layoutService.layoutConfig().menuMode === 'topbar', /* @ts-ignore */
2598
+ ...(ngDevMode ? [{ debugName: "isTopbarMode" }] : /* istanbul ignore next */ []));
2599
+ isStaticInactive = computed(() => this.layoutService.layoutState().staticMenuDesktopInactive && this.isStatic(), /* @ts-ignore */
2600
+ ...(ngDevMode ? [{ debugName: "isStaticInactive" }] : /* istanbul ignore next */ []));
2601
+ isOverlayActive = computed(() => this.layoutService.layoutState().overlayMenuActive, /* @ts-ignore */
2602
+ ...(ngDevMode ? [{ debugName: "isOverlayActive" }] : /* istanbul ignore next */ []));
2603
+ isMobileActive = computed(() => this.layoutService.layoutState().staticMenuMobileActive, /* @ts-ignore */
2604
+ ...(ngDevMode ? [{ debugName: "isMobileActive" }] : /* istanbul ignore next */ []));
2605
+ isTopbarNavHidden = computed(() => !this.layoutService.layoutState().topbarMenuVisible && this.isTopbarMode(), /* @ts-ignore */
2606
+ ...(ngDevMode ? [{ debugName: "isTopbarNavHidden" }] : /* istanbul ignore next */ []));
2607
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
2608
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.6", type: AppLayout, isStandalone: true, selector: "app-layout", ngImport: i0, template: `
2609
+ <div
2610
+ class="layout-wrapper"
2611
+ [class.layout-overlay]="isOverlay()"
2612
+ [class.layout-static]="isStatic()"
2613
+ [class.layout-topbar-mode]="isTopbarMode()"
2614
+ [class.layout-static-inactive]="isStaticInactive()"
2615
+ [class.layout-overlay-active]="isOverlayActive()"
2616
+ [class.layout-mobile-active]="isMobileActive()"
2617
+ [class.layout-topbar-nav-hidden]="isTopbarNavHidden()"
2618
+ >
2231
2619
  <app-topbar></app-topbar>
2232
2620
  <app-sidebar></app-sidebar>
2233
2621
  <div class="layout-main-container">
@@ -2238,16 +2626,24 @@ class AppLayout {
2238
2626
  </div>
2239
2627
  <div class="layout-mask animate-fadein"></div>
2240
2628
  </div>
2241
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: AppTopbar, selector: "app-topbar" }, { kind: "component", type: AppSidebar, selector: "app-sidebar" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: AppFooter, selector: "app-footer" }] });
2629
+ `, isInline: true, dependencies: [{ kind: "component", type: AppTopbar, selector: "app-topbar" }, { kind: "component", type: AppSidebar, selector: "app-sidebar" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: AppFooter, selector: "app-footer" }] });
2242
2630
  }
2243
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AppLayout, decorators: [{
2631
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLayout, decorators: [{
2244
2632
  type: Component,
2245
2633
  args: [{
2246
2634
  selector: 'app-layout',
2247
- standalone: true,
2248
- imports: [CommonModule, AppTopbar, AppSidebar, RouterModule, AppFooter],
2635
+ imports: [AppTopbar, AppSidebar, RouterModule, AppFooter],
2249
2636
  template: `
2250
- <div class="layout-wrapper" [ngClass]="containerClass">
2637
+ <div
2638
+ class="layout-wrapper"
2639
+ [class.layout-overlay]="isOverlay()"
2640
+ [class.layout-static]="isStatic()"
2641
+ [class.layout-topbar-mode]="isTopbarMode()"
2642
+ [class.layout-static-inactive]="isStaticInactive()"
2643
+ [class.layout-overlay-active]="isOverlayActive()"
2644
+ [class.layout-mobile-active]="isMobileActive()"
2645
+ [class.layout-topbar-nav-hidden]="isTopbarNavHidden()"
2646
+ >
2251
2647
  <app-topbar></app-topbar>
2252
2648
  <app-sidebar></app-sidebar>
2253
2649
  <div class="layout-main-container">
@@ -2260,55 +2656,99 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2260
2656
  </div>
2261
2657
  `,
2262
2658
  }]
2263
- }], ctorParameters: () => [], propDecorators: { appSidebar: [{
2264
- type: ViewChild,
2265
- args: [AppSidebar]
2266
- }], appTopBar: [{
2267
- type: ViewChild,
2268
- args: [AppTopbar]
2269
- }] } });
2659
+ }], ctorParameters: () => [] });
2270
2660
 
2271
- const GreenTheme = definePreset(Material, {
2272
- semantic: {
2273
- colorScheme: {
2274
- light: {
2275
- primary: {
2276
- color: '#01712c',
2277
- inverseColor: '#119744',
2278
- hoverColor: '#119744',
2279
- activeColor: '#119744',
2280
- },
2281
- highlight: {
2282
- background: '#e2e8f0',
2283
- focusBackground: '#e2e8f0',
2284
- color: '#01712c',
2285
- focusColor: '#01712c',
2286
- },
2287
- },
2288
- },
2289
- },
2290
- });
2661
+ function buildPreset(presetName, primary) {
2662
+ return { preset: presetName, primary };
2663
+ }
2291
2664
 
2292
- const NavyBlueTheme = definePreset(Material, {
2293
- semantic: {
2294
- colorScheme: {
2295
- light: {
2296
- primary: {
2297
- color: '#3535cd',
2298
- inverseColor: '#0707a9',
2299
- hoverColor: '#0707a9',
2300
- activeColor: '#0707a9',
2301
- },
2302
- highlight: {
2303
- background: '#e2e8f0',
2304
- focusBackground: '#e2e8f0',
2305
- color: '#3535cd',
2306
- focusColor: '#3535cd',
2307
- },
2308
- },
2309
- },
2310
- },
2311
- });
2665
+ const NavyBlueTheme = {
2666
+ preset: 'Aura',
2667
+ primary: 'indigo',
2668
+ };
2669
+
2670
+ const GreenTheme = {
2671
+ preset: 'Aura',
2672
+ primary: 'green',
2673
+ };
2674
+
2675
+ const LAYOUT_MESSAGES = {
2676
+ // components/top-bar/app.profile.ts
2677
+ 'layout.profile.title': 'Profile',
2678
+ 'layout.profile.profile.picture.alt': 'Profile picture',
2679
+ 'layout.profile.copy.sign.up.link': 'Copy SignUp Link',
2680
+ 'layout.profile.logout': 'Logout',
2681
+ 'layout.profile.guest': 'Guest',
2682
+ 'layout.profile.link.copied': 'Link Copied',
2683
+ 'layout.profile.sign.up.link.copied': 'Sign Up Link copied.',
2684
+ 'layout.profile.failed.copy.sign.up.link': 'Failed to copy signup link.',
2685
+ // components/top-bar/app.launcher.ts
2686
+ 'layout.launcher.apps': 'Apps',
2687
+ 'layout.launcher.applications': 'Applications',
2688
+ // components/top-bar/app.company-branch-selector.ts
2689
+ 'layout.company.branch.selector.title': 'Switch Company & Branch',
2690
+ 'layout.company.branch.selector.select.company': 'Select Company',
2691
+ 'layout.company.branch.selector.branch': 'Branch',
2692
+ 'layout.company.branch.selector.select.branch': 'Select Branch',
2693
+ 'layout.company.branch.selector.switch.button': 'Switch',
2694
+ 'layout.company.branch.selector.no.company': 'No Company',
2695
+ 'layout.company.branch.selector.branch.required': 'Branch Required',
2696
+ 'layout.company.branch.selector.please.select.branch': 'Please select a branch',
2697
+ // components/top-bar/app.searchbar.ts
2698
+ 'layout.topbar.search': 'Search',
2699
+ 'layout.topbar.search.placeholder': 'Search for content...',
2700
+ // components/app.topbar.ts
2701
+ 'layout.topbar.toggle.menu': 'Toggle menu',
2702
+ 'layout.topbar.toggle.dark.mode': 'Toggle dark mode',
2703
+ 'layout.topbar.open.theme.settings': 'Open theme settings',
2704
+ 'layout.topbar.more.options': 'More options',
2705
+ // components/app.configurator.ts
2706
+ 'layout.configurator.primary': 'Primary',
2707
+ 'layout.configurator.surface': 'Surface',
2708
+ 'layout.configurator.presets': 'Presets',
2709
+ 'layout.configurator.menu.mode': 'Menu Mode',
2710
+ 'layout.configurator.menu.mode.static': 'Static',
2711
+ 'layout.configurator.menu.mode.overlay': 'Overlay',
2712
+ 'layout.configurator.menu.mode.topbar': 'Topbar',
2713
+ // components/app.footer.ts
2714
+ 'layout.footer.by': 'by',
2715
+ // app-menu.config.ts (main app menu configuration)
2716
+ 'menu.dashboard': 'Dashboard',
2717
+ 'menu.administration': 'Administration',
2718
+ 'menu.iam': 'IAM',
2719
+ 'menu.storage': 'Storage',
2720
+ 'menu.forms': 'Forms',
2721
+ 'menu.email': 'Email',
2722
+ 'menu.event.manager': 'Event Manager',
2723
+ 'menu.task.manager': 'Task Manager',
2724
+ 'menu.notifications': 'Notifications',
2725
+ 'menu.localization': 'Localization',
2726
+ 'menu.components': 'Components',
2727
+ 'menu.components.display': 'Display',
2728
+ 'menu.components.menu': 'Menu',
2729
+ 'menu.components.form': 'Form',
2730
+ 'menu.components.overlay': 'Overlay',
2731
+ 'menu.components.data': 'Data',
2732
+ 'menu.components.panel': 'Panel',
2733
+ 'menu.components.media': 'Media',
2734
+ 'menu.components.misc': 'Directives',
2735
+ 'menu.projects': 'Projects',
2736
+ 'menu.projects.active': 'Active Projects',
2737
+ 'menu.projects.archived': 'Archived',
2738
+ 'menu.projects.reports': 'Reports',
2739
+ // Those are layout part from notification module, but we put them here to avoid circular dependency between layout and notification module
2740
+ // components/notification-bell/notification-bell.component.ts
2741
+ 'notification.title': 'Notifications',
2742
+ // components/notification-list/notification-list.component.ts
2743
+ 'notification.mark.all.read': 'Mark all read',
2744
+ 'notification.view.all': 'View all notifications',
2745
+ 'notification.empty': 'No notifications',
2746
+ // components/notification-item/notification-item.component.ts
2747
+ 'notification.time.just.now': 'Just now',
2748
+ 'notification.time.minutes.ago': '{{count}}m ago',
2749
+ 'notification.time.hours.ago': '{{count}}h ago',
2750
+ 'notification.time.days.ago': '{{count}}d ago',
2751
+ };
2312
2752
 
2313
2753
  // Components
2314
2754
 
@@ -2316,5 +2756,5 @@ const NavyBlueTheme = definePreset(Material, {
2316
2756
  * Generated bundle index. Do not edit.
2317
2757
  */
2318
2758
 
2319
- export { AppCompanyBranchSelector, AppConfigurator, AppFloatingConfigurator, AppFooter, AppLauncher, AppLayout, AppMenu, AppMenuitem, AppProfile, AppSidebar, AppTopbar, GreenTheme, LAYOUT_AUTH_API, LAYOUT_AUTH_STATE, LayoutPersistenceService, LayoutService, NavyBlueTheme, filterAppsByPermissions, filterMenuByPermissions };
2759
+ export { AppCompanyBranchSelector, AppConfigurator, AppFloatingConfigurator, AppFooter, AppLauncher, AppLayout, AppMenu, AppMenuitem, AppProfile, AppSearchbar, AppSidebar, AppTopbar, GreenTheme, LAYOUT_AUTH_API, LAYOUT_AUTH_STATE, LAYOUT_IS_RTL, LAYOUT_LANGUAGE_SELECTOR, LAYOUT_LANGUAGE_SELECTOR_PANEL, LAYOUT_MESSAGES, LAYOUT_NOTIFICATION_BELL, LAYOUT_SEARCHBAR, LAYOUT_SEARCH_ADAPTER, LayoutPersistenceService, LayoutService, NavyBlueTheme, buildPreset, filterAppsByPermissions, filterMenuByPermissions };
2320
2760
  //# sourceMappingURL=flusys-ng-layout.mjs.map