@flusys/ng-layout 1.1.0 → 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,32 +1,19 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, PLATFORM_ID, Injectable, DOCUMENT, signal, computed, effect, ChangeDetectionStrategy, Component, afterNextRender, InjectionToken, DestroyRef, ElementRef, viewChild, input, Renderer2 } from '@angular/core';
3
- import { isPlatformBrowser, NgClass, DOCUMENT as DOCUMENT$1 } from '@angular/common';
4
- import { APP_CONFIG, DEFAULT_APP_NAME, DEFAULT_AUTHOR, isCompanyFeatureEnabled } from '@flusys/ng-core';
5
- import * as i2$2 from '@flusys/ng-shared';
6
- import { evaluateLogicNode, PermissionValidatorService, AngularModule, IconComponent } from '@flusys/ng-shared';
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';
7
8
  import { Subject, fromEvent, filter as filter$1 } from 'rxjs';
8
- import * as i1 from '@angular/forms';
9
+ import * as i2 from '@angular/forms';
9
10
  import { FormsModule } from '@angular/forms';
10
- import * as i1$2 from '@angular/router';
11
- import { Router, RouterModule, NavigationEnd } from '@angular/router';
12
- import { updatePreset, updateSurfacePalette, $t, definePreset } from '@primeuix/themes';
13
- import Aura from '@primeuix/themes/aura';
14
- import Lara from '@primeuix/themes/lara';
15
- import Nora from '@primeuix/themes/nora';
16
- import * as i2 from 'primeng/selectbutton';
17
- import { SelectButtonModule } from 'primeng/selectbutton';
18
- import * as i1$1 from 'primeng/button';
19
- import { ButtonModule } from 'primeng/button';
20
- import * as i2$1 from 'primeng/styleclass';
21
- import { StyleClassModule } from 'primeng/styleclass';
22
- 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';
23
16
  import { filter } from 'rxjs/operators';
24
- import { MessageService } from 'primeng/api';
25
- import * as i4 from 'primeng/select';
26
- import { SelectModule } from 'primeng/select';
27
- import * as i2$3 from 'primeng/ripple';
28
- import { RippleModule } from 'primeng/ripple';
29
- import Material from '@primeuix/themes/material';
30
17
 
31
18
  /** Filter launcher apps based on user permission codes */
32
19
  function filterAppsByPermissions(apps, permissionCodes) {
@@ -89,36 +76,11 @@ function filterMenuItem(item, permissionCode) {
89
76
 
90
77
  const STORAGE_KEY = 'flusys.layout.config';
91
78
  const STORAGE_VERSION = 1;
92
- /**
93
- * Service for persisting layout configuration to localStorage.
94
- * Handles loading, saving, and validation of layout preferences with SSR-safe checks.
95
- *
96
- * @example
97
- * ```typescript
98
- * const persistence = inject(LayoutPersistenceService);
99
- *
100
- * // Load saved config
101
- * const config = persistence.load();
102
- *
103
- * // Save config
104
- * persistence.save({ darkTheme: true, preset: 'Aura' });
105
- *
106
- * // Clear saved config
107
- * persistence.clear();
108
- * ```
109
- */
110
79
  class LayoutPersistenceService {
111
80
  platformId = inject(PLATFORM_ID);
112
81
  isBrowser = isPlatformBrowser(this.platformId);
113
82
  validPresets = ['Aura', 'Lara', 'Nora'];
114
- validMenuModes = ['static', 'overlay'];
115
- /**
116
- * Load configuration from localStorage.
117
- * Returns null if no saved config or invalid data.
118
- * SSR-safe (returns null on server).
119
- *
120
- * @returns Validated layout configuration or null
121
- */
83
+ validMenuModes = ['static', 'overlay', 'topbar'];
122
84
  load() {
123
85
  if (!this.isBrowser)
124
86
  return null;
@@ -134,12 +96,6 @@ class LayoutPersistenceService {
134
96
  return null;
135
97
  }
136
98
  }
137
- /**
138
- * Save configuration to localStorage.
139
- * SSR-safe (no-op on server).
140
- *
141
- * @param config Layout configuration to save
142
- */
143
99
  save(config) {
144
100
  if (!this.isBrowser)
145
101
  return;
@@ -154,64 +110,46 @@ class LayoutPersistenceService {
154
110
  // Silent fail - localStorage may be full or disabled
155
111
  }
156
112
  }
157
- /**
158
- * Clear stored configuration.
159
- * SSR-safe (no-op on server).
160
- */
161
113
  clear() {
162
114
  if (!this.isBrowser)
163
115
  return;
164
116
  localStorage.removeItem(STORAGE_KEY);
165
117
  }
166
- /**
167
- * Validate and sanitize configuration.
168
- * Returns null if version mismatch.
169
- *
170
- * @param config Configuration to validate
171
- * @returns Validated configuration or null
172
- */
173
118
  validateConfig(config) {
174
- // Version mismatch - clear and return null
175
119
  if (config._version && config._version !== STORAGE_VERSION) {
176
120
  this.clear();
177
121
  return null;
178
122
  }
179
- // Validate preset - silently fix invalid values
180
123
  if (config.preset && !this.validPresets.includes(config.preset)) {
181
124
  config.preset = 'Aura';
182
125
  }
183
- // Validate menuMode - silently fix invalid values
184
126
  if (config.menuMode && !this.validMenuModes.includes(config.menuMode)) {
185
127
  config.menuMode = 'static';
186
128
  }
187
- // Validate darkTheme
188
129
  if (typeof config.darkTheme !== 'boolean') {
189
130
  config.darkTheme = false;
190
131
  }
191
- // Remove version field from returned config
192
132
  const { _version, ...cleanConfig } = config;
193
133
  return cleanConfig;
194
134
  }
195
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LayoutPersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
196
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", 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' });
197
137
  }
198
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LayoutPersistenceService, decorators: [{
138
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutPersistenceService, decorators: [{
199
139
  type: Injectable,
200
- args: [{
201
- providedIn: 'root',
202
- }]
140
+ args: [{ providedIn: 'root' }]
203
141
  }] });
204
142
 
205
- /** Layout configuration and state management service */
143
+ const DESKTOP_BREAKPOINT = 991;
206
144
  class LayoutService {
207
145
  document = inject(DOCUMENT);
208
146
  platformId = inject(PLATFORM_ID);
209
147
  isBrowser = isPlatformBrowser(this.platformId);
210
148
  persistence = inject(LayoutPersistenceService);
211
149
  appConfig = inject(APP_CONFIG, { optional: true });
212
- DEFAULT_CONFIG = {
150
+ DEFAULT_CONFIG = this.appConfig?.layoutConfig ?? {
213
151
  preset: 'Aura',
214
- primary: 'emerald',
152
+ primary: 'violet',
215
153
  surface: null,
216
154
  darkTheme: false,
217
155
  menuMode: 'static',
@@ -222,66 +160,101 @@ class LayoutService {
222
160
  configSidebarVisible: false,
223
161
  staticMenuMobileActive: false,
224
162
  menuHoverActive: false,
163
+ topbarMenuVisible: true,
225
164
  };
165
+ memorable = this.DEFAULT_CONFIG.isMemorable !== false;
226
166
  _layoutConfig = signal({
227
167
  ...this.DEFAULT_CONFIG,
228
- ...this.persistence.load(),
229
- }, ...(ngDevMode ? [{ debugName: "_layoutConfig" }] : []));
230
- _layoutState = signal(this.DEFAULT_STATE, ...(ngDevMode ? [{ debugName: "_layoutState" }] : []));
231
- _transitionComplete = signal(false, ...(ngDevMode ? [{ debugName: "_transitionComplete" }] : []));
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 */ []));
232
175
  layoutConfig = this._layoutConfig.asReadonly();
233
176
  layoutState = this._layoutState.asReadonly();
234
177
  transitionComplete = this._transitionComplete.asReadonly();
235
- _userProfile = signal(null, ...(ngDevMode ? [{ debugName: "_userProfile" }] : []));
236
- _companyProfile = signal(null, ...(ngDevMode ? [{ debugName: "_companyProfile" }] : []));
237
- _rawMenu = signal([], ...(ngDevMode ? [{ debugName: "_rawMenu" }] : []));
238
- _rawApps = signal([], ...(ngDevMode ? [{ debugName: "_rawApps" }] : []));
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 */ []));
239
186
  permissionValidator = inject(PermissionValidatorService);
187
+ destroyRef = inject(DestroyRef);
188
+ _windowWidth = signal(this.isBrowser ? window.innerWidth : DESKTOP_BREAKPOINT + 1, /* @ts-ignore */
189
+ ...(ngDevMode ? [{ debugName: "_windowWidth" }] : /* istanbul ignore next */ []));
240
190
  userProfile = this._userProfile.asReadonly();
241
191
  companyProfile = this._companyProfile.asReadonly();
242
- // Static app info from config
243
192
  appName = this.appConfig?.appName ?? DEFAULT_APP_NAME;
193
+ appLogo = this.appConfig?.appLogo ?? '';
194
+ isAppLogoSvg = this.appLogo.toLowerCase().endsWith('.svg');
244
195
  authorName = this.appConfig?.author?.name ?? DEFAULT_AUTHOR.name;
245
196
  authorUrl = this.appConfig?.author?.url ?? DEFAULT_AUTHOR.url;
246
- // Permission-filtered menu and apps
247
- menu = computed(() => filterMenuByPermissions(this._rawMenu(), this.permissionValidator.permissions()), ...(ngDevMode ? [{ debugName: "menu" }] : []));
248
- apps = computed(() => filterAppsByPermissions(this._rawApps(), this.permissionValidator.permissions()), ...(ngDevMode ? [{ debugName: "apps" }] : []));
249
- hasApps = computed(() => this.apps().length > 0, ...(ngDevMode ? [{ debugName: "hasApps" }] : []));
250
- // Computed signals - Layout
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 */ []));
251
203
  isSidebarActive = computed(() => this._layoutState().overlayMenuActive ||
252
- this._layoutState().staticMenuMobileActive, ...(ngDevMode ? [{ debugName: "isSidebarActive" }] : []));
253
- isDarkTheme = computed(() => this._layoutConfig().darkTheme, ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : []));
254
- getPrimary = computed(() => this._layoutConfig().primary, ...(ngDevMode ? [{ debugName: "getPrimary" }] : []));
255
- getSurface = computed(() => this._layoutConfig().surface, ...(ngDevMode ? [{ debugName: "getSurface" }] : []));
256
- isOverlay = computed(() => this._layoutConfig().menuMode === 'overlay', ...(ngDevMode ? [{ debugName: "isOverlay" }] : []));
257
- // User profile computed signals
258
- userName = computed(() => this._userProfile()?.name ?? 'User', ...(ngDevMode ? [{ debugName: "userName" }] : []));
259
- userEmail = computed(() => this._userProfile()?.email ?? '', ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
260
- userProfilePictureUrl = computed(() => this._userProfile()?.profilePictureUrl ?? null, ...(ngDevMode ? [{ debugName: "userProfilePictureUrl" }] : []));
261
- companyLogoUrl = computed(() => this._companyProfile()?.logoUrl ?? null, ...(ngDevMode ? [{ debugName: "companyLogoUrl" }] : []));
262
- isAuthenticated = computed(() => !!this._userProfile(), ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : []));
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 */ []));
263
226
  companyName = computed(() => {
264
227
  if (!this.appConfig || !isCompanyFeatureEnabled(this.appConfig))
265
228
  return this.appName;
266
229
  return this._companyProfile()?.name ?? this.appName;
267
- }, ...(ngDevMode ? [{ debugName: "companyName" }] : []));
268
- // RxJS Subjects for event communication
269
- configUpdate = new Subject();
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 */ []));
270
236
  overlayOpen = new Subject();
271
237
  menuSource = new Subject();
272
238
  resetSource = new Subject();
273
239
  menuSource$ = this.menuSource.asObservable();
274
240
  resetSource$ = this.resetSource.asObservable();
275
- configUpdate$ = this.configUpdate.asObservable();
276
241
  overlayOpen$ = this.overlayOpen.asObservable();
277
242
  initialized = false;
278
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
+ }
279
252
  effect(() => {
280
253
  const config = this._layoutConfig();
281
- this.configUpdate.next(config);
282
254
  if (this.initialized) {
283
255
  this.handleDarkModeTransition(config);
284
- this.persistence.save(config);
256
+ if (this.memorable)
257
+ this.persistence.save(config);
285
258
  }
286
259
  this.initialized = true;
287
260
  });
@@ -292,7 +265,8 @@ class LayoutService {
292
265
  }
293
266
  handleDarkModeTransition(config) {
294
267
  const doc = this.document;
295
- if ('startViewTransition' in doc && typeof doc.startViewTransition === 'function') {
268
+ if ('startViewTransition' in doc &&
269
+ typeof doc.startViewTransition === 'function') {
296
270
  doc
297
271
  .startViewTransition(() => this.toggleDarkMode(config))
298
272
  .ready.then(() => this.onTransitionEnd())
@@ -309,31 +283,41 @@ class LayoutService {
309
283
  }
310
284
  onMenuToggle() {
311
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) => ({
298
+ ...prev,
299
+ staticMenuMobileActive: newMobileActive,
300
+ }));
301
+ if (newMobileActive)
302
+ this.overlayOpen.next();
303
+ return;
304
+ }
305
+ // Desktop: use overlay or static mode
312
306
  if (this.isOverlay()) {
313
307
  const newOverlayActive = !state.overlayMenuActive;
314
- this._layoutState.update((prev) => ({ ...prev, overlayMenuActive: newOverlayActive }));
308
+ this._layoutState.update((prev) => ({
309
+ ...prev,
310
+ overlayMenuActive: newOverlayActive,
311
+ }));
315
312
  if (newOverlayActive)
316
313
  this.overlayOpen.next();
317
- return;
318
314
  }
319
- if (this.isDesktop()) {
315
+ else {
320
316
  this._layoutState.update((prev) => ({
321
317
  ...prev,
322
318
  staticMenuDesktopInactive: !state.staticMenuDesktopInactive,
323
319
  }));
324
320
  }
325
- else {
326
- const newMobileActive = !state.staticMenuMobileActive;
327
- this._layoutState.update((prev) => ({ ...prev, staticMenuMobileActive: newMobileActive }));
328
- if (newMobileActive)
329
- this.overlayOpen.next();
330
- }
331
- }
332
- isDesktop() {
333
- return this.isBrowser ? window.innerWidth > 991 : true;
334
- }
335
- isMobile() {
336
- return !this.isDesktop();
337
321
  }
338
322
  onMenuStateChange(event) {
339
323
  this.menuSource.next(event);
@@ -341,21 +325,18 @@ class LayoutService {
341
325
  reset() {
342
326
  this.resetSource.next(true);
343
327
  }
344
- // Config & state updates
345
328
  updateLayoutConfig(config) {
346
329
  this._layoutConfig.update((prev) => ({ ...prev, ...config }));
347
330
  }
348
331
  updateLayoutState(state) {
349
332
  this._layoutState.update((prev) => ({ ...prev, ...state }));
350
333
  }
351
- // Profile setters
352
334
  setUserProfile(profile) {
353
335
  this._userProfile.set(profile);
354
336
  }
355
337
  setCompanyProfile(profile) {
356
338
  this._companyProfile.set(profile);
357
339
  }
358
- // Menu & apps
359
340
  setMenu(items) {
360
341
  this._rawMenu.set(items);
361
342
  }
@@ -368,361 +349,128 @@ class LayoutService {
368
349
  clearApps() {
369
350
  this._rawApps.set([]);
370
351
  }
371
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
372
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", 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' });
373
354
  }
374
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LayoutService, decorators: [{
355
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutService, decorators: [{
375
356
  type: Injectable,
376
357
  args: [{ providedIn: 'root' }]
377
358
  }], ctorParameters: () => [] });
378
359
 
379
360
  class AppFooter {
380
361
  layoutService = inject(LayoutService);
381
- // Footer shows product branding (appName), not user's company
382
362
  appName = this.layoutService.appName;
363
+ displayLogo = this.layoutService.appLogo;
364
+ isLogoSvg = this.layoutService.isAppLogoSvg;
383
365
  authorName = this.layoutService.authorName;
384
366
  authorUrl = this.layoutService.authorUrl;
385
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppFooter, deps: [], target: i0.ɵɵFactoryTarget.Component });
386
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.5", type: AppFooter, isStandalone: true, selector: "app-footer", ngImport: i0, template: `<div class="layout-footer">
387
- {{ appName }} by
388
- <a [href]="authorUrl" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">{{ authorName }}</a>
389
- </div>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
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" }] });
390
387
  }
391
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppFooter, decorators: [{
388
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFooter, decorators: [{
392
389
  type: Component,
393
390
  args: [{
394
391
  selector: 'app-footer',
395
- changeDetection: ChangeDetectionStrategy.OnPush,
392
+ imports: [TranslatePipe, InlineSvgComponent],
396
393
  template: `<div class="layout-footer">
397
- {{ appName }} by
398
- <a [href]="authorUrl" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">{{ authorName }}</a>
399
- </div>`,
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>`,
400
412
  }]
401
413
  }] });
402
414
 
403
- const presets = {
404
- Aura,
405
- Lara,
406
- Nora,
407
- };
408
415
  class AppConfigurator {
409
416
  router = inject(Router);
417
+ translateAdapter = inject(TranslateService, { optional: true });
418
+ platformId = inject(PLATFORM_ID);
410
419
  layoutService = inject(LayoutService);
411
- presets = Object.keys(presets);
412
- showMenuModeButton = signal(!this.router.url.includes('auth'), ...(ngDevMode ? [{ debugName: "showMenuModeButton" }] : []));
413
- menuModeOptions = [
414
- { label: 'Static', value: 'static' },
415
- { label: 'Overlay', value: 'overlay' },
416
- ];
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 */ []));
417
434
  constructor() {
418
- // Use afterNextRender for browser-only initialization (replaces isServer check)
419
- afterNextRender(() => {
420
- const preset = this.layoutService.layoutConfig().preset ?? 'Aura';
421
- this.onPresetChange(preset);
422
- });
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
+ });
445
+ }
423
446
  }
424
- surfaces = [
425
- {
426
- name: 'slate',
427
- palette: {
428
- 0: '#ffffff',
429
- 50: '#f8fafc',
430
- 100: '#f1f5f9',
431
- 200: '#e2e8f0',
432
- 300: '#cbd5e1',
433
- 400: '#94a3b8',
434
- 500: '#64748b',
435
- 600: '#475569',
436
- 700: '#334155',
437
- 800: '#1e293b',
438
- 900: '#0f172a',
439
- 950: '#020617',
440
- },
441
- },
442
- {
443
- name: 'gray',
444
- palette: {
445
- 0: '#ffffff',
446
- 50: '#f9fafb',
447
- 100: '#f3f4f6',
448
- 200: '#e5e7eb',
449
- 300: '#d1d5db',
450
- 400: '#9ca3af',
451
- 500: '#6b7280',
452
- 600: '#4b5563',
453
- 700: '#374151',
454
- 800: '#1f2937',
455
- 900: '#111827',
456
- 950: '#030712',
457
- },
458
- },
459
- {
460
- name: 'zinc',
461
- palette: {
462
- 0: '#ffffff',
463
- 50: '#fafafa',
464
- 100: '#f4f4f5',
465
- 200: '#e4e4e7',
466
- 300: '#d4d4d8',
467
- 400: '#a1a1aa',
468
- 500: '#71717a',
469
- 600: '#52525b',
470
- 700: '#3f3f46',
471
- 800: '#27272a',
472
- 900: '#18181b',
473
- 950: '#09090b',
474
- },
475
- },
476
- {
477
- name: 'neutral',
478
- palette: {
479
- 0: '#ffffff',
480
- 50: '#fafafa',
481
- 100: '#f5f5f5',
482
- 200: '#e5e5e5',
483
- 300: '#d4d4d4',
484
- 400: '#a3a3a3',
485
- 500: '#737373',
486
- 600: '#525252',
487
- 700: '#404040',
488
- 800: '#262626',
489
- 900: '#171717',
490
- 950: '#0a0a0a',
491
- },
492
- },
493
- {
494
- name: 'stone',
495
- palette: {
496
- 0: '#ffffff',
497
- 50: '#fafaf9',
498
- 100: '#f5f5f4',
499
- 200: '#e7e5e4',
500
- 300: '#d6d3d1',
501
- 400: '#a8a29e',
502
- 500: '#78716c',
503
- 600: '#57534e',
504
- 700: '#44403c',
505
- 800: '#292524',
506
- 900: '#1c1917',
507
- 950: '#0c0a09',
508
- },
509
- },
510
- {
511
- name: 'soho',
512
- palette: {
513
- 0: '#ffffff',
514
- 50: '#ececec',
515
- 100: '#dedfdf',
516
- 200: '#c4c4c6',
517
- 300: '#adaeb0',
518
- 400: '#97979b',
519
- 500: '#7f8084',
520
- 600: '#6a6b70',
521
- 700: '#55565b',
522
- 800: '#3f4046',
523
- 900: '#2c2c34',
524
- 950: '#16161d',
525
- },
526
- },
527
- {
528
- name: 'viva',
529
- palette: {
530
- 0: '#ffffff',
531
- 50: '#f3f3f3',
532
- 100: '#e7e7e8',
533
- 200: '#cfd0d0',
534
- 300: '#b7b8b9',
535
- 400: '#9fa1a1',
536
- 500: '#87898a',
537
- 600: '#6e7173',
538
- 700: '#565a5b',
539
- 800: '#3e4244',
540
- 900: '#262b2c',
541
- 950: '#0e1315',
542
- },
543
- },
544
- {
545
- name: 'ocean',
546
- palette: {
547
- 0: '#ffffff',
548
- 50: '#fbfcfc',
549
- 100: '#F7F9F8',
550
- 200: '#EFF3F2',
551
- 300: '#DADEDD',
552
- 400: '#B1B7B6',
553
- 500: '#828787',
554
- 600: '#5F7274',
555
- 700: '#415B61',
556
- 800: '#29444E',
557
- 900: '#183240',
558
- 950: '#0c1920',
559
- },
560
- },
561
- ];
562
- selectedPrimaryColor = computed(() => {
563
- return this.layoutService.layoutConfig().primary;
564
- }, ...(ngDevMode ? [{ debugName: "selectedPrimaryColor" }] : []));
565
- selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface, ...(ngDevMode ? [{ debugName: "selectedSurfaceColor" }] : []));
566
- selectedPreset = computed(() => this.layoutService.layoutConfig().preset, ...(ngDevMode ? [{ debugName: "selectedPreset" }] : []));
567
- 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 */ []));
568
466
  primaryColors = computed(() => {
569
- const presetPalette = presets[this.layoutService.layoutConfig().preset].primitive;
570
- const colors = [
571
- 'emerald',
572
- 'green',
573
- 'lime',
574
- 'orange',
575
- 'amber',
576
- 'yellow',
577
- 'teal',
578
- 'cyan',
579
- 'sky',
580
- 'blue',
581
- 'indigo',
582
- 'violet',
583
- 'purple',
584
- 'fuchsia',
585
- 'pink',
586
- 'rose',
587
- ];
588
467
  const palettes = [{ name: 'noir', palette: {} }];
589
- colors.forEach((color) => {
590
- palettes.push({
591
- name: color,
592
- palette: presetPalette?.[color],
593
- });
594
- });
595
- return palettes;
596
- }, ...(ngDevMode ? [{ debugName: "primaryColors" }] : []));
597
- getPresetExt() {
598
- const color = this.primaryColors().find((c) => c.name === this.selectedPrimaryColor()) || {};
599
- const preset = this.layoutService.layoutConfig().preset;
600
- if (color.name === 'noir') {
601
- return {
602
- semantic: {
603
- primary: {
604
- 50: '{surface.50}',
605
- 100: '{surface.100}',
606
- 200: '{surface.200}',
607
- 300: '{surface.300}',
608
- 400: '{surface.400}',
609
- 500: '{surface.500}',
610
- 600: '{surface.600}',
611
- 700: '{surface.700}',
612
- 800: '{surface.800}',
613
- 900: '{surface.900}',
614
- 950: '{surface.950}',
615
- },
616
- colorScheme: {
617
- light: {
618
- primary: {
619
- color: '{primary.950}',
620
- contrastColor: '#ffffff',
621
- hoverColor: '{primary.800}',
622
- activeColor: '{primary.700}',
623
- },
624
- highlight: {
625
- background: '{primary.950}',
626
- focusBackground: '{primary.700}',
627
- color: '#ffffff',
628
- focusColor: '#ffffff',
629
- },
630
- },
631
- dark: {
632
- primary: {
633
- color: '{primary.50}',
634
- contrastColor: '{primary.950}',
635
- hoverColor: '{primary.200}',
636
- activeColor: '{primary.300}',
637
- },
638
- highlight: {
639
- background: '{primary.50}',
640
- focusBackground: '{primary.300}',
641
- color: '{primary.950}',
642
- focusColor: '{primary.950}',
643
- },
644
- },
645
- },
646
- },
647
- };
648
- }
649
- else {
650
- if (preset === 'Nora') {
651
- return {
652
- semantic: {
653
- primary: color.palette,
654
- colorScheme: {
655
- light: {
656
- primary: {
657
- color: '{primary.600}',
658
- contrastColor: '#ffffff',
659
- hoverColor: '{primary.700}',
660
- activeColor: '{primary.800}',
661
- },
662
- highlight: {
663
- background: '{primary.600}',
664
- focusBackground: '{primary.700}',
665
- color: '#ffffff',
666
- focusColor: '#ffffff',
667
- },
668
- },
669
- dark: {
670
- primary: {
671
- color: '{primary.500}',
672
- contrastColor: '{surface.900}',
673
- hoverColor: '{primary.400}',
674
- activeColor: '{primary.300}',
675
- },
676
- highlight: {
677
- background: '{primary.500}',
678
- focusBackground: '{primary.400}',
679
- color: '{surface.900}',
680
- focusColor: '{surface.900}',
681
- },
682
- },
683
- },
684
- },
685
- };
686
- }
687
- else {
688
- return {
689
- semantic: {
690
- primary: color.palette,
691
- colorScheme: {
692
- light: {
693
- primary: {
694
- color: '{primary.500}',
695
- contrastColor: '#ffffff',
696
- hoverColor: '{primary.600}',
697
- activeColor: '{primary.700}',
698
- },
699
- highlight: {
700
- background: '{primary.50}',
701
- focusBackground: '{primary.100}',
702
- color: '{primary.700}',
703
- focusColor: '{primary.800}',
704
- },
705
- },
706
- dark: {
707
- primary: {
708
- color: '{primary.400}',
709
- contrastColor: '{surface.900}',
710
- hoverColor: '{primary.300}',
711
- activeColor: '{primary.200}',
712
- },
713
- highlight: {
714
- background: 'color-mix(in srgb, {primary.400}, transparent 84%)',
715
- focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)',
716
- color: 'rgba(255,255,255,.87)',
717
- focusColor: 'rgba(255,255,255,.87)',
718
- },
719
- },
720
- },
721
- },
722
- };
723
- }
468
+ for (const [name, pal] of Object.entries(PRIMARY_PALETTES)) {
469
+ palettes.push({ name, palette: pal });
724
470
  }
725
- }
471
+ return palettes;
472
+ }, /* @ts-ignore */
473
+ ...(ngDevMode ? [{ debugName: "primaryColors" }] : /* istanbul ignore next */ []));
726
474
  updateColors(event, type, color) {
727
475
  if (type === 'primary') {
728
476
  this.layoutService.updateLayoutConfig({ primary: color.name });
@@ -730,291 +478,727 @@ class AppConfigurator {
730
478
  else if (type === 'surface') {
731
479
  this.layoutService.updateLayoutConfig({ surface: color.name });
732
480
  }
733
- this.applyTheme(type, color);
481
+ this._applyColorChange(type, color);
734
482
  event.stopPropagation();
735
483
  }
736
- applyTheme(type, color) {
484
+ _applyColorChange(type, color) {
485
+ const isDark = this.layoutService.layoutConfig().darkTheme ?? false;
737
486
  if (type === 'primary') {
738
- 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);
739
493
  }
740
494
  else if (type === 'surface') {
741
- updateSurfacePalette(color.palette);
495
+ const surfacePal = SURFACE_PALETTES[color.name ?? 'slate'];
496
+ if (surfacePal)
497
+ applySurfacePalette(surfacePal);
742
498
  }
743
499
  }
744
500
  onPresetChange(event) {
745
501
  this.layoutService.updateLayoutConfig({ preset: event });
746
- const preset = presets[event];
747
- // Use selected surface or default (slate for light, zinc for dark)
748
- const selectedSurface = this.selectedSurfaceColor();
749
- const defaultSurface = this.layoutService.layoutConfig().darkTheme ? 'zinc' : 'slate';
750
- const surfacePalette = this.surfaces.find((s) => s.name === (selectedSurface || defaultSurface))?.palette;
751
- $t()
752
- .preset(preset)
753
- .preset(this.getPresetExt())
754
- .surfacePalette(surfacePalette)
755
- .use({ useDefaultOptions: true });
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 });
756
506
  }
757
507
  onMenuModeChange(event) {
758
508
  this.layoutService.updateLayoutConfig({ menuMode: event });
759
509
  }
760
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
761
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppConfigurator, isStandalone: true, selector: "app-configurator", host: { styleAttribute: "background-color: var(--surface-overlay)", classAttribute: "hidden absolute top-[3.25rem] right-0 w-[calc(100vw-2rem)] sm:w-72 max-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: `
510
+ translate(key) {
511
+ return this.translateAdapter?.translate(key) ?? key;
512
+ }
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: `
762
515
  <div class="flex flex-col gap-4">
763
516
  <div>
764
- <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>
765
520
  <div class="pt-2 flex gap-2 flex-wrap justify-start">
766
521
  @for (primaryColor of primaryColors(); track primaryColor.name) {
767
522
  <button
768
523
  type="button"
769
524
  [title]="primaryColor.name"
770
525
  (click)="updateColors($event, 'primary', primaryColor)"
771
- [ngClass]="{
772
- 'outline-primary': primaryColor.name === selectedPrimaryColor()
773
- }"
526
+ [class.outline-primary]="primaryColor.name === selectedPrimaryColor()"
774
527
  class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
775
- [style]="{
776
- 'background-color': primaryColor?.name === 'noir' ? 'var(--text-color)' : primaryColor?.palette?.['500']
777
- }"
528
+ [style.background-color]="
529
+ primaryColor?.name === 'noir'
530
+ ? 'var(--f-text-color)'
531
+ : primaryColor?.palette?.['500']
532
+ "
778
533
  ></button>
779
534
  }
780
535
  </div>
781
536
  </div>
782
537
  <div>
783
- <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>
784
541
  <div class="pt-2 flex gap-2 flex-wrap justify-start">
785
542
  @for (surface of surfaces; track surface.name) {
786
543
  <button
787
544
  type="button"
788
545
  [title]="surface.name"
789
546
  (click)="updateColors($event, 'surface', surface)"
790
- [ngClass]="{
791
- 'outline-primary': selectedSurfaceColor()
792
- ? selectedSurfaceColor() === surface.name
793
- : layoutService.layoutConfig().darkTheme
794
- ? surface.name === 'zinc'
795
- : surface.name === 'slate'
796
- }"
547
+ [class.outline-primary]="isSurfaceSelected(surface.name)"
797
548
  class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
798
- [style]="{
799
- 'background-color': surface?.name === 'noir' ? 'var(--text-color)' : surface?.palette?.['500']
800
- }"
549
+ [style.background-color]="
550
+ surface?.name === 'noir'
551
+ ? 'var(--f-text-color)'
552
+ : surface?.palette?.['500']
553
+ "
801
554
  ></button>
802
555
  }
803
556
  </div>
804
557
  </div>
805
558
  <div class="flex flex-col gap-2">
806
- <span class="text-sm text-muted-color font-semibold">Presets</span>
807
- <p-selectbutton
559
+ <span class="text-sm text-muted-color font-semibold">{{
560
+ 'layout.configurator.presets' | translate
561
+ }}</span>
562
+ <f-selectbutton
808
563
  [options]="presets"
809
564
  [ngModel]="selectedPreset()"
810
565
  (ngModelChange)="onPresetChange($event)"
811
566
  [allowEmpty]="false"
812
567
  size="small"
813
- />
568
+ />
814
569
  </div>
815
570
  @if (showMenuModeButton()) {
816
571
  <div class="flex flex-col gap-2">
817
- <span class="text-sm text-muted-color font-semibold">Menu Mode</span>
818
- <p-selectbutton
572
+ <span class="text-sm text-muted-color font-semibold">{{
573
+ 'layout.configurator.menu.mode' | translate
574
+ }}</span>
575
+ <f-selectbutton
819
576
  [ngModel]="menuMode()"
820
577
  (ngModelChange)="onMenuModeChange($event)"
821
- [options]="menuModeOptions"
578
+ [options]="menuModeOptions()"
822
579
  [allowEmpty]="false"
823
580
  size="small"
824
- />
581
+ />
825
582
  </div>
826
583
  }
827
584
  </div>
828
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i2.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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" }] });
829
586
  }
830
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppConfigurator, decorators: [{
587
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppConfigurator, decorators: [{
831
588
  type: Component,
832
589
  args: [{
833
590
  selector: 'app-configurator',
834
- changeDetection: ChangeDetectionStrategy.OnPush,
835
- imports: [NgClass, FormsModule, SelectButtonModule],
591
+ imports: [UiModule, FormsModule, TranslatePipe],
836
592
  template: `
837
593
  <div class="flex flex-col gap-4">
838
594
  <div>
839
- <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>
840
598
  <div class="pt-2 flex gap-2 flex-wrap justify-start">
841
599
  @for (primaryColor of primaryColors(); track primaryColor.name) {
842
600
  <button
843
601
  type="button"
844
602
  [title]="primaryColor.name"
845
603
  (click)="updateColors($event, 'primary', primaryColor)"
846
- [ngClass]="{
847
- 'outline-primary': primaryColor.name === selectedPrimaryColor()
848
- }"
604
+ [class.outline-primary]="primaryColor.name === selectedPrimaryColor()"
849
605
  class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
850
- [style]="{
851
- 'background-color': primaryColor?.name === 'noir' ? 'var(--text-color)' : primaryColor?.palette?.['500']
852
- }"
606
+ [style.background-color]="
607
+ primaryColor?.name === 'noir'
608
+ ? 'var(--f-text-color)'
609
+ : primaryColor?.palette?.['500']
610
+ "
853
611
  ></button>
854
612
  }
855
613
  </div>
856
614
  </div>
857
615
  <div>
858
- <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>
859
619
  <div class="pt-2 flex gap-2 flex-wrap justify-start">
860
620
  @for (surface of surfaces; track surface.name) {
861
621
  <button
862
622
  type="button"
863
623
  [title]="surface.name"
864
624
  (click)="updateColors($event, 'surface', surface)"
865
- [ngClass]="{
866
- 'outline-primary': selectedSurfaceColor()
867
- ? selectedSurfaceColor() === surface.name
868
- : layoutService.layoutConfig().darkTheme
869
- ? surface.name === 'zinc'
870
- : surface.name === 'slate'
871
- }"
625
+ [class.outline-primary]="isSurfaceSelected(surface.name)"
872
626
  class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
873
- [style]="{
874
- 'background-color': surface?.name === 'noir' ? 'var(--text-color)' : surface?.palette?.['500']
875
- }"
627
+ [style.background-color]="
628
+ surface?.name === 'noir'
629
+ ? 'var(--f-text-color)'
630
+ : surface?.palette?.['500']
631
+ "
876
632
  ></button>
877
633
  }
878
634
  </div>
879
635
  </div>
880
636
  <div class="flex flex-col gap-2">
881
- <span class="text-sm text-muted-color font-semibold">Presets</span>
882
- <p-selectbutton
637
+ <span class="text-sm text-muted-color font-semibold">{{
638
+ 'layout.configurator.presets' | translate
639
+ }}</span>
640
+ <f-selectbutton
883
641
  [options]="presets"
884
642
  [ngModel]="selectedPreset()"
885
643
  (ngModelChange)="onPresetChange($event)"
886
644
  [allowEmpty]="false"
887
645
  size="small"
888
- />
646
+ />
889
647
  </div>
890
648
  @if (showMenuModeButton()) {
891
649
  <div class="flex flex-col gap-2">
892
- <span class="text-sm text-muted-color font-semibold">Menu Mode</span>
893
- <p-selectbutton
650
+ <span class="text-sm text-muted-color font-semibold">{{
651
+ 'layout.configurator.menu.mode' | translate
652
+ }}</span>
653
+ <f-selectbutton
894
654
  [ngModel]="menuMode()"
895
655
  (ngModelChange)="onMenuModeChange($event)"
896
- [options]="menuModeOptions"
656
+ [options]="menuModeOptions()"
897
657
  [allowEmpty]="false"
898
658
  size="small"
899
- />
659
+ />
900
660
  </div>
901
661
  }
902
662
  </div>
903
- `,
663
+ `,
904
664
  host: {
905
- class: 'hidden absolute top-[3.25rem] right-0 w-[calc(100vw-2rem)] sm:w-72 max-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)]',
906
- 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;',
907
667
  },
908
668
  }]
909
669
  }], ctorParameters: () => [] });
910
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');
679
+
911
680
  class AppFloatingConfigurator {
912
681
  layoutService = inject(LayoutService);
913
- isDarkTheme = computed(() => this.layoutService.layoutConfig().darkTheme, ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : []));
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 */ []));
914
687
  toggleDarkMode() {
915
688
  const currentDarkTheme = this.layoutService.layoutConfig().darkTheme;
916
689
  this.layoutService.updateLayoutConfig({ darkTheme: !currentDarkTheme });
917
690
  }
918
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppFloatingConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
919
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.5", type: AppFloatingConfigurator, isStandalone: true, selector: "app-floating-configurator", ngImport: i0, template: `
920
- <div class="fixed flex flex-col md:flex-row gap-2 md:gap-4 top-4 md:top-8 right-2 md:right-8 z-50">
921
- <p-button type="button" (onClick)="toggleDarkMode()" [rounded]="true" [icon]="isDarkTheme() ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary" />
922
- <div class="relative">
923
- <p-button icon="pi pi-palette" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true" type="button" rounded />
924
- <app-configurator />
925
- </div>
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" />
926
721
  </div>
927
- `, 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" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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" }] });
928
741
  }
929
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppFloatingConfigurator, decorators: [{
742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFloatingConfigurator, decorators: [{
930
743
  type: Component,
931
- args: [{
932
- selector: 'app-floating-configurator',
933
- changeDetection: ChangeDetectionStrategy.OnPush,
934
- imports: [ButtonModule, StyleClassModule, AppConfigurator],
935
- template: `
936
- <div class="fixed flex flex-col md:flex-row gap-2 md:gap-4 top-4 md:top-8 right-2 md:right-8 z-50">
937
- <p-button type="button" (onClick)="toggleDarkMode()" [rounded]="true" [icon]="isDarkTheme() ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary" />
938
- <div class="relative">
939
- <p-button icon="pi pi-palette" pStyleClass="@next" enterFromClass="hidden" enterActiveClass="animate-scalein" leaveToClass="hidden" leaveActiveClass="animate-fadeout" [hideOnOutsideClick]="true" type="button" rounded />
940
- <app-configurator />
941
- </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" />
942
773
  </div>
943
- `
944
- }]
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"] }]
945
793
  }] });
946
794
 
947
- const LAYOUT_AUTH_STATE = new InjectionToken('LAYOUT_AUTH_STATE');
948
- const LAYOUT_AUTH_API = new InjectionToken('LAYOUT_AUTH_API');
949
-
950
- /**
951
- * View Transitions API type definitions
952
- * @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
953
- */
954
-
955
- /** Company/branch switcher displayed in top bar */
956
- class AppCompanyBranchSelector {
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);
805
+ layoutService = inject(LayoutService);
957
806
  destroyRef = inject(DestroyRef);
958
- authState = inject(LAYOUT_AUTH_STATE, { optional: true });
959
- authApi = inject(LAYOUT_AUTH_API, { optional: true });
960
- messageService = inject(MessageService);
961
- document = inject(DOCUMENT$1);
962
- elementRef = inject(ElementRef);
963
- _isActive = signal(false, ...(ngDevMode ? [{ debugName: "_isActive" }] : []));
964
- isActive = this._isActive.asReadonly();
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 */ []));
965
837
  constructor() {
966
- fromEvent(this.document, 'click')
967
- .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
968
- .subscribe((event) => {
969
- const target = event.target;
970
- if (!this.elementRef.nativeElement.contains(target)) {
971
- this._isActive.set(false);
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
+ });
853
+ });
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());
972
877
  }
973
878
  });
974
879
  }
975
- currentCompanyName = computed(() => this.authState?.currentCompanyInfo()?.name ?? 'No Company', ...(ngDevMode ? [{ debugName: "currentCompanyName" }] : []));
976
- currentBranchName = computed(() => this.authState?.currentBranchInfo()?.name ?? null, ...(ngDevMode ? [{ debugName: "currentBranchName" }] : []));
977
- _companies = signal([], ...(ngDevMode ? [{ debugName: "_companies" }] : []));
978
- companies = this._companies.asReadonly();
979
- _branches = signal([], ...(ngDevMode ? [{ debugName: "_branches" }] : []));
980
- branches = this._branches.asReadonly();
981
- _selectedCompanyId = signal(null, ...(ngDevMode ? [{ debugName: "_selectedCompanyId" }] : []));
982
- selectedCompanyId = this._selectedCompanyId.asReadonly();
983
- _selectedBranchId = signal(null, ...(ngDevMode ? [{ debugName: "_selectedBranchId" }] : []));
984
- selectedBranchId = this._selectedBranchId.asReadonly();
985
- _isLoadingCompanies = signal(false, ...(ngDevMode ? [{ debugName: "_isLoadingCompanies" }] : []));
986
- isLoadingCompanies = this._isLoadingCompanies.asReadonly();
987
- _isLoadingBranches = signal(false, ...(ngDevMode ? [{ debugName: "_isLoadingBranches" }] : []));
988
- isLoadingBranches = this._isLoadingBranches.asReadonly();
989
- _isSwitching = signal(false, ...(ngDevMode ? [{ debugName: "_isSwitching" }] : []));
990
- isSwitching = this._isSwitching.asReadonly();
991
- canSwitch = computed(() => {
992
- const selectedCompany = this.selectedCompanyId();
993
- if (!selectedCompany)
994
- return false;
995
- const currentCompanyId = this.authState?.currentCompanyInfo()?.id;
996
- const currentBranchId = this.authState?.currentBranchInfo()?.id;
997
- const selectedBranch = this.selectedBranchId();
998
- return selectedCompany !== currentCompanyId || selectedBranch !== currentBranchId;
999
- }, ...(ngDevMode ? [{ debugName: "canSwitch" }] : []));
1000
- onPanelToggle() {
1001
- this._isActive.update((v) => !v);
1002
- if (this.isActive() && this.companies().length === 0) {
1003
- this.loadCompanies();
880
+ onMouseEnter() {
881
+ if (this.layoutService.isTopbar() && this.item().children) {
882
+ this.updateSubmenuPosition();
1004
883
  }
1005
884
  }
1006
- loadCompanies() {
1007
- if (!this.authApi)
885
+ updateSubmenuPosition() {
886
+ const linkEl = this.parentLink()?.nativeElement;
887
+ if (!linkEl || typeof linkEl.getBoundingClientRect !== 'function')
1008
888
  return;
1009
- this._isLoadingCompanies.set(true);
1010
- this.authApi
1011
- .getUserCompanies()
1012
- .pipe(takeUntilDestroyed(this.destroyRef))
1013
- .subscribe({
1014
- next: (companies) => {
1015
- this._companies.set(companies);
1016
- this._isLoadingCompanies.set(false);
1017
- },
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();
1176
+ if (!selectedCompany)
1177
+ return false;
1178
+ const currentCompanyId = this.authState?.currentCompanyInfo()?.id;
1179
+ const currentBranchId = this.authState?.currentBranchInfo()?.id;
1180
+ const selectedBranch = this.selectedBranchId();
1181
+ return (selectedCompany !== currentCompanyId || selectedBranch !== currentBranchId);
1182
+ }, /* @ts-ignore */
1183
+ ...(ngDevMode ? [{ debugName: "canSwitch" }] : /* istanbul ignore next */ []));
1184
+ onPanelToggle() {
1185
+ this._isActive.update((v) => !v);
1186
+ if (this.isActive() && this.companies().length === 0) {
1187
+ this.loadCompanies();
1188
+ }
1189
+ }
1190
+ loadCompanies() {
1191
+ if (!this.authApi)
1192
+ return;
1193
+ this._isLoadingCompanies.set(true);
1194
+ this.authApi
1195
+ .getUserCompanies()
1196
+ .pipe(takeUntilDestroyed(this.destroyRef))
1197
+ .subscribe({
1198
+ next: (companies) => {
1199
+ this._companies.set(companies);
1200
+ this._isLoadingCompanies.set(false);
1201
+ },
1018
1202
  error: () => {
1019
1203
  // Error toast handled by global interceptor
1020
1204
  this._isLoadingCompanies.set(false);
@@ -1064,8 +1248,8 @@ class AppCompanyBranchSelector {
1064
1248
  if (this.branches().length > 0 && !selectedBranch) {
1065
1249
  this.messageService.add({
1066
1250
  severity: 'warn',
1067
- summary: 'Branch Required',
1068
- 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'),
1069
1253
  });
1070
1254
  return;
1071
1255
  }
@@ -1081,13 +1265,16 @@ class AppCompanyBranchSelector {
1081
1265
  },
1082
1266
  });
1083
1267
  }
1084
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppCompanyBranchSelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
1085
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", 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: `
1086
1273
  <button
1087
1274
  type="button"
1088
1275
  class="layout-topbar-action"
1089
1276
  [class.layout-topbar-action-highlight]="isActive()"
1090
- pStyleClass="@next"
1277
+ fStyleClass="@next"
1091
1278
  enterFromClass="hidden"
1092
1279
  enterActiveClass="animate-scalein"
1093
1280
  leaveToClass="hidden"
@@ -1095,31 +1282,35 @@ class AppCompanyBranchSelector {
1095
1282
  [hideOnOutsideClick]="true"
1096
1283
  (click)="onPanelToggle()"
1097
1284
  >
1098
- <i class="pi pi-building"></i>
1285
+ <ui-icon name="building"></ui-icon>
1099
1286
  <span>{{ currentCompanyName() }}</span>
1100
1287
  @if (currentBranchName()) {
1101
- <span class="text-xs opacity-70"> | {{ currentBranchName() }}</span>
1288
+ <span class="text-xs opacity-70 ml-1">| {{ currentBranchName() }}</span>
1102
1289
  }
1103
1290
  </button>
1104
1291
  <div
1105
- 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)] w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[300px] max-w-[360px]"
1106
- 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;"
1107
1294
  >
1108
1295
  <div class="flex flex-col gap-4">
1109
- <span class="text-sm text-muted-color font-semibold"
1110
- >Switch Company & Branch</span
1111
- >
1296
+ <span class="text-sm text-muted-color font-semibold">{{
1297
+ 'layout.company.branch.selector.title' | translate
1298
+ }}</span>
1112
1299
 
1113
1300
  <!-- Company Selector -->
1114
1301
  <div class="flex flex-col gap-2">
1115
- <label class="text-sm text-muted-color font-semibold">Company</label>
1116
- <p-select
1302
+ <label class="text-sm text-muted-color font-semibold">{{
1303
+ 'shared.company' | translate
1304
+ }}</label>
1305
+ <f-select
1117
1306
  [options]="companies()"
1118
1307
  [ngModel]="selectedCompanyId()"
1119
1308
  (ngModelChange)="onCompanyChange($event)"
1120
1309
  optionLabel="name"
1121
1310
  optionValue="id"
1122
- placeholder="Select Company"
1311
+ [placeholder]="
1312
+ 'layout.company.branch.selector.select.company' | translate
1313
+ "
1123
1314
  class="w-full"
1124
1315
  [loading]="isLoadingCompanies()"
1125
1316
  />
@@ -1128,14 +1319,18 @@ class AppCompanyBranchSelector {
1128
1319
  <!-- Branch Selector -->
1129
1320
  @if (selectedCompanyId() && branches().length > 0) {
1130
1321
  <div class="flex flex-col gap-2">
1131
- <label class="text-sm text-muted-color font-semibold">Branch</label>
1132
- <p-select
1322
+ <label class="text-sm text-muted-color font-semibold">{{
1323
+ 'layout.company.branch.selector.branch' | translate
1324
+ }}</label>
1325
+ <f-select
1133
1326
  [options]="branches()"
1134
1327
  [ngModel]="selectedBranchId()"
1135
1328
  (ngModelChange)="onBranchChange($event)"
1136
1329
  optionLabel="name"
1137
1330
  optionValue="id"
1138
- placeholder="Select Branch"
1331
+ [placeholder]="
1332
+ 'layout.company.branch.selector.select.branch' | translate
1333
+ "
1139
1334
  class="w-full"
1140
1335
  [loading]="isLoadingBranches()"
1141
1336
  />
@@ -1144,32 +1339,37 @@ class AppCompanyBranchSelector {
1144
1339
 
1145
1340
  <!-- Actions -->
1146
1341
  <div class="flex gap-2 pt-2">
1147
- <p-button
1148
- label="Switch"
1149
- icon="pi pi-sync"
1150
- styleClass="flex-1"
1151
- [loading]="isSwitching()"
1152
- [disabled]="!canSwitch()"
1153
- (onClick)="onSwitch()"
1154
- />
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>
1155
1355
  </div>
1156
1356
  </div>
1157
1357
  </div>
1158
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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" }] });
1159
1359
  }
1160
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppCompanyBranchSelector, decorators: [{
1360
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppCompanyBranchSelector, decorators: [{
1161
1361
  type: Component,
1162
1362
  args: [{
1163
1363
  selector: 'app-company-branch-selector',
1164
- changeDetection: ChangeDetectionStrategy.OnPush,
1165
- imports: [AngularModule, StyleClassModule, ButtonModule, SelectModule],
1364
+ imports: [UiModule, AngularModule,
1365
+ TranslatePipe,],
1166
1366
  host: { class: 'relative' },
1167
1367
  template: `
1168
1368
  <button
1169
1369
  type="button"
1170
1370
  class="layout-topbar-action"
1171
1371
  [class.layout-topbar-action-highlight]="isActive()"
1172
- pStyleClass="@next"
1372
+ fStyleClass="@next"
1173
1373
  enterFromClass="hidden"
1174
1374
  enterActiveClass="animate-scalein"
1175
1375
  leaveToClass="hidden"
@@ -1177,31 +1377,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1177
1377
  [hideOnOutsideClick]="true"
1178
1378
  (click)="onPanelToggle()"
1179
1379
  >
1180
- <i class="pi pi-building"></i>
1380
+ <ui-icon name="building"></ui-icon>
1181
1381
  <span>{{ currentCompanyName() }}</span>
1182
1382
  @if (currentBranchName()) {
1183
- <span class="text-xs opacity-70"> | {{ currentBranchName() }}</span>
1383
+ <span class="text-xs opacity-70 ml-1">| {{ currentBranchName() }}</span>
1184
1384
  }
1185
1385
  </button>
1186
1386
  <div
1187
- 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)] w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[300px] max-w-[360px]"
1188
- 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;"
1189
1389
  >
1190
1390
  <div class="flex flex-col gap-4">
1191
- <span class="text-sm text-muted-color font-semibold"
1192
- >Switch Company & Branch</span
1193
- >
1391
+ <span class="text-sm text-muted-color font-semibold">{{
1392
+ 'layout.company.branch.selector.title' | translate
1393
+ }}</span>
1194
1394
 
1195
1395
  <!-- Company Selector -->
1196
1396
  <div class="flex flex-col gap-2">
1197
- <label class="text-sm text-muted-color font-semibold">Company</label>
1198
- <p-select
1397
+ <label class="text-sm text-muted-color font-semibold">{{
1398
+ 'shared.company' | translate
1399
+ }}</label>
1400
+ <f-select
1199
1401
  [options]="companies()"
1200
1402
  [ngModel]="selectedCompanyId()"
1201
1403
  (ngModelChange)="onCompanyChange($event)"
1202
1404
  optionLabel="name"
1203
1405
  optionValue="id"
1204
- placeholder="Select Company"
1406
+ [placeholder]="
1407
+ 'layout.company.branch.selector.select.company' | translate
1408
+ "
1205
1409
  class="w-full"
1206
1410
  [loading]="isLoadingCompanies()"
1207
1411
  />
@@ -1210,14 +1414,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1210
1414
  <!-- Branch Selector -->
1211
1415
  @if (selectedCompanyId() && branches().length > 0) {
1212
1416
  <div class="flex flex-col gap-2">
1213
- <label class="text-sm text-muted-color font-semibold">Branch</label>
1214
- <p-select
1417
+ <label class="text-sm text-muted-color font-semibold">{{
1418
+ 'layout.company.branch.selector.branch' | translate
1419
+ }}</label>
1420
+ <f-select
1215
1421
  [options]="branches()"
1216
1422
  [ngModel]="selectedBranchId()"
1217
1423
  (ngModelChange)="onBranchChange($event)"
1218
1424
  optionLabel="name"
1219
1425
  optionValue="id"
1220
- placeholder="Select Branch"
1426
+ [placeholder]="
1427
+ 'layout.company.branch.selector.select.branch' | translate
1428
+ "
1221
1429
  class="w-full"
1222
1430
  [loading]="isLoadingBranches()"
1223
1431
  />
@@ -1226,14 +1434,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1226
1434
 
1227
1435
  <!-- Actions -->
1228
1436
  <div class="flex gap-2 pt-2">
1229
- <p-button
1230
- label="Switch"
1231
- icon="pi pi-sync"
1232
- styleClass="flex-1"
1233
- [loading]="isSwitching()"
1234
- [disabled]="!canSwitch()"
1235
- (onClick)="onSwitch()"
1236
- />
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>
1237
1450
  </div>
1238
1451
  </div>
1239
1452
  </div>
@@ -1242,15 +1455,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1242
1455
  }], ctorParameters: () => [] });
1243
1456
 
1244
1457
  class AppLauncher {
1458
+ layoutService = inject(LayoutService);
1245
1459
  destroyRef = inject(DestroyRef);
1246
1460
  document = inject(DOCUMENT$1);
1247
1461
  elementRef = inject(ElementRef);
1248
- layoutService = inject(LayoutService);
1249
- _isActive = signal(false, ...(ngDevMode ? [{ debugName: "_isActive" }] : []));
1462
+ _isActive = signal(false, /* @ts-ignore */
1463
+ ...(ngDevMode ? [{ debugName: "_isActive" }] : /* istanbul ignore next */ []));
1250
1464
  isActive = this._isActive.asReadonly();
1251
1465
  constructor() {
1252
1466
  fromEvent(this.document, 'click')
1253
- .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this._isActive()))
1467
+ .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1254
1468
  .subscribe((event) => {
1255
1469
  const target = event.target;
1256
1470
  if (!this.elementRef.nativeElement.contains(target)) {
@@ -1258,114 +1472,129 @@ class AppLauncher {
1258
1472
  }
1259
1473
  });
1260
1474
  }
1261
- togglePanel() {
1475
+ toggleActive() {
1262
1476
  this._isActive.update((v) => !v);
1263
1477
  }
1264
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppLauncher, deps: [], target: i0.ɵɵFactoryTarget.Component });
1265
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", 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: `
1266
1480
  <button
1267
1481
  type="button"
1268
1482
  class="layout-topbar-action"
1269
1483
  [class.layout-topbar-action-highlight]="isActive()"
1270
- pStyleClass="@next"
1484
+ fStyleClass="@next"
1271
1485
  enterFromClass="hidden"
1272
1486
  enterActiveClass="animate-scalein"
1273
1487
  leaveToClass="hidden"
1274
1488
  leaveActiveClass="animate-fadeout"
1275
1489
  [hideOnOutsideClick]="true"
1276
- (click)="togglePanel()"
1490
+ (click)="toggleActive()"
1277
1491
  >
1278
- <i class="pi pi-th-large"></i>
1279
- <span>Apps</span>
1492
+ <ui-icon name="th-large"></ui-icon>
1493
+ <span>{{ 'layout.launcher.apps' | translate }}</span>
1280
1494
  </button>
1281
1495
  <div
1282
- 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)] w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]"
1283
- 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);"
1284
1498
  >
1285
- <div class="flex flex-col gap-2">
1286
- <span class="text-sm text-muted-color font-semibold">Applications</span>
1287
- <div class="grid grid-cols-2 sm: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">
1288
1510
  @for (app of layoutService.apps(); track app.id) {
1289
1511
  <a
1290
- class="group flex flex-col items-center 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"
1291
1513
  [href]="app.url"
1292
1514
  target="_blank"
1293
1515
  rel="noopener noreferrer"
1294
1516
  >
1295
1517
  <div
1296
- 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"
1297
1519
  >
1298
- <lib-icon
1299
- [icon]="app.icon"
1300
- [iconType]="app.iconType"
1301
- class="text-xl text-primary group-hover:text-primary-contrast transition-colors duration-200"
1302
- />
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>
1303
1524
  </div>
1304
1525
  <span
1305
- class="text-xs mt-2 text-center text-muted-color group-hover:text-color transition-colors duration-200 truncate max-w-full"
1306
- >{{ 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"
1307
1527
  >
1528
+ {{ app.nameKey ? (app.nameKey | translate) : app.name }}
1529
+ </span>
1308
1530
  </a>
1309
1531
  }
1310
1532
  </div>
1311
- </div>
1533
+ }
1312
1534
  </div>
1313
- `, 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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" }] });
1314
1536
  }
1315
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppLauncher, decorators: [{
1537
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLauncher, decorators: [{
1316
1538
  type: Component,
1317
1539
  args: [{
1318
1540
  selector: 'app-launcher',
1319
- changeDetection: ChangeDetectionStrategy.OnPush,
1320
- imports: [StyleClassModule, IconComponent],
1541
+ imports: [UiModule, TranslatePipe],
1321
1542
  host: { class: 'relative' },
1322
1543
  template: `
1323
1544
  <button
1324
1545
  type="button"
1325
1546
  class="layout-topbar-action"
1326
1547
  [class.layout-topbar-action-highlight]="isActive()"
1327
- pStyleClass="@next"
1548
+ fStyleClass="@next"
1328
1549
  enterFromClass="hidden"
1329
1550
  enterActiveClass="animate-scalein"
1330
1551
  leaveToClass="hidden"
1331
1552
  leaveActiveClass="animate-fadeout"
1332
1553
  [hideOnOutsideClick]="true"
1333
- (click)="togglePanel()"
1554
+ (click)="toggleActive()"
1334
1555
  >
1335
- <i class="pi pi-th-large"></i>
1336
- <span>Apps</span>
1556
+ <ui-icon name="th-large"></ui-icon>
1557
+ <span>{{ 'layout.launcher.apps' | translate }}</span>
1337
1558
  </button>
1338
1559
  <div
1339
- 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)] w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]"
1340
- 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);"
1341
1562
  >
1342
- <div class="flex flex-col gap-2">
1343
- <span class="text-sm text-muted-color font-semibold">Applications</span>
1344
- <div class="grid grid-cols-2 sm: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">
1345
1574
  @for (app of layoutService.apps(); track app.id) {
1346
1575
  <a
1347
- class="group flex flex-col items-center 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"
1348
1577
  [href]="app.url"
1349
1578
  target="_blank"
1350
1579
  rel="noopener noreferrer"
1351
1580
  >
1352
1581
  <div
1353
- 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"
1354
1583
  >
1355
- <lib-icon
1356
- [icon]="app.icon"
1357
- [iconType]="app.iconType"
1358
- class="text-xl text-primary group-hover:text-primary-contrast transition-colors duration-200"
1359
- />
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>
1360
1588
  </div>
1361
1589
  <span
1362
- class="text-xs mt-2 text-center text-muted-color group-hover:text-color transition-colors duration-200 truncate max-w-full"
1363
- >{{ 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"
1364
1591
  >
1592
+ {{ app.nameKey ? (app.nameKey | translate) : app.name }}
1593
+ </span>
1365
1594
  </a>
1366
1595
  }
1367
1596
  </div>
1368
- </div>
1597
+ }
1369
1598
  </div>
1370
1599
  `,
1371
1600
  }]
@@ -1377,9 +1606,14 @@ class AppProfile {
1377
1606
  authApi = inject(LAYOUT_AUTH_API, { optional: true });
1378
1607
  layoutService = inject(LayoutService);
1379
1608
  messageService = inject(MessageService);
1609
+ translateAdapter = inject(TranslateService, {
1610
+ optional: true,
1611
+ });
1612
+ appConfig = inject(APP_CONFIG, { optional: true });
1380
1613
  document = inject(DOCUMENT$1);
1381
1614
  elementRef = inject(ElementRef);
1382
- _isActive = signal(false, ...(ngDevMode ? [{ debugName: "_isActive" }] : []));
1615
+ _isActive = signal(false, /* @ts-ignore */
1616
+ ...(ngDevMode ? [{ debugName: "_isActive" }] : /* istanbul ignore next */ []));
1383
1617
  isActive = this._isActive.asReadonly();
1384
1618
  constructor() {
1385
1619
  fromEvent(this.document, 'click')
@@ -1394,9 +1628,15 @@ class AppProfile {
1394
1628
  togglePanel() {
1395
1629
  this._isActive.update((v) => !v);
1396
1630
  }
1397
- userName = computed(() => this.authState?.loginUserData()?.name ?? 'Guest', ...(ngDevMode ? [{ debugName: "userName" }] : []));
1398
- userEmail = computed(() => this.authState?.loginUserData()?.email ?? '', ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
1399
- 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 */ []));
1400
1640
  logout() {
1401
1641
  if (!this.authApi)
1402
1642
  return;
@@ -1405,10 +1645,13 @@ class AppProfile {
1405
1645
  .pipe(takeUntilDestroyed(this.destroyRef))
1406
1646
  .subscribe({
1407
1647
  next: (result) => {
1648
+ const r = result;
1408
1649
  this.messageService.add({
1409
1650
  severity: 'success',
1410
- summary: 'Success!',
1411
- detail: result.message,
1651
+ summary: this.translate('shared.success'),
1652
+ detail: r?.messageKey
1653
+ ? this.translate(r.messageKey, r.messageVariables)
1654
+ : (r?.message ?? ''),
1412
1655
  });
1413
1656
  this.authApi?.navigateLogin(false);
1414
1657
  },
@@ -1425,25 +1668,28 @@ class AppProfile {
1425
1668
  .then(() => {
1426
1669
  this.messageService.add({
1427
1670
  severity: 'success',
1428
- summary: 'Link Copied',
1429
- detail: 'Sign Up Link copied.',
1671
+ summary: this.translate('layout.profile.link.copied'),
1672
+ detail: this.translate('layout.profile.sign.up.link.copied'),
1430
1673
  });
1431
1674
  })
1432
1675
  .catch(() => {
1433
1676
  this.messageService.add({
1434
1677
  severity: 'error',
1435
- summary: 'Sorry',
1436
- detail: 'Failed to copy signup link.',
1678
+ summary: this.translate('shared.error'),
1679
+ detail: this.translate('layout.profile.failed.copy.sign.up.link'),
1437
1680
  });
1438
1681
  });
1439
1682
  }
1440
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppProfile, deps: [], target: i0.ɵɵFactoryTarget.Component });
1441
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", 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: `
1442
1688
  <button
1443
1689
  type="button"
1444
1690
  class="layout-topbar-action"
1445
1691
  [class.layout-topbar-action-highlight]="isActive()"
1446
- pStyleClass="@next"
1692
+ fStyleClass="@next"
1447
1693
  enterFromClass="hidden"
1448
1694
  enterActiveClass="animate-scalein"
1449
1695
  leaveToClass="hidden"
@@ -1451,12 +1697,12 @@ class AppProfile {
1451
1697
  [hideOnOutsideClick]="true"
1452
1698
  (click)="togglePanel()"
1453
1699
  >
1454
- <i class="pi pi-user"></i>
1455
- <span>Profile</span>
1700
+ <ui-icon name="user"></ui-icon>
1701
+ <span>{{ 'layout.profile.title' | translate }}</span>
1456
1702
  </button>
1457
1703
  <div
1458
- 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)] w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]"
1459
- 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;"
1460
1706
  >
1461
1707
  <div class="flex flex-col gap-3">
1462
1708
  <!-- User Info -->
@@ -1468,18 +1714,22 @@ class AppProfile {
1468
1714
  <img
1469
1715
  [src]="profilePicture()"
1470
1716
  class="w-12 h-12 rounded-border object-cover"
1471
- alt="Profile"
1717
+ [alt]="'layout.profile.profile.picture.alt' | translate"
1472
1718
  />
1473
1719
  } @else {
1474
1720
  <div
1475
1721
  class="w-12 h-12 rounded-border bg-primary flex items-center justify-center"
1476
1722
  >
1477
- <i class="pi pi-user text-xl text-primary-contrast"></i>
1723
+ <ui-icon name="user" class="text-xl text-primary-contrast"></ui-icon>
1478
1724
  </div>
1479
1725
  }
1480
1726
  <div class="flex flex-col min-w-0 flex-1">
1481
- <span class="font-semibold text-color truncate">{{ userName() }}</span>
1482
- <span class="text-sm text-muted-color truncate">{{ userEmail() }}</span>
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>
1483
1733
  </div>
1484
1734
  </a>
1485
1735
 
@@ -1488,38 +1738,39 @@ class AppProfile {
1488
1738
 
1489
1739
  <!-- Menu Items -->
1490
1740
  <div class="flex flex-col gap-1">
1491
- <a
1492
- class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1493
- (click)="copySignUpLink()"
1494
- >
1495
- <i class="pi pi-link text-muted-color"></i>
1496
- <span>Copy SignUp Link</span>
1497
- </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
+ }
1498
1750
  <a
1499
1751
  class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1500
1752
  (click)="logout()"
1501
1753
  >
1502
- <i class="pi pi-sign-out text-muted-color"></i>
1503
- <span>Logout</span>
1754
+ <ui-icon name="sign-out" class="text-muted-color"></ui-icon>
1755
+ <span>{{ 'layout.profile.logout' | translate }}</span>
1504
1756
  </a>
1505
1757
  </div>
1506
1758
  </div>
1507
1759
  </div>
1508
- `, 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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" }] });
1509
1761
  }
1510
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppProfile, decorators: [{
1762
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppProfile, decorators: [{
1511
1763
  type: Component,
1512
1764
  args: [{
1513
1765
  selector: 'app-profile',
1514
- changeDetection: ChangeDetectionStrategy.OnPush,
1515
- imports: [AngularModule, StyleClassModule],
1766
+ imports: [UiModule, AngularModule, TranslatePipe],
1516
1767
  host: { class: 'relative' },
1517
1768
  template: `
1518
1769
  <button
1519
1770
  type="button"
1520
1771
  class="layout-topbar-action"
1521
1772
  [class.layout-topbar-action-highlight]="isActive()"
1522
- pStyleClass="@next"
1773
+ fStyleClass="@next"
1523
1774
  enterFromClass="hidden"
1524
1775
  enterActiveClass="animate-scalein"
1525
1776
  leaveToClass="hidden"
@@ -1527,12 +1778,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1527
1778
  [hideOnOutsideClick]="true"
1528
1779
  (click)="togglePanel()"
1529
1780
  >
1530
- <i class="pi pi-user"></i>
1531
- <span>Profile</span>
1781
+ <ui-icon name="user"></ui-icon>
1782
+ <span>{{ 'layout.profile.title' | translate }}</span>
1532
1783
  </button>
1533
1784
  <div
1534
- 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)] w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[280px] max-w-[320px]"
1535
- 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;"
1536
1787
  >
1537
1788
  <div class="flex flex-col gap-3">
1538
1789
  <!-- User Info -->
@@ -1544,18 +1795,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1544
1795
  <img
1545
1796
  [src]="profilePicture()"
1546
1797
  class="w-12 h-12 rounded-border object-cover"
1547
- alt="Profile"
1798
+ [alt]="'layout.profile.profile.picture.alt' | translate"
1548
1799
  />
1549
1800
  } @else {
1550
1801
  <div
1551
1802
  class="w-12 h-12 rounded-border bg-primary flex items-center justify-center"
1552
1803
  >
1553
- <i class="pi pi-user text-xl text-primary-contrast"></i>
1804
+ <ui-icon name="user" class="text-xl text-primary-contrast"></ui-icon>
1554
1805
  </div>
1555
1806
  }
1556
1807
  <div class="flex flex-col min-w-0 flex-1">
1557
- <span class="font-semibold text-color truncate">{{ userName() }}</span>
1558
- <span class="text-sm text-muted-color truncate">{{ userEmail() }}</span>
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>
1559
1814
  </div>
1560
1815
  </a>
1561
1816
 
@@ -1564,19 +1819,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1564
1819
 
1565
1820
  <!-- Menu Items -->
1566
1821
  <div class="flex flex-col gap-1">
1567
- <a
1568
- class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1569
- (click)="copySignUpLink()"
1570
- >
1571
- <i class="pi pi-link text-muted-color"></i>
1572
- <span>Copy SignUp Link</span>
1573
- </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
+ }
1574
1831
  <a
1575
1832
  class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1576
1833
  (click)="logout()"
1577
1834
  >
1578
- <i class="pi pi-sign-out text-muted-color"></i>
1579
- <span>Logout</span>
1835
+ <ui-icon name="sign-out" class="text-muted-color"></ui-icon>
1836
+ <span>{{ 'layout.profile.logout' | translate }}</span>
1580
1837
  </a>
1581
1838
  </div>
1582
1839
  </div>
@@ -1585,24 +1842,401 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1585
1842
  }]
1586
1843
  }], ctorParameters: () => [] });
1587
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
+
1588
2200
  class AppTopbar {
1589
2201
  appConfig = inject(APP_CONFIG);
1590
2202
  document = inject(DOCUMENT$1);
1591
2203
  destroyRef = inject(DestroyRef);
1592
2204
  layoutService = inject(LayoutService);
1593
- configContainer = viewChild('configContainer', ...(ngDevMode ? [{ debugName: "configContainer" }] : []));
1594
- 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 */ []));
1595
2222
  constructor() {
1596
2223
  fromEvent(this.document, 'click')
1597
2224
  .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.activePanel() !== null))
1598
2225
  .subscribe((event) => {
1599
2226
  this.handleOutsideClick(event);
1600
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
+ });
1601
2236
  }
1602
2237
  companyName = this.layoutService.companyName;
1603
- enableCompanyFeature = computed(() => {
1604
- return isCompanyFeatureEnabled(this.appConfig);
1605
- }, ...(ngDevMode ? [{ debugName: "enableCompanyFeature" }] : []));
2238
+ displayLogo = this.layoutService.displayLogo;
2239
+ enableCompanyFeature = isCompanyFeatureEnabled(this.appConfig);
1606
2240
  toggleDarkMode() {
1607
2241
  const currentDarkTheme = this.layoutService.layoutConfig().darkTheme;
1608
2242
  this.layoutService.updateLayoutConfig({ darkTheme: !currentDarkTheme });
@@ -1617,38 +2251,90 @@ class AppTopbar {
1617
2251
  this.activePanel.set(null);
1618
2252
  }
1619
2253
  }
1620
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1621
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", 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">
1622
- <div class="layout-topbar-logo-container">
1623
- <button
1624
- class="layout-menu-button layout-topbar-action"
1625
- (click)="layoutService.onMenuToggle()"
1626
- >
1627
- <i class="pi pi-bars"></i>
1628
- </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
+ }
1629
2282
  <a class="layout-topbar-logo" routerLink="/">
1630
- <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>
1631
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
+ }
1632
2305
  </div>
1633
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
+
1634
2319
  <div class="layout-topbar-actions">
2320
+ @if (hasSearchAdapter()) {
2321
+ <app-searchbar />
2322
+ }
1635
2323
  <div class="layout-config-menu">
1636
2324
  <button
1637
2325
  type="button"
1638
2326
  class="layout-topbar-action"
2327
+ [attr.aria-label]="'layout.topbar.toggle.dark.mode' | translate"
1639
2328
  (click)="toggleDarkMode()"
1640
2329
  >
1641
- <i
1642
- class="pi"
1643
- [class.pi-moon]="layoutService.isDarkTheme()"
1644
- [class.pi-sun]="!layoutService.isDarkTheme()"
1645
- ></i>
2330
+ <ui-icon [name]="layoutService.isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
1646
2331
  </button>
1647
2332
  <div class="relative" #configContainer>
1648
2333
  <button
1649
2334
  class="layout-topbar-action"
1650
2335
  [class.layout-topbar-action-highlight]="activePanel() === 'config'"
1651
- pStyleClass="@next"
2336
+ [attr.aria-label]="'layout.topbar.open.theme.settings' | translate"
2337
+ fStyleClass="@next"
1652
2338
  enterFromClass="hidden"
1653
2339
  enterActiveClass="animate-scalein"
1654
2340
  leaveToClass="hidden"
@@ -1656,7 +2342,7 @@ class AppTopbar {
1656
2342
  [hideOnOutsideClick]="true"
1657
2343
  (click)="togglePanel('config')"
1658
2344
  >
1659
- <i class="pi pi-palette"></i>
2345
+ <ui-icon name="palette"></ui-icon>
1660
2346
  </button>
1661
2347
  <app-configurator />
1662
2348
  </div>
@@ -1664,14 +2350,15 @@ class AppTopbar {
1664
2350
 
1665
2351
  <button
1666
2352
  class="layout-topbar-menu-button layout-topbar-action"
1667
- pStyleClass="@next"
2353
+ [attr.aria-label]="'layout.topbar.more.options' | translate"
2354
+ fStyleClass="@next"
1668
2355
  enterFromClass="hidden"
1669
2356
  enterActiveClass="animate-scalein"
1670
2357
  leaveToClass="hidden"
1671
2358
  leaveActiveClass="animate-fadeout"
1672
2359
  [hideOnOutsideClick]="true"
1673
2360
  >
1674
- <i class="pi pi-ellipsis-v"></i>
2361
+ <ui-icon name="ellipsis-v"></ui-icon>
1675
2362
  </button>
1676
2363
 
1677
2364
  <div class="layout-topbar-menu hidden lg:block">
@@ -1679,59 +2366,105 @@ class AppTopbar {
1679
2366
  @if (layoutService.hasApps()) {
1680
2367
  <app-launcher />
1681
2368
  }
1682
- @if (enableCompanyFeature()) {
2369
+ @if (languageSelectorComponent) {
2370
+ <ng-container *ngComponentOutlet="languageSelectorComponent" />
2371
+ }
2372
+ @if (enableCompanyFeature) {
1683
2373
  <app-company-branch-selector />
1684
2374
  }
2375
+ @if (notificationBellComponent) {
2376
+ <ng-container *ngComponentOutlet="notificationBellComponent" />
2377
+ }
1685
2378
  <app-profile />
1686
2379
  </div>
1687
2380
  </div>
1688
2381
  </div>
1689
- </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: 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" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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" }] });
1690
2383
  }
1691
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppTopbar, decorators: [{
2384
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppTopbar, decorators: [{
1692
2385
  type: Component,
1693
2386
  args: [{
1694
2387
  selector: 'app-topbar',
1695
- changeDetection: ChangeDetectionStrategy.OnPush,
1696
- imports: [
1697
- RouterModule,
1698
- StyleClassModule,
2388
+ imports: [UiModule, RouterModule,
2389
+ NgComponentOutlet,
2390
+ TranslatePipe,
2391
+ InlineSvgComponent,
1699
2392
  AppConfigurator,
2393
+ AppMenu,
1700
2394
  AppProfile,
1701
2395
  AppCompanyBranchSelector,
1702
2396
  AppLauncher,
1703
- ],
1704
- template: ` <div class="layout-topbar">
1705
- <div class="layout-topbar-logo-container">
1706
- <button
1707
- class="layout-menu-button layout-topbar-action"
1708
- (click)="layoutService.onMenuToggle()"
1709
- >
1710
- <i class="pi pi-bars"></i>
1711
- </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
+ }
1712
2412
  <a class="layout-topbar-logo" routerLink="/">
1713
- <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>
1714
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
+ }
1715
2435
  </div>
1716
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
+
1717
2449
  <div class="layout-topbar-actions">
2450
+ @if (hasSearchAdapter()) {
2451
+ <app-searchbar />
2452
+ }
1718
2453
  <div class="layout-config-menu">
1719
2454
  <button
1720
2455
  type="button"
1721
2456
  class="layout-topbar-action"
2457
+ [attr.aria-label]="'layout.topbar.toggle.dark.mode' | translate"
1722
2458
  (click)="toggleDarkMode()"
1723
2459
  >
1724
- <i
1725
- class="pi"
1726
- [class.pi-moon]="layoutService.isDarkTheme()"
1727
- [class.pi-sun]="!layoutService.isDarkTheme()"
1728
- ></i>
2460
+ <ui-icon [name]="layoutService.isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
1729
2461
  </button>
1730
2462
  <div class="relative" #configContainer>
1731
2463
  <button
1732
2464
  class="layout-topbar-action"
1733
2465
  [class.layout-topbar-action-highlight]="activePanel() === 'config'"
1734
- pStyleClass="@next"
2466
+ [attr.aria-label]="'layout.topbar.open.theme.settings' | translate"
2467
+ fStyleClass="@next"
1735
2468
  enterFromClass="hidden"
1736
2469
  enterActiveClass="animate-scalein"
1737
2470
  leaveToClass="hidden"
@@ -1739,7 +2472,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1739
2472
  [hideOnOutsideClick]="true"
1740
2473
  (click)="togglePanel('config')"
1741
2474
  >
1742
- <i class="pi pi-palette"></i>
2475
+ <ui-icon name="palette"></ui-icon>
1743
2476
  </button>
1744
2477
  <app-configurator />
1745
2478
  </div>
@@ -1747,14 +2480,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1747
2480
 
1748
2481
  <button
1749
2482
  class="layout-topbar-menu-button layout-topbar-action"
1750
- pStyleClass="@next"
2483
+ [attr.aria-label]="'layout.topbar.more.options' | translate"
2484
+ fStyleClass="@next"
1751
2485
  enterFromClass="hidden"
1752
2486
  enterActiveClass="animate-scalein"
1753
2487
  leaveToClass="hidden"
1754
2488
  leaveActiveClass="animate-fadeout"
1755
2489
  [hideOnOutsideClick]="true"
1756
2490
  >
1757
- <i class="pi pi-ellipsis-v"></i>
2491
+ <ui-icon name="ellipsis-v"></ui-icon>
1758
2492
  </button>
1759
2493
 
1760
2494
  <div class="layout-topbar-menu hidden lg:block">
@@ -1762,294 +2496,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1762
2496
  @if (layoutService.hasApps()) {
1763
2497
  <app-launcher />
1764
2498
  }
1765
- @if (enableCompanyFeature()) {
2499
+ @if (languageSelectorComponent) {
2500
+ <ng-container *ngComponentOutlet="languageSelectorComponent" />
2501
+ }
2502
+ @if (enableCompanyFeature) {
1766
2503
  <app-company-branch-selector />
1767
2504
  }
2505
+ @if (notificationBellComponent) {
2506
+ <ng-container *ngComponentOutlet="notificationBellComponent" />
2507
+ }
1768
2508
  <app-profile />
1769
2509
  </div>
1770
2510
  </div>
1771
2511
  </div>
1772
2512
  </div>`,
1773
2513
  }]
1774
- }], ctorParameters: () => [], propDecorators: { configContainer: [{ type: i0.ViewChild, args: ['configContainer', { isSignal: true }] }] } });
1775
-
1776
- class AppMenuitem {
1777
- // Signal inputs
1778
- item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
1779
- index = input.required(...(ngDevMode ? [{ debugName: "index" }] : []));
1780
- parentKey = input('', ...(ngDevMode ? [{ debugName: "parentKey" }] : []));
1781
- // Injected services
1782
- router = inject(Router);
1783
- layoutService = inject(LayoutService);
1784
- destroyRef = inject(DestroyRef);
1785
- // State signals - private writable + public readonly pattern
1786
- _active = signal(false, ...(ngDevMode ? [{ debugName: "_active" }] : []));
1787
- active = this._active.asReadonly();
1788
- // Computed signals
1789
- key = computed(() => {
1790
- const parent = this.parentKey();
1791
- return parent ? `${parent}-${this.index()}` : String(this.index());
1792
- }, ...(ngDevMode ? [{ debugName: "key" }] : []));
1793
- routerLink = computed(() => {
1794
- const menuItem = this.item();
1795
- return menuItem.routerLink ?? [];
1796
- }, ...(ngDevMode ? [{ debugName: "routerLink" }] : []));
1797
- // Computed options for routerLinkActive - use 'exact' for root, 'subset' for others
1798
- routerLinkActiveOptions = computed(() => {
1799
- const link = this.routerLink();
1800
- const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1801
- return {
1802
- paths: pathsMatch,
1803
- queryParams: 'ignored',
1804
- matrixParams: 'ignored',
1805
- fragment: 'ignored',
1806
- };
1807
- }, ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : []));
1808
- constructor() {
1809
- // Subscribe to menu source changes
1810
- this.layoutService.menuSource$
1811
- .pipe(takeUntilDestroyed(this.destroyRef))
1812
- .subscribe((value) => {
1813
- Promise.resolve(null).then(() => {
1814
- const currentKey = this.key();
1815
- if (value.routeEvent) {
1816
- this._active.set(value.key === currentKey ||
1817
- value.key?.startsWith(currentKey + '-'));
1818
- }
1819
- else if (value.key !== currentKey &&
1820
- !value.key?.startsWith(currentKey + '-')) {
1821
- this._active.set(false);
1822
- }
1823
- });
1824
- });
1825
- // Subscribe to menu reset
1826
- this.layoutService.resetSource$
1827
- .pipe(takeUntilDestroyed(this.destroyRef))
1828
- .subscribe(() => this._active.set(false));
1829
- // Subscribe to navigation events
1830
- this.router.events
1831
- .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1832
- .subscribe(() => {
1833
- if (this.item().routerLink) {
1834
- this.updateActiveStateFromRoute();
1835
- }
1836
- });
1837
- // Effect to update active state on init when item has routerLink
1838
- effect(() => {
1839
- const menuItem = this.item();
1840
- if (menuItem.routerLink) {
1841
- this.updateActiveStateFromRoute();
1842
- }
1843
- });
1844
- }
1845
- updateActiveStateFromRoute() {
1846
- const link = this.routerLink();
1847
- if (!link.length)
1848
- return;
1849
- // Use 'exact' for root path to avoid matching all routes
1850
- // Use 'subset' for all other paths to match child routes
1851
- const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1852
- const isActive = this.router.isActive(link[0], {
1853
- paths: pathsMatch,
1854
- queryParams: 'ignored',
1855
- matrixParams: 'ignored',
1856
- fragment: 'ignored',
1857
- });
1858
- if (isActive) {
1859
- this.layoutService.onMenuStateChange({
1860
- key: this.key(),
1861
- routeEvent: true,
1862
- });
1863
- }
1864
- }
1865
- itemClick() {
1866
- if (this.item().children) {
1867
- this._active.update((prev) => !prev);
1868
- }
1869
- this.layoutService.onMenuStateChange({ key: this.key() });
1870
- }
1871
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenuitem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1872
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", 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: `
1873
- <ng-container>
1874
- @if (item().children?.length) {
1875
- <a (click)="itemClick()" tabindex="0" pRipple>
1876
- @if (item().icon) {
1877
- <lib-icon
1878
- [icon]="item().icon!"
1879
- [iconType]="item().iconType"
1880
- class="layout-menuitem-icon"
1881
- />
1882
- }
1883
- <span class="layout-menuitem-text">{{ item().label }}</span>
1884
- @if (item().children) {
1885
- <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
1886
- }
1887
- </a>
1888
- }
1889
- @if (item().routerLink && !item().children?.length) {
1890
- <a
1891
- (click)="itemClick()"
1892
- [routerLink]="routerLink()"
1893
- routerLinkActive="active-route"
1894
- [routerLinkActiveOptions]="routerLinkActiveOptions()"
1895
- tabindex="0"
1896
- pRipple
1897
- >
1898
- @if (item().icon) {
1899
- <lib-icon
1900
- [icon]="item().icon!"
1901
- [iconType]="item().iconType"
1902
- class="layout-menuitem-icon"
1903
- />
1904
- }
1905
- <span class="layout-menuitem-text">{{ item().label }}</span>
1906
- </a>
1907
- }
1908
- @if (item().children) {
1909
- <ul
1910
- [class.submenu-expanded]="active()"
1911
- [class.submenu-collapsed]="!active()"
1912
- >
1913
- @for (
1914
- child of item().children;
1915
- track 'menu' + child.id + '' + i;
1916
- let i = $index
1917
- ) {
1918
- <li
1919
- app-menuitem
1920
- [item]="child"
1921
- [index]="i"
1922
- [parentKey]="key()"
1923
- ></li>
1924
- }
1925
- </ul>
1926
- }
1927
- </ng-container>
1928
- `, 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]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1929
- }
1930
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenuitem, decorators: [{
1931
- type: Component,
1932
- args: [{ selector: '[app-menuitem]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [IconComponent, RouterModule, RippleModule], template: `
1933
- <ng-container>
1934
- @if (item().children?.length) {
1935
- <a (click)="itemClick()" tabindex="0" pRipple>
1936
- @if (item().icon) {
1937
- <lib-icon
1938
- [icon]="item().icon!"
1939
- [iconType]="item().iconType"
1940
- class="layout-menuitem-icon"
1941
- />
1942
- }
1943
- <span class="layout-menuitem-text">{{ item().label }}</span>
1944
- @if (item().children) {
1945
- <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
1946
- }
1947
- </a>
1948
- }
1949
- @if (item().routerLink && !item().children?.length) {
1950
- <a
1951
- (click)="itemClick()"
1952
- [routerLink]="routerLink()"
1953
- routerLinkActive="active-route"
1954
- [routerLinkActiveOptions]="routerLinkActiveOptions()"
1955
- tabindex="0"
1956
- pRipple
1957
- >
1958
- @if (item().icon) {
1959
- <lib-icon
1960
- [icon]="item().icon!"
1961
- [iconType]="item().iconType"
1962
- class="layout-menuitem-icon"
1963
- />
1964
- }
1965
- <span class="layout-menuitem-text">{{ item().label }}</span>
1966
- </a>
1967
- }
1968
- @if (item().children) {
1969
- <ul
1970
- [class.submenu-expanded]="active()"
1971
- [class.submenu-collapsed]="!active()"
1972
- >
1973
- @for (
1974
- child of item().children;
1975
- track 'menu' + child.id + '' + i;
1976
- let i = $index
1977
- ) {
1978
- <li
1979
- app-menuitem
1980
- [item]="child"
1981
- [index]="i"
1982
- [parentKey]="key()"
1983
- ></li>
1984
- }
1985
- </ul>
1986
- }
1987
- </ng-container>
1988
- `, host: {
1989
- '[class.active-menuitem]': 'active()',
1990
- }, 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"] }]
1991
- }], 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 }] }] } });
1992
-
1993
- /**
1994
- * Main menu component that displays filtered menu items.
1995
- * Menu is automatically filtered based on user permissions using the permission checker (logicNode pattern).
1996
- * The filtering happens internally in LayoutService via computed signal.
1997
- */
1998
- class AppMenu {
1999
- layoutService = inject(LayoutService);
2000
- /**
2001
- * Filtered menu items from layout service.
2002
- * This signal is computed in LayoutService and automatically updates when:
2003
- * - Raw menu changes (setMenu called)
2004
- * - Permission state changes (user permissions updated)
2005
- */
2006
- menuItems = this.layoutService.menu;
2007
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
2008
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppMenu, isStandalone: true, selector: "app-menu", ngImport: i0, template: `<div class="layout-menu">
2009
- <ul>
2010
- @for (
2011
- item of menuItems();
2012
- track 'menu' + item.id + '' + i;
2013
- let i = $index
2014
- ) {
2015
- <li app-menuitem [item]="item" [index]="i"></li>
2016
- }
2017
- </ul>
2018
- </div>`, isInline: true, dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "ngmodule", type: RouterModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2019
- }
2020
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenu, decorators: [{
2021
- type: Component,
2022
- args: [{
2023
- selector: 'app-menu',
2024
- changeDetection: ChangeDetectionStrategy.OnPush,
2025
- imports: [AppMenuitem, RouterModule],
2026
- template: `<div class="layout-menu">
2027
- <ul>
2028
- @for (
2029
- item of menuItems();
2030
- track 'menu' + item.id + '' + i;
2031
- let i = $index
2032
- ) {
2033
- <li app-menuitem [item]="item" [index]="i"></li>
2034
- }
2035
- </ul>
2036
- </div>`,
2037
- }]
2038
- }] });
2514
+ }], ctorParameters: () => [], propDecorators: { configContainer: [{ type: i0.ViewChild, args: ['configContainer', { isSignal: true }] }], navContainer: [{ type: i0.ViewChild, args: ['navContainer', { isSignal: true }] }] } });
2039
2515
 
2040
2516
  class AppSidebar {
2041
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2042
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.5", 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: `
2043
2519
  <div class="layout-sidebar">
2044
2520
  <app-menu />
2045
2521
  </div>
2046
- `, isInline: true, dependencies: [{ kind: "component", type: AppMenu, selector: "app-menu" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2522
+ `, isInline: true, dependencies: [{ kind: "component", type: AppMenu, selector: "app-menu" }] });
2047
2523
  }
2048
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppSidebar, decorators: [{
2524
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSidebar, decorators: [{
2049
2525
  type: Component,
2050
2526
  args: [{
2051
2527
  selector: 'app-sidebar',
2052
- changeDetection: ChangeDetectionStrategy.OnPush,
2053
2528
  imports: [AppMenu],
2054
2529
  template: `
2055
2530
  <div class="layout-sidebar">
@@ -2088,11 +2563,14 @@ class AppLayout {
2088
2563
  isOutsideClicked(event) {
2089
2564
  const sidebarEl = this.document.querySelector('.layout-sidebar');
2090
2565
  const topbarEl = this.document.querySelector('.layout-menu-button');
2566
+ const topbarNavEl = this.document.querySelector('.layout-topbar-nav');
2091
2567
  const eventTarget = event.target;
2092
2568
  return !(sidebarEl?.isSameNode(eventTarget) ||
2093
2569
  sidebarEl?.contains(eventTarget) ||
2094
2570
  topbarEl?.isSameNode(eventTarget) ||
2095
- topbarEl?.contains(eventTarget));
2571
+ topbarEl?.contains(eventTarget) ||
2572
+ topbarNavEl?.isSameNode(eventTarget) ||
2573
+ topbarNavEl?.contains(eventTarget));
2096
2574
  }
2097
2575
  hideMenu() {
2098
2576
  this.layoutService.updateLayoutState({
@@ -2112,20 +2590,32 @@ class AppLayout {
2112
2590
  unblockBodyScroll() {
2113
2591
  this.document.body.classList.remove('blocked-scroll');
2114
2592
  }
2115
- containerClass = computed(() => {
2116
- const config = this.layoutService.layoutConfig();
2117
- const state = this.layoutService.layoutState();
2118
- return {
2119
- 'layout-overlay': config.menuMode === 'overlay',
2120
- 'layout-static': config.menuMode === 'static',
2121
- 'layout-static-inactive': state.staticMenuDesktopInactive && config.menuMode === 'static',
2122
- 'layout-overlay-active': state.overlayMenuActive,
2123
- 'layout-mobile-active': state.staticMenuMobileActive,
2124
- };
2125
- }, ...(ngDevMode ? [{ debugName: "containerClass" }] : []));
2126
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
2127
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.5", type: AppLayout, isStandalone: true, selector: "app-layout", ngImport: i0, template: `
2128
- <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
+ >
2129
2619
  <app-topbar></app-topbar>
2130
2620
  <app-sidebar></app-sidebar>
2131
2621
  <div class="layout-main-container">
@@ -2136,16 +2626,24 @@ class AppLayout {
2136
2626
  </div>
2137
2627
  <div class="layout-mask animate-fadein"></div>
2138
2628
  </div>
2139
- `, isInline: true, dependencies: [{ kind: "directive", type: 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" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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" }] });
2140
2630
  }
2141
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppLayout, decorators: [{
2631
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLayout, decorators: [{
2142
2632
  type: Component,
2143
2633
  args: [{
2144
2634
  selector: 'app-layout',
2145
- changeDetection: ChangeDetectionStrategy.OnPush,
2146
- imports: [NgClass, AppTopbar, AppSidebar, RouterModule, AppFooter],
2635
+ imports: [AppTopbar, AppSidebar, RouterModule, AppFooter],
2147
2636
  template: `
2148
- <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
+ >
2149
2647
  <app-topbar></app-topbar>
2150
2648
  <app-sidebar></app-sidebar>
2151
2649
  <div class="layout-main-container">
@@ -2160,47 +2658,97 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2160
2658
  }]
2161
2659
  }], ctorParameters: () => [] });
2162
2660
 
2163
- const NavyBlueTheme = definePreset(Material, {
2164
- semantic: {
2165
- colorScheme: {
2166
- light: {
2167
- primary: {
2168
- color: '#3535cd',
2169
- inverseColor: '#0707a9',
2170
- hoverColor: '#0707a9',
2171
- activeColor: '#0707a9',
2172
- },
2173
- highlight: {
2174
- background: '#e2e8f0',
2175
- focusBackground: '#e2e8f0',
2176
- color: '#3535cd',
2177
- focusColor: '#3535cd',
2178
- },
2179
- },
2180
- },
2181
- },
2182
- });
2661
+ function buildPreset(presetName, primary) {
2662
+ return { preset: presetName, primary };
2663
+ }
2183
2664
 
2184
- const GreenTheme = definePreset(Material, {
2185
- semantic: {
2186
- colorScheme: {
2187
- light: {
2188
- primary: {
2189
- color: '#01712c',
2190
- inverseColor: '#119744',
2191
- hoverColor: '#119744',
2192
- activeColor: '#119744',
2193
- },
2194
- highlight: {
2195
- background: '#e2e8f0',
2196
- focusBackground: '#e2e8f0',
2197
- color: '#01712c',
2198
- focusColor: '#01712c',
2199
- },
2200
- },
2201
- },
2202
- },
2203
- });
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
+ };
2204
2752
 
2205
2753
  // Components
2206
2754
 
@@ -2208,5 +2756,5 @@ const GreenTheme = definePreset(Material, {
2208
2756
  * Generated bundle index. Do not edit.
2209
2757
  */
2210
2758
 
2211
- 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 };
2212
2760
  //# sourceMappingURL=flusys-ng-layout.mjs.map