@flusys/ng-layout 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +150 -11
  2. package/assets/layout/base/_brand.scss +44 -0
  3. package/assets/layout/{_core.scss → base/_core.scss} +8 -1
  4. package/assets/layout/{_footer.scss → base/_footer.scss} +1 -1
  5. package/assets/layout/{_forms.scss → base/_forms.scss} +3 -3
  6. package/assets/layout/base/_menu.scss +52 -0
  7. package/assets/layout/base/_rtl.scss +230 -0
  8. package/assets/layout/base/_topbar.scss +132 -0
  9. package/assets/layout/base/variables/_common.scss +62 -0
  10. package/assets/layout/classic/_main.scss +14 -0
  11. package/assets/layout/classic/_menu.scss +77 -0
  12. package/assets/layout/classic/_responsive.scss +101 -0
  13. package/assets/layout/classic/_rtl.scss +26 -0
  14. package/assets/layout/classic/_topbar.scss +46 -0
  15. package/assets/layout/{_topbar_nav.scss → classic/_topbar_nav.scss} +31 -15
  16. package/assets/layout/classic/classic.scss +6 -0
  17. package/assets/layout/layout.scss +18 -16
  18. package/assets/layout/modern/_menu.scss +98 -0
  19. package/assets/layout/modern/_responsive.scss +93 -0
  20. package/assets/layout/modern/_rtl.scss +24 -0
  21. package/assets/layout/modern/_shell.scss +59 -0
  22. package/assets/layout/modern/_sidebar.scss +101 -0
  23. package/assets/layout/modern/_topbar.scss +37 -0
  24. package/assets/layout/modern/_variables.scss +114 -0
  25. package/assets/layout/modern/modern.scss +7 -0
  26. package/fesm2022/flusys-ng-layout.mjs +1315 -939
  27. package/fesm2022/flusys-ng-layout.mjs.map +1 -1
  28. package/package.json +4 -4
  29. package/types/flusys-ng-layout.d.ts +171 -64
  30. package/assets/layout/_main.scss +0 -13
  31. package/assets/layout/_menu.scss +0 -188
  32. package/assets/layout/_responsive.scss +0 -128
  33. package/assets/layout/_rtl.scss +0 -482
  34. package/assets/layout/_topbar.scss +0 -215
  35. package/assets/layout/variables/_common.scss +0 -20
  36. /package/assets/layout/{_mixins.scss → base/_mixins.scss} +0 -0
  37. /package/assets/layout/{_preloading.scss → base/_preloading.scss} +0 -0
  38. /package/assets/layout/{_typography.scss → base/_typography.scss} +0 -0
  39. /package/assets/layout/{_utils.scss → base/_utils.scss} +0 -0
  40. /package/assets/layout/{variables → base/variables}/_dark.scss +0 -0
  41. /package/assets/layout/{variables → base/variables}/_light.scss +0 -0
@@ -1,19 +1,19 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, PLATFORM_ID, Injectable, DOCUMENT, signal, DestroyRef, computed, effect, Component, InjectionToken, input, viewChild, ElementRef, Renderer2 } from '@angular/core';
3
- import * as i3 from '@flusys/ng-shared';
4
- import { evaluateLogicNode, PermissionValidatorService, InlineSvgComponent, TranslatePipe, TranslateService, IconComponent, AngularModule } from '@flusys/ng-shared';
5
- import { isPlatformBrowser, NgComponentOutlet, DOCUMENT as DOCUMENT$1 } from '@angular/common';
2
+ import { inject, PLATFORM_ID, Injectable, DOCUMENT, signal, DestroyRef, computed, effect, Renderer2, Component, input, viewChild, InjectionToken, ElementRef } from '@angular/core';
3
+ import { isPlatformBrowser, DOCUMENT as DOCUMENT$1, NgComponentOutlet } from '@angular/common';
6
4
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
5
  import { APP_CONFIG, DEFAULT_APP_NAME, DEFAULT_AUTHOR, isCompanyFeatureEnabled, isSignUpEnabled } from '@flusys/ng-core';
8
- import { Subject, fromEvent, filter as filter$1 } from 'rxjs';
9
- import * as i2 from '@angular/forms';
10
- import { FormsModule } from '@angular/forms';
11
- import * as i2$1 from '@angular/router';
12
- import { Router, NavigationEnd, isActive, RouterModule } from '@angular/router';
6
+ import * as i3 from '@flusys/ng-shared';
7
+ import { evaluateLogicNode, PermissionValidatorService, InlineSvgComponent, TranslatePipe, IconComponent, TranslateService, AngularModule } from '@flusys/ng-shared';
8
+ import { Subject, fromEvent, filter } from 'rxjs';
13
9
  import * as i1 from '@flusys/ng-ui';
14
- import { PRESET_NAMES, applyTheme, SURFACE_PALETTES, PRIMARY_PALETTES, applyPrimaryPalette, applySurfacePalette, PRESETS, UiModule, MessageService } from '@flusys/ng-ui';
10
+ import { PRESET_NAMES, UiModule, applyTheme, SURFACE_PALETTES, PRIMARY_PALETTES, NOIR_PRIMARY, applyPrimaryPalette, applySurfacePalette, PRESETS, MessageService } from '@flusys/ng-ui';
15
11
  export { applyTheme, buildBootstrapPreset } from '@flusys/ng-ui';
16
- import { filter } from 'rxjs/operators';
12
+ import * as i2 from '@angular/router';
13
+ import { Router, NavigationEnd, isActive, RouterModule } from '@angular/router';
14
+ import { filter as filter$1 } from 'rxjs/operators';
15
+ import * as i2$1 from '@angular/forms';
16
+ import { FormsModule } from '@angular/forms';
17
17
 
18
18
  /** Filter launcher apps based on user permission codes */
19
19
  function filterAppsByPermissions(apps, permissionCodes) {
@@ -76,11 +76,13 @@ function filterMenuItem(item, permissionCode) {
76
76
 
77
77
  const STORAGE_KEY = 'flusys.layout.config';
78
78
  const STORAGE_VERSION = 1;
79
+ // Sourced from the theme package so a new preset never silently fails validation.
80
+ const VALID_PRESETS = PRESET_NAMES;
81
+ const VALID_MENU_MODES = ['static', 'overlay', 'topbar'];
82
+ const VALID_LAYOUT_THEMES = ['classic', 'modern'];
79
83
  class LayoutPersistenceService {
80
84
  platformId = inject(PLATFORM_ID);
81
85
  isBrowser = isPlatformBrowser(this.platformId);
82
- validPresets = ['Aura', 'Lara', 'Nora'];
83
- validMenuModes = ['static', 'overlay', 'topbar'];
84
86
  load() {
85
87
  if (!this.isBrowser)
86
88
  return null;
@@ -88,8 +90,7 @@ class LayoutPersistenceService {
88
90
  const stored = localStorage.getItem(STORAGE_KEY);
89
91
  if (!stored)
90
92
  return null;
91
- const parsed = JSON.parse(stored);
92
- return this.validateConfig(parsed);
93
+ return this.validateConfig(JSON.parse(stored));
93
94
  }
94
95
  catch {
95
96
  this.clear();
@@ -107,7 +108,7 @@ class LayoutPersistenceService {
107
108
  localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
108
109
  }
109
110
  catch {
110
- // Silent fail - localStorage may be full or disabled
111
+ // localStorage may be full or disabled — the config simply stays unsaved.
111
112
  }
112
113
  }
113
114
  clear() {
@@ -115,22 +116,32 @@ class LayoutPersistenceService {
115
116
  return;
116
117
  localStorage.removeItem(STORAGE_KEY);
117
118
  }
118
- validateConfig(config) {
119
- if (config._version && config._version !== STORAGE_VERSION) {
119
+ /**
120
+ * Coerces a stored value only when it is present and unrecognised. A field the
121
+ * user never changed stays absent so the app's own default still wins when this
122
+ * result is merged over it.
123
+ */
124
+ validateConfig(stored) {
125
+ if (stored._version && stored._version !== STORAGE_VERSION) {
120
126
  this.clear();
121
127
  return null;
122
128
  }
123
- if (config.preset && !this.validPresets.includes(config.preset)) {
129
+ const { _version, ...config } = stored;
130
+ if (config.preset !== undefined && !VALID_PRESETS.includes(config.preset)) {
124
131
  config.preset = 'Aura';
125
132
  }
126
- if (config.menuMode && !this.validMenuModes.includes(config.menuMode)) {
133
+ if (config.menuMode !== undefined &&
134
+ !VALID_MENU_MODES.includes(config.menuMode)) {
127
135
  config.menuMode = 'static';
128
136
  }
129
- if (typeof config.darkTheme !== 'boolean') {
137
+ if (config.layoutTheme !== undefined &&
138
+ !VALID_LAYOUT_THEMES.includes(config.layoutTheme)) {
139
+ config.layoutTheme = 'classic';
140
+ }
141
+ if (config.darkTheme !== undefined && typeof config.darkTheme !== 'boolean') {
130
142
  config.darkTheme = false;
131
143
  }
132
- const { _version, ...cleanConfig } = config;
133
- return cleanConfig;
144
+ return config;
134
145
  }
135
146
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutPersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
136
147
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LayoutPersistenceService, providedIn: 'root' });
@@ -140,6 +151,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
140
151
  args: [{ providedIn: 'root' }]
141
152
  }] });
142
153
 
154
+ /** Matches the 991px shell breakpoint both skins use in SCSS. */
143
155
  const DESKTOP_BREAKPOINT = 991;
144
156
  class LayoutService {
145
157
  document = inject(DOCUMENT);
@@ -153,6 +165,7 @@ class LayoutService {
153
165
  surface: null,
154
166
  darkTheme: false,
155
167
  menuMode: 'static',
168
+ layoutTheme: 'classic',
156
169
  };
157
170
  DEFAULT_STATE = {
158
171
  staticMenuDesktopInactive: false,
@@ -211,22 +224,32 @@ class LayoutService {
211
224
  ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : /* istanbul ignore next */ []));
212
225
  isOverlay = computed(() => this._layoutConfig().menuMode === 'overlay', /* @ts-ignore */
213
226
  ...(ngDevMode ? [{ debugName: "isOverlay" }] : /* istanbul ignore next */ []));
214
- isTopbar = computed(() => this._layoutConfig().menuMode === 'topbar', /* @ts-ignore */
227
+ layoutTheme = computed(() => this._layoutConfig().layoutTheme ?? 'classic', /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "layoutTheme" }] : /* istanbul ignore next */ []));
229
+ isModernLayout = computed(() => this.layoutTheme() === 'modern', /* @ts-ignore */
230
+ ...(ngDevMode ? [{ debugName: "isModernLayout" }] : /* istanbul ignore next */ []));
231
+ isClassicLayout = computed(() => this.layoutTheme() === 'classic', /* @ts-ignore */
232
+ ...(ngDevMode ? [{ debugName: "isClassicLayout" }] : /* istanbul ignore next */ []));
233
+ /** The modern skin has no horizontal nav, so topbar mode only applies to the classic skin. */
234
+ isTopbar = computed(() => this._layoutConfig().menuMode === 'topbar' && !this.isModernLayout(), /* @ts-ignore */
215
235
  ...(ngDevMode ? [{ debugName: "isTopbar" }] : /* istanbul ignore next */ []));
216
236
  userName = computed(() => this._userProfile()?.name ?? 'Guest', /* @ts-ignore */
217
237
  ...(ngDevMode ? [{ debugName: "userName" }] : /* istanbul ignore next */ []));
218
238
  userEmail = computed(() => this._userProfile()?.email ?? '', /* @ts-ignore */
219
239
  ...(ngDevMode ? [{ debugName: "userEmail" }] : /* istanbul ignore next */ []));
220
- userProfilePictureUrl = computed(() => this._userProfile()?.profilePictureUrl ?? null, /* @ts-ignore */
240
+ // `||`, not `??`, throughout the block below: the backend returns "no picture" and
241
+ // "no logo" as an empty string as often as it does null, and an empty string would
242
+ // slip past `??` and render as a broken image.
243
+ userProfilePictureUrl = computed(() => this._userProfile()?.profilePictureUrl || null, /* @ts-ignore */
221
244
  ...(ngDevMode ? [{ debugName: "userProfilePictureUrl" }] : /* istanbul ignore next */ []));
222
- companyLogoUrl = computed(() => this._companyProfile()?.logoUrl ?? null, /* @ts-ignore */
245
+ companyLogoUrl = computed(() => this._companyProfile()?.logoUrl || null, /* @ts-ignore */
223
246
  ...(ngDevMode ? [{ debugName: "companyLogoUrl" }] : /* istanbul ignore next */ []));
224
247
  isAuthenticated = computed(() => !!this._userProfile(), /* @ts-ignore */
225
248
  ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ []));
226
249
  companyName = computed(() => {
227
250
  if (!this.appConfig || !isCompanyFeatureEnabled(this.appConfig))
228
251
  return this.appName;
229
- return this._companyProfile()?.name ?? this.appName;
252
+ return this._companyProfile()?.name || this.appName;
230
253
  }, /* @ts-ignore */
231
254
  ...(ngDevMode ? [{ debugName: "companyName" }] : /* istanbul ignore next */ []));
232
255
  displayLogo = computed(() => this.companyLogoUrl() ?? this.appLogo, /* @ts-ignore */
@@ -240,6 +263,7 @@ class LayoutService {
240
263
  resetSource$ = this.resetSource.asObservable();
241
264
  overlayOpen$ = this.overlayOpen.asObservable();
242
265
  initialized = false;
266
+ appliedDarkTheme = this._layoutConfig().darkTheme;
243
267
  constructor() {
244
268
  if (this.isBrowser) {
245
269
  this.toggleDarkMode();
@@ -252,7 +276,13 @@ class LayoutService {
252
276
  effect(() => {
253
277
  const config = this._layoutConfig();
254
278
  if (this.initialized) {
255
- this.handleDarkModeTransition(config);
279
+ // Only the dark-mode flip is worth a view transition. Every other field —
280
+ // preset, primary, menu mode, layout skin — used to start one too, which
281
+ // cross-faded the whole page for a change that never touched it.
282
+ if (config.darkTheme !== this.appliedDarkTheme) {
283
+ this.appliedDarkTheme = config.darkTheme;
284
+ this.handleDarkModeTransition(config);
285
+ }
256
286
  if (this.memorable)
257
287
  this.persistence.save(config);
258
288
  }
@@ -357,6 +387,84 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
357
387
  args: [{ providedIn: 'root' }]
358
388
  }], ctorParameters: () => [] });
359
389
 
390
+ /**
391
+ * Shared shell behaviour for every layout skin.
392
+ * Holds the menu open/close lifecycle so a skin only owns markup and styling.
393
+ */
394
+ class LayoutShellBase {
395
+ destroyRef = inject(DestroyRef);
396
+ document = inject(DOCUMENT);
397
+ renderer = inject(Renderer2);
398
+ router = inject(Router);
399
+ layoutService = inject(LayoutService);
400
+ menuOutsideClickListener = null;
401
+ /** Extra work a skin needs on every completed navigation. */
402
+ onNavigated() { }
403
+ constructor() {
404
+ this.layoutService.overlayOpen$
405
+ .pipe(takeUntilDestroyed(this.destroyRef))
406
+ .subscribe(() => {
407
+ if (!this.menuOutsideClickListener) {
408
+ this.menuOutsideClickListener = this.renderer.listen(this.document, 'click', (event) => {
409
+ if (this.isOutsideClicked(event)) {
410
+ this.hideMenu();
411
+ }
412
+ });
413
+ }
414
+ if (this.layoutService.layoutState().staticMenuMobileActive) {
415
+ this.blockBodyScroll();
416
+ }
417
+ });
418
+ this.router.events
419
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
420
+ .subscribe(() => {
421
+ this.hideMenu();
422
+ this.onNavigated();
423
+ });
424
+ }
425
+ isOutsideClicked(event) {
426
+ const eventTarget = event.target;
427
+ // querySelectorAll, not querySelector: a skin may render the same control more
428
+ // than once (the modern shell has a menu button in both the sidebar and the
429
+ // topbar) and matching only the first one reads a real click as an outside one.
430
+ return !this.outsideClickSelectors().some((selector) => Array.from(this.document.querySelectorAll(selector)).some((element) => element.isSameNode(eventTarget) || element.contains(eventTarget)));
431
+ }
432
+ hideMenu() {
433
+ this.layoutService.updateLayoutState({
434
+ overlayMenuActive: false,
435
+ staticMenuMobileActive: false,
436
+ menuHoverActive: false,
437
+ });
438
+ if (this.menuOutsideClickListener) {
439
+ this.menuOutsideClickListener();
440
+ this.menuOutsideClickListener = null;
441
+ }
442
+ this.unblockBodyScroll();
443
+ }
444
+ blockBodyScroll() {
445
+ this.document.body.classList.add('blocked-scroll');
446
+ }
447
+ unblockBodyScroll() {
448
+ this.document.body.classList.remove('blocked-scroll');
449
+ }
450
+ isOverlay = this.layoutService.isOverlay;
451
+ /** Delegated so a skin that has no horizontal nav is never treated as being in it. */
452
+ isTopbarMode = this.layoutService.isTopbar;
453
+ isOverlayActive = computed(() => this.layoutService.layoutState().overlayMenuActive, /* @ts-ignore */
454
+ ...(ngDevMode ? [{ debugName: "isOverlayActive" }] : /* istanbul ignore next */ []));
455
+ isMobileActive = computed(() => this.layoutService.layoutState().staticMenuMobileActive, /* @ts-ignore */
456
+ ...(ngDevMode ? [{ debugName: "isMobileActive" }] : /* istanbul ignore next */ []));
457
+ /**
458
+ * The desktop sidebar is collapsed in place. Only meaningful for the modes that
459
+ * keep the sidebar in the layout flow — an overlay drawer and the horizontal nav
460
+ * have nothing to collapse.
461
+ */
462
+ isDesktopCollapsed = computed(() => !!this.layoutService.layoutState().staticMenuDesktopInactive &&
463
+ !this.isOverlay() &&
464
+ !this.isTopbarMode(), /* @ts-ignore */
465
+ ...(ngDevMode ? [{ debugName: "isDesktopCollapsed" }] : /* istanbul ignore next */ []));
466
+ }
467
+
360
468
  class AppFooter {
361
469
  layoutService = inject(LayoutService);
362
470
  appName = this.layoutService.appName;
@@ -412,709 +520,698 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
412
520
  }]
413
521
  }] });
414
522
 
415
- class AppConfigurator {
523
+ class AppMenuitem {
524
+ // Signal inputs
525
+ item = input.required(/* @ts-ignore */
526
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
527
+ index = input.required(/* @ts-ignore */
528
+ ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
529
+ parentKey = input('', /* @ts-ignore */
530
+ ...(ngDevMode ? [{ debugName: "parentKey" }] : /* istanbul ignore next */ []));
531
+ // Injected services
416
532
  router = inject(Router);
417
- translateAdapter = inject(TranslateService, { optional: true });
418
- platformId = inject(PLATFORM_ID);
419
533
  layoutService = inject(LayoutService);
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;
534
+ destroyRef = inject(DestroyRef);
535
+ // View references
536
+ parentLink = viewChild('parentLink', /* @ts-ignore */
537
+ ...(ngDevMode ? [{ debugName: "parentLink" }] : /* istanbul ignore next */ []));
538
+ // State signals - private writable + public readonly pattern
539
+ _active = signal(false, /* @ts-ignore */
540
+ ...(ngDevMode ? [{ debugName: "_active" }] : /* istanbul ignore next */ []));
541
+ active = this._active.asReadonly();
542
+ // Computed signals
543
+ key = computed(() => {
544
+ const parent = this.parentKey();
545
+ return parent ? `${parent}-${this.index()}` : String(this.index());
432
546
  }, /* @ts-ignore */
433
- ...(ngDevMode ? [{ debugName: "menuModeOptions" }] : /* istanbul ignore next */ []));
547
+ ...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
548
+ routerLink = computed(() => {
549
+ const menuItem = this.item();
550
+ return menuItem.routerLink ?? [];
551
+ }, /* @ts-ignore */
552
+ ...(ngDevMode ? [{ debugName: "routerLink" }] : /* istanbul ignore next */ []));
553
+ // Computed options for routerLinkActive - use 'exact' for root, 'subset' for others
554
+ routerLinkActiveOptions = computed(() => {
555
+ const link = this.routerLink();
556
+ const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
557
+ return {
558
+ paths: pathsMatch,
559
+ queryParams: 'ignored',
560
+ matrixParams: 'ignored',
561
+ fragment: 'ignored',
562
+ };
563
+ }, /* @ts-ignore */
564
+ ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : /* istanbul ignore next */ []));
434
565
  constructor() {
435
- if (isPlatformBrowser(this.platformId)) {
436
- effect(() => {
437
- const config = this.layoutService.layoutConfig();
438
- applyTheme({
439
- preset: (config.preset ?? 'Aura'),
440
- primary: config.primary ?? 'violet',
441
- surface: config.surface ?? null,
442
- darkTheme: config.darkTheme ?? false,
443
- });
566
+ // Subscribe to menu source changes
567
+ this.layoutService.menuSource$
568
+ .pipe(takeUntilDestroyed(this.destroyRef))
569
+ .subscribe((value) => {
570
+ Promise.resolve(null).then(() => {
571
+ const currentKey = this.key();
572
+ if (value.routeEvent) {
573
+ this._active.set(value.key === currentKey ||
574
+ value.key?.startsWith(currentKey + '-'));
575
+ }
576
+ else if (value.key !== currentKey &&
577
+ !value.key?.startsWith(currentKey + '-')) {
578
+ this._active.set(false);
579
+ }
444
580
  });
581
+ });
582
+ // Subscribe to menu reset
583
+ this.layoutService.resetSource$
584
+ .pipe(takeUntilDestroyed(this.destroyRef))
585
+ .subscribe(() => this._active.set(false));
586
+ // Subscribe to navigation events
587
+ this.router.events
588
+ .pipe(filter$1((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
589
+ .subscribe(() => {
590
+ if (this.item().routerLink) {
591
+ this.updateActiveStateFromRoute();
592
+ }
593
+ });
594
+ // Effect to update active state on init when item has routerLink
595
+ effect(() => {
596
+ const menuItem = this.item();
597
+ if (menuItem.routerLink) {
598
+ this.updateActiveStateFromRoute();
599
+ }
600
+ });
601
+ // Effect to update submenu position when active in topbar mode
602
+ effect(() => {
603
+ if (this.active() && this.parentLink()) {
604
+ Promise.resolve().then(() => this.updateSubmenuPosition());
605
+ }
606
+ });
607
+ }
608
+ onMouseEnter() {
609
+ if (this.layoutService.isTopbar() && this.item().children) {
610
+ this.updateSubmenuPosition();
445
611
  }
446
612
  }
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';
613
+ updateSubmenuPosition() {
614
+ const linkEl = this.parentLink()?.nativeElement;
615
+ if (!linkEl || typeof linkEl.getBoundingClientRect !== 'function')
616
+ return;
617
+ const rect = linkEl.getBoundingClientRect();
618
+ const hostEl = linkEl.closest('[app-menuitem]');
619
+ if (!hostEl)
620
+ return;
621
+ // Position submenu to the left of the parent link and above it
622
+ const left = rect.left;
623
+ const top = rect.top + 30; // 8px gap above the menu item
624
+ hostEl.style.setProperty('--submenu-left', `${left}px`);
625
+ hostEl.style.setProperty('--submenu-top', `${top}px`);
461
626
  }
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 */ []));
466
- primaryColors = computed(() => {
467
- const palettes = [{ name: 'noir', palette: {} }];
468
- for (const [name, pal] of Object.entries(PRIMARY_PALETTES)) {
469
- palettes.push({ name, palette: pal });
470
- }
471
- return palettes;
472
- }, /* @ts-ignore */
473
- ...(ngDevMode ? [{ debugName: "primaryColors" }] : /* istanbul ignore next */ []));
474
- updateColors(event, type, color) {
475
- if (type === 'primary') {
476
- this.layoutService.updateLayoutConfig({ primary: color.name });
477
- }
478
- else if (type === 'surface') {
479
- this.layoutService.updateLayoutConfig({ surface: color.name });
627
+ updateActiveStateFromRoute() {
628
+ const link = this.routerLink();
629
+ if (!link.length)
630
+ return;
631
+ // Use 'exact' for root path to avoid matching all routes
632
+ // Use 'subset' for all other paths to match child routes
633
+ const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
634
+ const active = isActive(link[0], this.router, {
635
+ paths: pathsMatch,
636
+ queryParams: 'ignored',
637
+ matrixParams: 'ignored',
638
+ fragment: 'ignored',
639
+ })();
640
+ if (active) {
641
+ this.layoutService.onMenuStateChange({
642
+ key: this.key(),
643
+ routeEvent: true,
644
+ });
480
645
  }
481
- this._applyColorChange(type, color);
482
- event.stopPropagation();
483
646
  }
484
- _applyColorChange(type, color) {
485
- const isDark = this.layoutService.layoutConfig().darkTheme ?? false;
486
- if (type === 'primary') {
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);
493
- }
494
- else if (type === 'surface') {
495
- const surfacePal = SURFACE_PALETTES[color.name ?? 'slate'];
496
- if (surfacePal)
497
- applySurfacePalette(surfacePal);
647
+ itemClick() {
648
+ if (this.item().children && !this.layoutService.isTopbar()) {
649
+ this._active.update((prev) => !prev);
498
650
  }
651
+ this.layoutService.onMenuStateChange({ key: this.key() });
499
652
  }
500
- onPresetChange(event) {
501
- const primary = PRESETS[event]?.signaturePrimary ?? 'violet';
502
- this.layoutService.updateLayoutConfig({ preset: event, primary });
503
- const isDark = this.layoutService.layoutConfig().darkTheme ?? false;
504
- const surface = this.layoutService.layoutConfig().surface ?? null;
505
- applyTheme({ preset: event, primary, surface, darkTheme: isDark });
506
- }
507
- onMenuModeChange(event) {
508
- this.layoutService.updateLayoutConfig({ menuMode: event });
509
- }
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: `
515
- <div class="flex flex-col gap-4">
516
- <div>
517
- <span class="text-sm text-muted-color font-semibold">{{
518
- 'layout.configurator.primary' | translate
519
- }}</span>
520
- <div class="pt-2 flex gap-2 flex-wrap justify-start">
521
- @for (primaryColor of primaryColors(); track primaryColor.name) {
522
- <button
523
- type="button"
524
- [title]="primaryColor.name"
525
- (click)="updateColors($event, 'primary', primaryColor)"
526
- [class.outline-primary]="primaryColor.name === selectedPrimaryColor()"
527
- class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
528
- [style.background-color]="
529
- primaryColor?.name === 'noir'
530
- ? 'var(--f-text-color)'
531
- : primaryColor?.palette?.['500']
532
- "
533
- ></button>
653
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenuitem, deps: [], target: i0.ɵɵFactoryTarget.Component });
654
+ 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: `
655
+ <ng-container>
656
+ @if (item().children?.length) {
657
+ <a
658
+ (click)="itemClick()"
659
+ (mouseenter)="onMouseEnter()"
660
+ #parentLink
661
+ tabindex="0"
662
+ >
663
+ @if (item().icon) {
664
+ <lib-icon
665
+ [icon]="item().icon!"
666
+ [iconType]="item().iconType"
667
+ class="layout-menuitem-icon"
668
+ />
534
669
  }
535
- </div>
536
- </div>
537
- <div>
538
- <span class="text-sm text-muted-color font-semibold">{{
539
- 'layout.configurator.surface' | translate
540
- }}</span>
541
- <div class="pt-2 flex gap-2 flex-wrap justify-start">
542
- @for (surface of surfaces; track surface.name) {
543
- <button
544
- type="button"
545
- [title]="surface.name"
546
- (click)="updateColors($event, 'surface', surface)"
547
- [class.outline-primary]="isSurfaceSelected(surface.name)"
548
- class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
549
- [style.background-color]="
550
- surface?.name === 'noir'
551
- ? 'var(--f-text-color)'
552
- : surface?.palette?.['500']
553
- "
554
- ></button>
670
+ <span class="layout-menuitem-text">{{
671
+ item().labelKey ? (item().labelKey! | translate) : item().label
672
+ }}</span>
673
+ @if (item().children) {
674
+ @if (layoutService.isTopbar()) {
675
+ <ui-icon name="chevron-right" class="layout-submenu-toggler"></ui-icon>
676
+ } @else {
677
+ <ui-icon name="angle-down" class="layout-submenu-toggler"></ui-icon>
678
+ }
555
679
  }
556
- </div>
557
- </div>
558
- <div class="flex flex-col gap-2">
559
- <span class="text-sm text-muted-color font-semibold">{{
560
- 'layout.configurator.presets' | translate
561
- }}</span>
562
- <f-selectbutton
563
- [options]="presets"
564
- [ngModel]="selectedPreset()"
565
- (ngModelChange)="onPresetChange($event)"
566
- [allowEmpty]="false"
567
- size="small"
568
- />
569
- </div>
570
- @if (showMenuModeButton()) {
571
- <div class="flex flex-col gap-2">
572
- <span class="text-sm text-muted-color font-semibold">{{
573
- 'layout.configurator.menu.mode' | translate
680
+ </a>
681
+ }
682
+ @if (item().routerLink && !item().children?.length) {
683
+ <a
684
+ (click)="itemClick()"
685
+ [routerLink]="routerLink()"
686
+ routerLinkActive="active-route"
687
+ [routerLinkActiveOptions]="routerLinkActiveOptions()"
688
+ tabindex="0"
689
+ >
690
+ @if (item().icon) {
691
+ <lib-icon
692
+ [icon]="item().icon!"
693
+ [iconType]="item().iconType"
694
+ class="layout-menuitem-icon"
695
+ />
696
+ }
697
+ <span class="layout-menuitem-text">{{
698
+ item().labelKey ? (item().labelKey! | translate) : item().label
574
699
  }}</span>
575
- <f-selectbutton
576
- [ngModel]="menuMode()"
577
- (ngModelChange)="onMenuModeChange($event)"
578
- [options]="menuModeOptions()"
579
- [allowEmpty]="false"
580
- size="small"
581
- />
582
- </div>
700
+ </a>
583
701
  }
584
- </div>
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" }] });
702
+ @if (item().children) {
703
+ <ul
704
+ (mouseenter)="onMouseEnter()"
705
+ [class.submenu-expanded]="active() && !layoutService.isTopbar()"
706
+ [class.submenu-collapsed]="!active() && !layoutService.isTopbar()"
707
+ >
708
+ @for (
709
+ child of item().children;
710
+ track 'menu' + child.id + '' + i;
711
+ let i = $index
712
+ ) {
713
+ <li
714
+ app-menuitem
715
+ [item]="child"
716
+ [index]="i"
717
+ [parentKey]="key()"
718
+ ></li>
719
+ }
720
+ </ul>
721
+ }
722
+ </ng-container>
723
+ `, 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.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
586
724
  }
587
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppConfigurator, decorators: [{
725
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenuitem, decorators: [{
588
726
  type: Component,
589
- args: [{
590
- selector: 'app-configurator',
591
- imports: [UiModule, FormsModule, TranslatePipe],
592
- template: `
593
- <div class="flex flex-col gap-4">
594
- <div>
595
- <span class="text-sm text-muted-color font-semibold">{{
596
- 'layout.configurator.primary' | translate
597
- }}</span>
598
- <div class="pt-2 flex gap-2 flex-wrap justify-start">
599
- @for (primaryColor of primaryColors(); track primaryColor.name) {
600
- <button
601
- type="button"
602
- [title]="primaryColor.name"
603
- (click)="updateColors($event, 'primary', primaryColor)"
604
- [class.outline-primary]="primaryColor.name === selectedPrimaryColor()"
605
- class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
606
- [style.background-color]="
607
- primaryColor?.name === 'noir'
608
- ? 'var(--f-text-color)'
609
- : primaryColor?.palette?.['500']
610
- "
611
- ></button>
612
- }
613
- </div>
614
- </div>
615
- <div>
616
- <span class="text-sm text-muted-color font-semibold">{{
617
- 'layout.configurator.surface' | translate
618
- }}</span>
619
- <div class="pt-2 flex gap-2 flex-wrap justify-start">
620
- @for (surface of surfaces; track surface.name) {
621
- <button
622
- type="button"
623
- [title]="surface.name"
624
- (click)="updateColors($event, 'surface', surface)"
625
- [class.outline-primary]="isSurfaceSelected(surface.name)"
626
- class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
627
- [style.background-color]="
628
- surface?.name === 'noir'
629
- ? 'var(--f-text-color)'
630
- : surface?.palette?.['500']
631
- "
632
- ></button>
727
+ args: [{ selector: '[app-menuitem]', imports: [UiModule, IconComponent, RouterModule, TranslatePipe], template: `
728
+ <ng-container>
729
+ @if (item().children?.length) {
730
+ <a
731
+ (click)="itemClick()"
732
+ (mouseenter)="onMouseEnter()"
733
+ #parentLink
734
+ tabindex="0"
735
+ >
736
+ @if (item().icon) {
737
+ <lib-icon
738
+ [icon]="item().icon!"
739
+ [iconType]="item().iconType"
740
+ class="layout-menuitem-icon"
741
+ />
633
742
  }
634
- </div>
635
- </div>
636
- <div class="flex flex-col gap-2">
637
- <span class="text-sm text-muted-color font-semibold">{{
638
- 'layout.configurator.presets' | translate
639
- }}</span>
640
- <f-selectbutton
641
- [options]="presets"
642
- [ngModel]="selectedPreset()"
643
- (ngModelChange)="onPresetChange($event)"
644
- [allowEmpty]="false"
645
- size="small"
646
- />
647
- </div>
648
- @if (showMenuModeButton()) {
649
- <div class="flex flex-col gap-2">
650
- <span class="text-sm text-muted-color font-semibold">{{
651
- 'layout.configurator.menu.mode' | translate
743
+ <span class="layout-menuitem-text">{{
744
+ item().labelKey ? (item().labelKey! | translate) : item().label
652
745
  }}</span>
653
- <f-selectbutton
654
- [ngModel]="menuMode()"
655
- (ngModelChange)="onMenuModeChange($event)"
656
- [options]="menuModeOptions()"
657
- [allowEmpty]="false"
658
- size="small"
659
- />
660
- </div>
746
+ @if (item().children) {
747
+ @if (layoutService.isTopbar()) {
748
+ <ui-icon name="chevron-right" class="layout-submenu-toggler"></ui-icon>
749
+ } @else {
750
+ <ui-icon name="angle-down" class="layout-submenu-toggler"></ui-icon>
751
+ }
752
+ }
753
+ </a>
661
754
  }
662
- </div>
663
- `,
664
- host: {
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;',
667
- },
668
- }]
669
- }], ctorParameters: () => [] });
670
-
671
- const LAYOUT_AUTH_STATE = new InjectionToken('LAYOUT_AUTH_STATE');
672
- const LAYOUT_AUTH_API = new InjectionToken('LAYOUT_AUTH_API');
673
- const LAYOUT_NOTIFICATION_BELL = new InjectionToken('LAYOUT_NOTIFICATION_BELL');
674
- const LAYOUT_LANGUAGE_SELECTOR = new InjectionToken('LAYOUT_LANGUAGE_SELECTOR');
675
- const LAYOUT_LANGUAGE_SELECTOR_PANEL = new InjectionToken('LAYOUT_LANGUAGE_SELECTOR_PANEL');
676
- const LAYOUT_SEARCHBAR = new InjectionToken('LAYOUT_SEARCHBAR');
677
- const LAYOUT_IS_RTL = new InjectionToken('LAYOUT_IS_RTL');
678
- const LAYOUT_SEARCH_ADAPTER = new InjectionToken('LAYOUT_SEARCH_ADAPTER');
755
+ @if (item().routerLink && !item().children?.length) {
756
+ <a
757
+ (click)="itemClick()"
758
+ [routerLink]="routerLink()"
759
+ routerLinkActive="active-route"
760
+ [routerLinkActiveOptions]="routerLinkActiveOptions()"
761
+ tabindex="0"
762
+ >
763
+ @if (item().icon) {
764
+ <lib-icon
765
+ [icon]="item().icon!"
766
+ [iconType]="item().iconType"
767
+ class="layout-menuitem-icon"
768
+ />
769
+ }
770
+ <span class="layout-menuitem-text">{{
771
+ item().labelKey ? (item().labelKey! | translate) : item().label
772
+ }}</span>
773
+ </a>
774
+ }
775
+ @if (item().children) {
776
+ <ul
777
+ (mouseenter)="onMouseEnter()"
778
+ [class.submenu-expanded]="active() && !layoutService.isTopbar()"
779
+ [class.submenu-collapsed]="!active() && !layoutService.isTopbar()"
780
+ >
781
+ @for (
782
+ child of item().children;
783
+ track 'menu' + child.id + '' + i;
784
+ let i = $index
785
+ ) {
786
+ <li
787
+ app-menuitem
788
+ [item]="child"
789
+ [index]="i"
790
+ [parentKey]="key()"
791
+ ></li>
792
+ }
793
+ </ul>
794
+ }
795
+ </ng-container>
796
+ `, host: {
797
+ '[class.active-menuitem]': 'active()',
798
+ }, 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"] }]
799
+ }], 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 }] }] } });
679
800
 
680
- class AppFloatingConfigurator {
801
+ /**
802
+ * Main menu component that displays filtered menu items.
803
+ * Menu is automatically filtered based on user permissions using the permission checker (logicNode pattern).
804
+ * The filtering happens internally in LayoutService via computed signal.
805
+ */
806
+ class AppMenu {
681
807
  layoutService = inject(LayoutService);
682
- languageSelectorPanel = inject(LAYOUT_LANGUAGE_SELECTOR_PANEL, {
683
- optional: true,
684
- });
685
- isDarkTheme = computed(() => this.layoutService.layoutConfig().darkTheme, /* @ts-ignore */
686
- ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : /* istanbul ignore next */ []));
687
- toggleDarkMode() {
688
- const currentDarkTheme = this.layoutService.layoutConfig().darkTheme;
689
- this.layoutService.updateLayoutConfig({ darkTheme: !currentDarkTheme });
690
- }
691
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFloatingConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
692
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppFloatingConfigurator, isStandalone: true, selector: "app-floating-configurator", ngImport: i0, template: `
693
- <div
694
- class="floating-configurator fixed flex flex-col md:flex-row gap-2 md:gap-4 top-4 md:top-8 z-50"
695
- >
696
- <button
697
- fButton
698
- type="button"
699
- (click)="toggleDarkMode()"
700
- [rounded]="true"
701
- severity="secondary"
702
- >
703
- <ui-icon pButtonIcon [name]="isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
704
- </button>
705
- @if (languageSelectorPanel) {
706
- <div class="relative">
707
- <button
708
- fButton
709
- fStyleClass="@next"
710
- enterFromClass="hidden"
711
- enterActiveClass="animate-scalein"
712
- leaveToClass="hidden"
713
- leaveActiveClass="animate-fadeout"
714
- [hideOnOutsideClick]="true"
715
- type="button"
716
- rounded
717
- >
718
- <ui-icon pButtonIcon name="globe"></ui-icon>
719
- </button>
720
- <ng-container *ngComponentOutlet="languageSelectorPanel" />
721
- </div>
808
+ /**
809
+ * Filtered menu items from layout service.
810
+ * This signal is computed in LayoutService and automatically updates when:
811
+ * - Raw menu changes (setMenu called)
812
+ * - Permission state changes (user permissions updated)
813
+ */
814
+ menuItems = this.layoutService.menu;
815
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
816
+ 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">
817
+ <ul>
818
+ @for (
819
+ item of menuItems();
820
+ track 'menu' + item.id + '' + i;
821
+ let i = $index
822
+ ) {
823
+ <li app-menuitem [item]="item" [index]="i"></li>
722
824
  }
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" }] });
825
+ </ul>
826
+ </div>`, isInline: true, dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "ngmodule", type: RouterModule }] });
741
827
  }
742
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFloatingConfigurator, decorators: [{
828
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenu, decorators: [{
743
829
  type: Component,
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" />
773
- </div>
830
+ args: [{
831
+ selector: 'app-menu',
832
+ imports: [AppMenuitem, RouterModule],
833
+ template: `<div class="layout-menu">
834
+ <ul>
835
+ @for (
836
+ item of menuItems();
837
+ track 'menu' + item.id + '' + i;
838
+ let i = $index
839
+ ) {
840
+ <li app-menuitem [item]="item" [index]="i"></li>
774
841
  }
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>
842
+ </ul>
843
+ </div>`,
844
+ }]
845
+ }] });
846
+
847
+ class AppSidebar {
848
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
849
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.6", type: AppSidebar, isStandalone: true, selector: "app-sidebar", ngImport: i0, template: `
850
+ <div class="layout-sidebar">
851
+ <app-menu />
791
852
  </div>
792
- `, styles: [".floating-configurator{inset-inline-end:.5rem}@media(min-width:768px){.floating-configurator{inset-inline-end:2rem}}\n"] }]
853
+ `, isInline: true, dependencies: [{ kind: "component", type: AppMenu, selector: "app-menu" }] });
854
+ }
855
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSidebar, decorators: [{
856
+ type: Component,
857
+ args: [{
858
+ selector: 'app-sidebar',
859
+ imports: [AppMenu],
860
+ template: `
861
+ <div class="layout-sidebar">
862
+ <app-menu />
863
+ </div>
864
+ `,
865
+ }]
793
866
  }] });
794
867
 
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
868
+ /**
869
+ * Home link showing the active company logo and name.
870
+ * Every skin renders this same markup; sizing and colour come from the bar it sits in.
871
+ */
872
+ class AppBrandLogo {
873
+ layoutService = inject(LayoutService);
874
+ companyName = this.layoutService.companyName;
875
+ displayLogo = this.layoutService.displayLogo;
876
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppBrandLogo, deps: [], target: i0.ɵɵFactoryTarget.Component });
877
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppBrandLogo, isStandalone: true, selector: "app-brand-logo", host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
878
+ <a class="layout-brand-logo" routerLink="/">
879
+ @if (displayLogo()) {
880
+ @if (layoutService.isDisplayLogoSvg()) {
881
+ <inline-svg [src]="displayLogo()" class="logo-image" />
882
+ } @else {
883
+ <img [src]="displayLogo()" [alt]="companyName()" class="logo-image" />
884
+ }
885
+ }
886
+ <span>{{ companyName() }}</span>
887
+ </a>
888
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: InlineSvgComponent, selector: "inline-svg", inputs: ["src"] }] });
889
+ }
890
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppBrandLogo, decorators: [{
891
+ type: Component,
892
+ args: [{
893
+ selector: 'app-brand-logo',
894
+ imports: [RouterModule, InlineSvgComponent],
895
+ host: { style: 'display: contents' },
896
+ template: `
897
+ <a class="layout-brand-logo" routerLink="/">
898
+ @if (displayLogo()) {
899
+ @if (layoutService.isDisplayLogoSvg()) {
900
+ <inline-svg [src]="displayLogo()" class="logo-image" />
901
+ } @else {
902
+ <img [src]="displayLogo()" [alt]="companyName()" class="logo-image" />
903
+ }
904
+ }
905
+ <span>{{ companyName() }}</span>
906
+ </a>
907
+ `,
908
+ }]
909
+ }] });
910
+
911
+ const LAYOUT_AUTH_STATE = new InjectionToken('LAYOUT_AUTH_STATE');
912
+ const LAYOUT_AUTH_API = new InjectionToken('LAYOUT_AUTH_API');
913
+ const LAYOUT_NOTIFICATION_BELL = new InjectionToken('LAYOUT_NOTIFICATION_BELL');
914
+ const LAYOUT_LANGUAGE_SELECTOR = new InjectionToken('LAYOUT_LANGUAGE_SELECTOR');
915
+ const LAYOUT_LANGUAGE_SELECTOR_PANEL = new InjectionToken('LAYOUT_LANGUAGE_SELECTOR_PANEL');
916
+ const LAYOUT_SEARCHBAR = new InjectionToken('LAYOUT_SEARCHBAR');
917
+ const LAYOUT_IS_RTL = new InjectionToken('LAYOUT_IS_RTL');
918
+ const LAYOUT_SEARCH_ADAPTER = new InjectionToken('LAYOUT_SEARCH_ADAPTER');
919
+
920
+ class AppConfigurator {
804
921
  router = inject(Router);
922
+ translateAdapter = inject(TranslateService, { optional: true });
923
+ platformId = inject(PLATFORM_ID);
805
924
  layoutService = inject(LayoutService);
806
- destroyRef = inject(DestroyRef);
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
- };
925
+ presets = PRESET_NAMES;
926
+ showMenuModeButton = signal(!this.router.url.includes('auth'), /* @ts-ignore */
927
+ ...(ngDevMode ? [{ debugName: "showMenuModeButton" }] : /* istanbul ignore next */ []));
928
+ menuModeOptions = computed(() => {
929
+ const options = [
930
+ { label: this.translate('layout.configurator.menu.mode.static'), value: 'static' },
931
+ { label: this.translate('layout.configurator.menu.mode.overlay'), value: 'overlay' },
932
+ ];
933
+ // Topbar mode renders a horizontal nav, which only the classic skin provides.
934
+ if (this.layoutService.isDesktop() && !this.layoutService.isModernLayout()) {
935
+ options.push({ label: this.translate('layout.configurator.menu.mode.topbar'), value: 'topbar' });
936
+ }
937
+ return options;
835
938
  }, /* @ts-ignore */
836
- ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : /* istanbul ignore next */ []));
939
+ ...(ngDevMode ? [{ debugName: "menuModeOptions" }] : /* istanbul ignore next */ []));
940
+ layoutThemeOptions = computed(() => [
941
+ { label: this.translate('layout.configurator.layout.theme.classic'), value: 'classic' },
942
+ { label: this.translate('layout.configurator.layout.theme.modern'), value: 'modern' },
943
+ ], /* @ts-ignore */
944
+ ...(ngDevMode ? [{ debugName: "layoutThemeOptions" }] : /* istanbul ignore next */ []));
837
945
  constructor() {
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
- }
946
+ if (isPlatformBrowser(this.platformId)) {
947
+ effect(() => {
948
+ const config = this.layoutService.layoutConfig();
949
+ applyTheme({
950
+ preset: (config.preset ?? 'Aura'),
951
+ primary: config.primary ?? 'violet',
952
+ surface: config.surface ?? null,
953
+ darkTheme: config.darkTheme ?? false,
954
+ });
852
955
  });
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());
877
- }
878
- });
879
- }
880
- onMouseEnter() {
881
- if (this.layoutService.isTopbar() && this.item().children) {
882
- this.updateSubmenuPosition();
883
956
  }
884
957
  }
885
- updateSubmenuPosition() {
886
- const linkEl = this.parentLink()?.nativeElement;
887
- if (!linkEl || typeof linkEl.getBoundingClientRect !== 'function')
888
- return;
889
- const rect = linkEl.getBoundingClientRect();
890
- const hostEl = linkEl.closest('[app-menuitem]');
891
- if (!hostEl)
892
- return;
893
- // Position submenu to the left of the parent link and above it
894
- const left = rect.left;
895
- const top = rect.top + 30; // 8px gap above the menu item
896
- hostEl.style.setProperty('--submenu-left', `${left}px`);
897
- hostEl.style.setProperty('--submenu-top', `${top}px`);
958
+ surfaces = Object.entries(SURFACE_PALETTES).map(([name, pal]) => ({
959
+ name,
960
+ palette: pal,
961
+ }));
962
+ selectedPrimaryColor = computed(() => this.layoutService.layoutConfig().primary, /* @ts-ignore */
963
+ ...(ngDevMode ? [{ debugName: "selectedPrimaryColor" }] : /* istanbul ignore next */ []));
964
+ selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface, /* @ts-ignore */
965
+ ...(ngDevMode ? [{ debugName: "selectedSurfaceColor" }] : /* istanbul ignore next */ []));
966
+ isSurfaceSelected(surfaceName) {
967
+ const sel = this.selectedSurfaceColor();
968
+ if (sel)
969
+ return sel === surfaceName;
970
+ const isDark = this.layoutService.layoutConfig().darkTheme;
971
+ return isDark ? surfaceName === 'zinc' : surfaceName === 'slate';
898
972
  }
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
- });
973
+ selectedPreset = computed(() => this.layoutService.layoutConfig().preset, /* @ts-ignore */
974
+ ...(ngDevMode ? [{ debugName: "selectedPreset" }] : /* istanbul ignore next */ []));
975
+ menuMode = computed(() => this.layoutService.layoutConfig().menuMode, /* @ts-ignore */
976
+ ...(ngDevMode ? [{ debugName: "menuMode" }] : /* istanbul ignore next */ []));
977
+ layoutTheme = this.layoutService.layoutTheme;
978
+ primaryColors = computed(() => {
979
+ const palettes = [{ name: 'noir', palette: {} }];
980
+ for (const [name, pal] of Object.entries(PRIMARY_PALETTES)) {
981
+ palettes.push({ name, palette: pal });
917
982
  }
983
+ return palettes;
984
+ }, /* @ts-ignore */
985
+ ...(ngDevMode ? [{ debugName: "primaryColors" }] : /* istanbul ignore next */ []));
986
+ updateColors(event, type, color) {
987
+ if (type === 'primary') {
988
+ this.layoutService.updateLayoutConfig({ primary: color.name });
989
+ }
990
+ else if (type === 'surface') {
991
+ this.layoutService.updateLayoutConfig({ surface: color.name });
992
+ }
993
+ this._applyColorChange(type, color);
994
+ event.stopPropagation();
918
995
  }
919
- itemClick() {
920
- if (this.item().children && !this.layoutService.isTopbar()) {
921
- this._active.update((prev) => !prev);
996
+ _applyColorChange(type, color) {
997
+ const isDark = this.layoutService.layoutConfig().darkTheme ?? false;
998
+ if (type === 'primary') {
999
+ const primary = color.name ?? 'violet';
1000
+ // NOIR_PRIMARY, not a copy of a surface palette: its shades are references to
1001
+ // the active surface ramp, which is what lets applyPrimaryPalette recognise the
1002
+ // neutral and derive the same tokens a preset switch to Nora would.
1003
+ const pal = primary === 'noir' ? NOIR_PRIMARY : PRIMARY_PALETTES[primary];
1004
+ if (pal)
1005
+ applyPrimaryPalette(pal, isDark);
1006
+ }
1007
+ else if (type === 'surface') {
1008
+ const surfacePal = SURFACE_PALETTES[color.name ?? 'slate'];
1009
+ if (surfacePal)
1010
+ applySurfacePalette(surfacePal);
922
1011
  }
923
- this.layoutService.onMenuStateChange({ key: this.key() });
924
1012
  }
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
- />
1013
+ onPresetChange(event) {
1014
+ const primary = PRESETS[event]?.signaturePrimary ?? 'violet';
1015
+ this.layoutService.updateLayoutConfig({ preset: event, primary });
1016
+ const isDark = this.layoutService.layoutConfig().darkTheme ?? false;
1017
+ const surface = this.layoutService.layoutConfig().surface ?? null;
1018
+ applyTheme({ preset: event, primary, surface, darkTheme: isDark });
1019
+ }
1020
+ onMenuModeChange(event) {
1021
+ this.layoutService.updateLayoutConfig({ menuMode: event });
1022
+ }
1023
+ onLayoutThemeChange(event) {
1024
+ const menuMode = this.layoutService.layoutConfig().menuMode;
1025
+ this.layoutService.updateLayoutConfig({
1026
+ layoutTheme: event,
1027
+ // Modern skin has no horizontal nav — fall back to the static sidebar.
1028
+ menuMode: event === 'modern' && menuMode === 'topbar' ? 'static' : menuMode,
1029
+ });
1030
+ }
1031
+ translate(key) {
1032
+ return this.translateAdapter?.translate(key) ?? key;
1033
+ }
1034
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
1035
+ 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: `
1036
+ <div class="flex flex-col gap-4">
1037
+ <div>
1038
+ <span class="text-sm text-muted-color font-semibold">{{
1039
+ 'layout.configurator.primary' | translate
1040
+ }}</span>
1041
+ <div class="pt-2 flex gap-2 flex-wrap justify-start">
1042
+ @for (primaryColor of primaryColors(); track primaryColor.name) {
1043
+ <button
1044
+ type="button"
1045
+ [title]="primaryColor.name"
1046
+ (click)="updateColors($event, 'primary', primaryColor)"
1047
+ [class.outline-primary]="primaryColor.name === selectedPrimaryColor()"
1048
+ class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
1049
+ [style.background-color]="
1050
+ primaryColor?.name === 'noir'
1051
+ ? 'var(--f-text-color)'
1052
+ : primaryColor?.palette?.['500']
1053
+ "
1054
+ ></button>
941
1055
  }
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
- }
1056
+ </div>
1057
+ </div>
1058
+ <div>
1059
+ <span class="text-sm text-muted-color font-semibold">{{
1060
+ 'layout.configurator.surface' | translate
1061
+ }}</span>
1062
+ <div class="pt-2 flex gap-2 flex-wrap justify-start">
1063
+ @for (surface of surfaces; track surface.name) {
1064
+ <button
1065
+ type="button"
1066
+ [title]="surface.name"
1067
+ (click)="updateColors($event, 'surface', surface)"
1068
+ [class.outline-primary]="isSurfaceSelected(surface.name)"
1069
+ class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
1070
+ [style.background-color]="
1071
+ surface?.name === 'noir'
1072
+ ? 'var(--f-text-color)'
1073
+ : surface?.palette?.['500']
1074
+ "
1075
+ ></button>
951
1076
  }
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
1077
+ </div>
1078
+ </div>
1079
+ <div class="flex flex-col gap-2">
1080
+ <span class="text-sm text-muted-color font-semibold">{{
1081
+ 'layout.configurator.presets' | translate
1082
+ }}</span>
1083
+ <f-selectbutton
1084
+ [options]="presets"
1085
+ [ngModel]="selectedPreset()"
1086
+ (ngModelChange)="onPresetChange($event)"
1087
+ [allowEmpty]="false"
1088
+ size="small"
1089
+ />
1090
+ </div>
1091
+ @if (showMenuModeButton()) {
1092
+ <div class="flex flex-col gap-2">
1093
+ <span class="text-sm text-muted-color font-semibold">{{
1094
+ 'layout.configurator.layout.theme' | translate
971
1095
  }}</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>
1096
+ <f-selectbutton
1097
+ [ngModel]="layoutTheme()"
1098
+ (ngModelChange)="onLayoutThemeChange($event)"
1099
+ [options]="layoutThemeOptions()"
1100
+ [allowEmpty]="false"
1101
+ size="small"
1102
+ />
1103
+ </div>
1104
+ <div class="flex flex-col gap-2">
1105
+ <span class="text-sm text-muted-color font-semibold">{{
1106
+ 'layout.configurator.menu.mode' | translate
1107
+ }}</span>
1108
+ <f-selectbutton
1109
+ [ngModel]="menuMode()"
1110
+ (ngModelChange)="onMenuModeChange($event)"
1111
+ [options]="menuModeOptions()"
1112
+ [allowEmpty]="false"
1113
+ size="small"
1114
+ />
1115
+ </div>
993
1116
  }
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" }] });
1117
+ </div>
1118
+ `, 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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
996
1119
  }
997
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppMenuitem, decorators: [{
1120
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppConfigurator, decorators: [{
998
1121
  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
- }
1122
+ args: [{
1123
+ selector: 'app-configurator',
1124
+ imports: [UiModule, FormsModule, TranslatePipe],
1125
+ template: `
1126
+ <div class="flex flex-col gap-4">
1127
+ <div>
1128
+ <span class="text-sm text-muted-color font-semibold">{{
1129
+ 'layout.configurator.primary' | translate
1130
+ }}</span>
1131
+ <div class="pt-2 flex gap-2 flex-wrap justify-start">
1132
+ @for (primaryColor of primaryColors(); track primaryColor.name) {
1133
+ <button
1134
+ type="button"
1135
+ [title]="primaryColor.name"
1136
+ (click)="updateColors($event, 'primary', primaryColor)"
1137
+ [class.outline-primary]="primaryColor.name === selectedPrimaryColor()"
1138
+ class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
1139
+ [style.background-color]="
1140
+ primaryColor?.name === 'noir'
1141
+ ? 'var(--f-text-color)'
1142
+ : primaryColor?.palette?.['500']
1143
+ "
1144
+ ></button>
1024
1145
  }
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
- />
1146
+ </div>
1147
+ </div>
1148
+ <div>
1149
+ <span class="text-sm text-muted-color font-semibold">{{
1150
+ 'layout.configurator.surface' | translate
1151
+ }}</span>
1152
+ <div class="pt-2 flex gap-2 flex-wrap justify-start">
1153
+ @for (surface of surfaces; track surface.name) {
1154
+ <button
1155
+ type="button"
1156
+ [title]="surface.name"
1157
+ (click)="updateColors($event, 'surface', surface)"
1158
+ [class.outline-primary]="isSurfaceSelected(surface.name)"
1159
+ class="border-none w-5 h-5 rounded-full p-0 cursor-pointer outline-none outline-offset-1"
1160
+ [style.background-color]="
1161
+ surface?.name === 'noir'
1162
+ ? 'var(--f-text-color)'
1163
+ : surface?.palette?.['500']
1164
+ "
1165
+ ></button>
1041
1166
  }
1042
- <span class="layout-menuitem-text">{{
1043
- item().labelKey ? (item().labelKey! | translate) : item().label
1167
+ </div>
1168
+ </div>
1169
+ <div class="flex flex-col gap-2">
1170
+ <span class="text-sm text-muted-color font-semibold">{{
1171
+ 'layout.configurator.presets' | translate
1172
+ }}</span>
1173
+ <f-selectbutton
1174
+ [options]="presets"
1175
+ [ngModel]="selectedPreset()"
1176
+ (ngModelChange)="onPresetChange($event)"
1177
+ [allowEmpty]="false"
1178
+ size="small"
1179
+ />
1180
+ </div>
1181
+ @if (showMenuModeButton()) {
1182
+ <div class="flex flex-col gap-2">
1183
+ <span class="text-sm text-muted-color font-semibold">{{
1184
+ 'layout.configurator.layout.theme' | translate
1044
1185
  }}</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>
1186
+ <f-selectbutton
1187
+ [ngModel]="layoutTheme()"
1188
+ (ngModelChange)="onLayoutThemeChange($event)"
1189
+ [options]="layoutThemeOptions()"
1190
+ [allowEmpty]="false"
1191
+ size="small"
1192
+ />
1193
+ </div>
1194
+ <div class="flex flex-col gap-2">
1195
+ <span class="text-sm text-muted-color font-semibold">{{
1196
+ 'layout.configurator.menu.mode' | translate
1197
+ }}</span>
1198
+ <f-selectbutton
1199
+ [ngModel]="menuMode()"
1200
+ (ngModelChange)="onMenuModeChange($event)"
1201
+ [options]="menuModeOptions()"
1202
+ [allowEmpty]="false"
1203
+ size="small"
1204
+ />
1205
+ </div>
1113
1206
  }
1114
- </ul>
1115
- </div>`,
1207
+ </div>
1208
+ `,
1209
+ host: {
1210
+ 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',
1211
+ style: 'background-color: var(--surface-overlay); inset-inline-end: 0;',
1212
+ },
1116
1213
  }]
1117
- }] });
1214
+ }], ctorParameters: () => [] });
1118
1215
 
1119
1216
  /**
1120
1217
  * View Transitions API type definitions
@@ -1137,7 +1234,7 @@ class AppCompanyBranchSelector {
1137
1234
  isActive = this._isActive.asReadonly();
1138
1235
  constructor() {
1139
1236
  fromEvent(this.document, 'click')
1140
- .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1237
+ .pipe(takeUntilDestroyed(this.destroyRef), filter$1(() => this.isActive()))
1141
1238
  .subscribe((event) => {
1142
1239
  const target = event.target;
1143
1240
  if (!this.elementRef.nativeElement.contains(target)) {
@@ -1355,7 +1452,7 @@ class AppCompanyBranchSelector {
1355
1452
  </div>
1356
1453
  </div>
1357
1454
  </div>
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", "appendTo", "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" }] });
1455
+ `, 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", "appendTo", "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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1359
1456
  }
1360
1457
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppCompanyBranchSelector, decorators: [{
1361
1458
  type: Component,
@@ -1464,7 +1561,7 @@ class AppLauncher {
1464
1561
  isActive = this._isActive.asReadonly();
1465
1562
  constructor() {
1466
1563
  fromEvent(this.document, 'click')
1467
- .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1564
+ .pipe(takeUntilDestroyed(this.destroyRef), filter$1(() => this.isActive()))
1468
1565
  .subscribe((event) => {
1469
1566
  const target = event.target;
1470
1567
  if (!this.elementRef.nativeElement.contains(target)) {
@@ -1617,7 +1714,7 @@ class AppProfile {
1617
1714
  isActive = this._isActive.asReadonly();
1618
1715
  constructor() {
1619
1716
  fromEvent(this.document, 'click')
1620
- .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1717
+ .pipe(takeUntilDestroyed(this.destroyRef), filter$1(() => this.isActive()))
1621
1718
  .subscribe((event) => {
1622
1719
  const target = event.target;
1623
1720
  if (!this.elementRef.nativeElement.contains(target)) {
@@ -1757,7 +1854,7 @@ class AppProfile {
1757
1854
  </div>
1758
1855
  </div>
1759
1856
  </div>
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" }] });
1857
+ `, 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.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" }] });
1761
1858
  }
1762
1859
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppProfile, decorators: [{
1763
1860
  type: Component,
@@ -2086,7 +2183,7 @@ class AppSearchbar {
2086
2183
  }
2087
2184
  }
2088
2185
  </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" }] });
2186
+ `, 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:var(--layout-shell-topbar-bottom, 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$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2090
2187
  }
2091
2188
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSearchbar, decorators: [{
2092
2189
  type: Component,
@@ -2194,10 +2291,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
2194
2291
  }
2195
2292
  }
2196
2293
  </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"] }]
2294
+ `, 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:var(--layout-shell-topbar-bottom, 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
2295
  }], ctorParameters: () => [], propDecorators: { searchWrapper: [{ type: i0.ViewChild, args: ['searchWrapper', { isSignal: true }] }], searchInput: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
2199
2296
 
2200
- class AppTopbar {
2297
+ /**
2298
+ * Trailing action rail of the topbar — search, theme controls and the widget menu.
2299
+ * Skins own the bar around it; the rail itself is identical in every skin, and
2300
+ * `display: contents` on the host keeps it a direct flex child of the bar.
2301
+ */
2302
+ class AppTopbarActions {
2201
2303
  appConfig = inject(APP_CONFIG);
2202
2304
  document = inject(DOCUMENT$1);
2203
2305
  destroyRef = inject(DestroyRef);
@@ -2211,111 +2313,34 @@ class AppTopbar {
2211
2313
  searchAdapter = inject(LAYOUT_SEARCH_ADAPTER, {
2212
2314
  optional: true,
2213
2315
  });
2214
- hasSearchAdapter = computed(() => !!this.searchAdapter, /* @ts-ignore */
2215
- ...(ngDevMode ? [{ debugName: "hasSearchAdapter" }] : /* istanbul ignore next */ []));
2216
2316
  configContainer = viewChild('configContainer', /* @ts-ignore */
2217
2317
  ...(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 */ []));
2318
+ _configPanelOpen = signal(false, /* @ts-ignore */
2319
+ ...(ngDevMode ? [{ debugName: "_configPanelOpen" }] : /* istanbul ignore next */ []));
2320
+ configPanelOpen = this._configPanelOpen.asReadonly();
2321
+ hasSearchAdapter = computed(() => !!this.searchAdapter, /* @ts-ignore */
2322
+ ...(ngDevMode ? [{ debugName: "hasSearchAdapter" }] : /* istanbul ignore next */ []));
2323
+ enableCompanyFeature = isCompanyFeatureEnabled(this.appConfig);
2222
2324
  constructor() {
2223
2325
  fromEvent(this.document, 'click')
2224
- .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.activePanel() !== null))
2326
+ .pipe(takeUntilDestroyed(this.destroyRef), filter$1(() => this._configPanelOpen()))
2225
2327
  .subscribe((event) => {
2226
- this.handleOutsideClick(event);
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));
2328
+ const configEl = this.configContainer()?.nativeElement;
2329
+ if (configEl && !configEl.contains(event.target)) {
2330
+ this._configPanelOpen.set(false);
2234
2331
  }
2235
2332
  });
2236
2333
  }
2237
- companyName = this.layoutService.companyName;
2238
- displayLogo = this.layoutService.displayLogo;
2239
- enableCompanyFeature = isCompanyFeatureEnabled(this.appConfig);
2240
2334
  toggleDarkMode() {
2241
- const currentDarkTheme = this.layoutService.layoutConfig().darkTheme;
2242
- this.layoutService.updateLayoutConfig({ darkTheme: !currentDarkTheme });
2243
- }
2244
- togglePanel(panel) {
2245
- this.activePanel.update((current) => (current === panel ? null : panel));
2246
- }
2247
- handleOutsideClick(event) {
2248
- const target = event.target;
2249
- const configEl = this.configContainer()?.nativeElement;
2250
- if (configEl && !configEl.contains(target)) {
2251
- this.activePanel.set(null);
2252
- }
2253
- }
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`);
2335
+ this.layoutService.updateLayoutConfig({
2336
+ darkTheme: !this.layoutService.layoutConfig().darkTheme,
2265
2337
  });
2266
2338
  }
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
- }
2282
- <a class="layout-topbar-logo" routerLink="/">
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>
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
- }
2305
- </div>
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>
2339
+ toggleConfigPanel() {
2340
+ this._configPanelOpen.update((open) => !open);
2317
2341
  }
2318
-
2342
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppTopbarActions, deps: [], target: i0.ɵɵFactoryTarget.Component });
2343
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppTopbarActions, isStandalone: true, selector: "app-topbar-actions", host: { styleAttribute: "display: contents" }, viewQueries: [{ propertyName: "configContainer", first: true, predicate: ["configContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
2319
2344
  <div class="layout-topbar-actions">
2320
2345
  @if (hasSearchAdapter()) {
2321
2346
  <app-searchbar />
@@ -2327,12 +2352,15 @@ class AppTopbar {
2327
2352
  [attr.aria-label]="'layout.topbar.toggle.dark.mode' | translate"
2328
2353
  (click)="toggleDarkMode()"
2329
2354
  >
2330
- <ui-icon [name]="layoutService.isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
2355
+ <ui-icon
2356
+ [name]="layoutService.isDarkTheme() ? 'moon' : 'sun'"
2357
+ ></ui-icon>
2331
2358
  </button>
2332
2359
  <div class="relative" #configContainer>
2333
2360
  <button
2361
+ type="button"
2334
2362
  class="layout-topbar-action"
2335
- [class.layout-topbar-action-highlight]="activePanel() === 'config'"
2363
+ [class.layout-topbar-action-highlight]="configPanelOpen()"
2336
2364
  [attr.aria-label]="'layout.topbar.open.theme.settings' | translate"
2337
2365
  fStyleClass="@next"
2338
2366
  enterFromClass="hidden"
@@ -2340,7 +2368,7 @@ class AppTopbar {
2340
2368
  leaveToClass="hidden"
2341
2369
  leaveActiveClass="animate-fadeout"
2342
2370
  [hideOnOutsideClick]="true"
2343
- (click)="togglePanel('config')"
2371
+ (click)="toggleConfigPanel()"
2344
2372
  >
2345
2373
  <ui-icon name="palette"></ui-icon>
2346
2374
  </button>
@@ -2349,6 +2377,7 @@ class AppTopbar {
2349
2377
  </div>
2350
2378
 
2351
2379
  <button
2380
+ type="button"
2352
2381
  class="layout-topbar-menu-button layout-topbar-action"
2353
2382
  [attr.aria-label]="'layout.topbar.more.options' | translate"
2354
2383
  fStyleClass="@next"
@@ -2379,73 +2408,24 @@ class AppTopbar {
2379
2408
  </div>
2380
2409
  </div>
2381
2410
  </div>
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" }] });
2411
+ `, 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: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { 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" }, { kind: "component", type: AppSearchbar, selector: "app-searchbar" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2383
2412
  }
2384
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppTopbar, decorators: [{
2413
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppTopbarActions, decorators: [{
2385
2414
  type: Component,
2386
2415
  args: [{
2387
- selector: 'app-topbar',
2388
- imports: [UiModule, RouterModule,
2416
+ selector: 'app-topbar-actions',
2417
+ imports: [
2418
+ UiModule,
2389
2419
  NgComponentOutlet,
2390
2420
  TranslatePipe,
2391
- InlineSvgComponent,
2392
2421
  AppConfigurator,
2393
- AppMenu,
2394
2422
  AppProfile,
2395
2423
  AppCompanyBranchSelector,
2396
2424
  AppLauncher,
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
- }
2412
- <a class="layout-topbar-logo" routerLink="/">
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>
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
- }
2435
- </div>
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
-
2425
+ AppSearchbar,
2426
+ ],
2427
+ host: { style: 'display: contents' },
2428
+ template: `
2449
2429
  <div class="layout-topbar-actions">
2450
2430
  @if (hasSearchAdapter()) {
2451
2431
  <app-searchbar />
@@ -2457,12 +2437,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
2457
2437
  [attr.aria-label]="'layout.topbar.toggle.dark.mode' | translate"
2458
2438
  (click)="toggleDarkMode()"
2459
2439
  >
2460
- <ui-icon [name]="layoutService.isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
2440
+ <ui-icon
2441
+ [name]="layoutService.isDarkTheme() ? 'moon' : 'sun'"
2442
+ ></ui-icon>
2461
2443
  </button>
2462
2444
  <div class="relative" #configContainer>
2463
2445
  <button
2446
+ type="button"
2464
2447
  class="layout-topbar-action"
2465
- [class.layout-topbar-action-highlight]="activePanel() === 'config'"
2448
+ [class.layout-topbar-action-highlight]="configPanelOpen()"
2466
2449
  [attr.aria-label]="'layout.topbar.open.theme.settings' | translate"
2467
2450
  fStyleClass="@next"
2468
2451
  enterFromClass="hidden"
@@ -2470,7 +2453,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
2470
2453
  leaveToClass="hidden"
2471
2454
  leaveActiveClass="animate-fadeout"
2472
2455
  [hideOnOutsideClick]="true"
2473
- (click)="togglePanel('config')"
2456
+ (click)="toggleConfigPanel()"
2474
2457
  >
2475
2458
  <ui-icon name="palette"></ui-icon>
2476
2459
  </button>
@@ -2479,6 +2462,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
2479
2462
  </div>
2480
2463
 
2481
2464
  <button
2465
+ type="button"
2482
2466
  class="layout-topbar-menu-button layout-topbar-action"
2483
2467
  [attr.aria-label]="'layout.topbar.more.options' | translate"
2484
2468
  fStyleClass="@next"
@@ -2509,109 +2493,149 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
2509
2493
  </div>
2510
2494
  </div>
2511
2495
  </div>
2512
- </div>`,
2513
- }]
2514
- }], ctorParameters: () => [], propDecorators: { configContainer: [{ type: i0.ViewChild, args: ['configContainer', { isSignal: true }] }], navContainer: [{ type: i0.ViewChild, args: ['navContainer', { isSignal: true }] }] } });
2515
-
2516
- class AppSidebar {
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: `
2519
- <div class="layout-sidebar">
2520
- <app-menu />
2521
- </div>
2522
- `, isInline: true, dependencies: [{ kind: "component", type: AppMenu, selector: "app-menu" }] });
2523
- }
2524
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppSidebar, decorators: [{
2525
- type: Component,
2526
- args: [{
2527
- selector: 'app-sidebar',
2528
- imports: [AppMenu],
2529
- template: `
2530
- <div class="layout-sidebar">
2531
- <app-menu />
2532
- </div>
2533
2496
  `,
2534
2497
  }]
2535
- }] });
2498
+ }], ctorParameters: () => [], propDecorators: { configContainer: [{ type: i0.ViewChild, args: ['configContainer', { isSignal: true }] }] } });
2536
2499
 
2537
- class AppLayout {
2538
- destroyRef = inject(DestroyRef);
2539
- document = inject(DOCUMENT);
2500
+ class AppTopbar {
2540
2501
  layoutService = inject(LayoutService);
2541
- renderer = inject(Renderer2);
2542
- router = inject(Router);
2543
- menuOutsideClickListener = null;
2502
+ navContainer = viewChild('navContainer', /* @ts-ignore */
2503
+ ...(ngDevMode ? [{ debugName: "navContainer" }] : /* istanbul ignore next */ []));
2544
2504
  constructor() {
2545
- this.layoutService.overlayOpen$
2546
- .pipe(takeUntilDestroyed(this.destroyRef))
2547
- .subscribe(() => {
2548
- if (!this.menuOutsideClickListener) {
2549
- this.menuOutsideClickListener = this.renderer.listen(this.document, 'click', (event) => {
2550
- if (this.isOutsideClicked(event)) {
2551
- this.hideMenu();
2552
- }
2553
- });
2554
- }
2555
- if (this.layoutService.layoutState().staticMenuMobileActive) {
2556
- this.blockBodyScroll();
2557
- }
2505
+ effect((onCleanup) => {
2506
+ const nav = this.navContainer();
2507
+ if (!nav)
2508
+ return;
2509
+ const subscription = fromEvent(nav.nativeElement, 'scroll').subscribe(() => this.recalculateHoveredSubmenu(nav.nativeElement));
2510
+ onCleanup(() => subscription.unsubscribe());
2558
2511
  });
2559
- this.router.events
2560
- .pipe(filter$1((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
2561
- .subscribe(() => this.hideMenu());
2562
- }
2563
- isOutsideClicked(event) {
2564
- const sidebarEl = this.document.querySelector('.layout-sidebar');
2565
- const topbarEl = this.document.querySelector('.layout-menu-button');
2566
- const topbarNavEl = this.document.querySelector('.layout-topbar-nav');
2567
- const eventTarget = event.target;
2568
- return !(sidebarEl?.isSameNode(eventTarget) ||
2569
- sidebarEl?.contains(eventTarget) ||
2570
- topbarEl?.isSameNode(eventTarget) ||
2571
- topbarEl?.contains(eventTarget) ||
2572
- topbarNavEl?.isSameNode(eventTarget) ||
2573
- topbarNavEl?.contains(eventTarget));
2574
2512
  }
2575
- hideMenu() {
2576
- this.layoutService.updateLayoutState({
2577
- overlayMenuActive: false,
2578
- staticMenuMobileActive: false,
2579
- menuHoverActive: false,
2513
+ /**
2514
+ * Horizontal-nav submenus are `position: fixed`, so scrolling the nav strip leaves
2515
+ * an open submenu behind — re-anchor it to the item it belongs to.
2516
+ */
2517
+ recalculateHoveredSubmenu(navEl) {
2518
+ navEl.querySelectorAll('li:hover').forEach((li) => {
2519
+ const link = li.querySelector(':scope > a');
2520
+ if (!link)
2521
+ return;
2522
+ const rect = link.getBoundingClientRect();
2523
+ li.style.setProperty('--submenu-left', `${rect.left - 12}px`);
2524
+ li.style.setProperty('--submenu-top', `${rect.bottom + 12}px`);
2580
2525
  });
2581
- if (this.menuOutsideClickListener) {
2582
- this.menuOutsideClickListener();
2583
- this.menuOutsideClickListener = null;
2584
- }
2585
- this.unblockBodyScroll();
2586
2526
  }
2587
- blockBodyScroll() {
2588
- this.document.body.classList.add('blocked-scroll');
2527
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2528
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppTopbar, isStandalone: true, selector: "app-topbar", viewQueries: [{ propertyName: "navContainer", first: true, predicate: ["navContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `<div class="layout-topbar">
2529
+ <div
2530
+ class="layout-topbar-logo-container"
2531
+ [class.layout-topbar-logo-container-desktop]="layoutService.isDesktop()"
2532
+ >
2533
+ @if (layoutService.isMobile()) {
2534
+ <button
2535
+ type="button"
2536
+ class="layout-menu-button layout-topbar-action"
2537
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2538
+ (click)="layoutService.onMenuToggle()"
2539
+ >
2540
+ <ui-icon name="bars"></ui-icon>
2541
+ </button>
2542
+ }
2543
+ <app-brand-logo />
2544
+ @if (layoutService.isDesktop()) {
2545
+ <button
2546
+ type="button"
2547
+ class="layout-menu-button layout-topbar-action layout-topbar-action-right"
2548
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2549
+ (click)="layoutService.onMenuToggle()"
2550
+ >
2551
+ <ui-icon name="bars"></ui-icon>
2552
+ </button>
2553
+ }
2554
+ </div>
2555
+
2556
+ @if (layoutService.isTopbar() && layoutService.isDesktop()) {
2557
+ <div
2558
+ #navContainer
2559
+ class="layout-topbar-nav-horizontal"
2560
+ [class.layout-topbar-nav-hidden]="
2561
+ !layoutService.layoutState().topbarMenuVisible
2562
+ "
2563
+ >
2564
+ <app-menu />
2565
+ </div>
2589
2566
  }
2590
- unblockBodyScroll() {
2591
- this.document.body.classList.remove('blocked-scroll');
2567
+
2568
+ <app-topbar-actions />
2569
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "component", type: AppBrandLogo, selector: "app-brand-logo" }, { kind: "component", type: AppMenu, selector: "app-menu" }, { kind: "component", type: AppTopbarActions, selector: "app-topbar-actions" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2570
+ }
2571
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppTopbar, decorators: [{
2572
+ type: Component,
2573
+ args: [{
2574
+ selector: 'app-topbar',
2575
+ imports: [UiModule, TranslatePipe, AppBrandLogo, AppMenu, AppTopbarActions],
2576
+ template: `<div class="layout-topbar">
2577
+ <div
2578
+ class="layout-topbar-logo-container"
2579
+ [class.layout-topbar-logo-container-desktop]="layoutService.isDesktop()"
2580
+ >
2581
+ @if (layoutService.isMobile()) {
2582
+ <button
2583
+ type="button"
2584
+ class="layout-menu-button layout-topbar-action"
2585
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2586
+ (click)="layoutService.onMenuToggle()"
2587
+ >
2588
+ <ui-icon name="bars"></ui-icon>
2589
+ </button>
2590
+ }
2591
+ <app-brand-logo />
2592
+ @if (layoutService.isDesktop()) {
2593
+ <button
2594
+ type="button"
2595
+ class="layout-menu-button layout-topbar-action layout-topbar-action-right"
2596
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2597
+ (click)="layoutService.onMenuToggle()"
2598
+ >
2599
+ <ui-icon name="bars"></ui-icon>
2600
+ </button>
2601
+ }
2602
+ </div>
2603
+
2604
+ @if (layoutService.isTopbar() && layoutService.isDesktop()) {
2605
+ <div
2606
+ #navContainer
2607
+ class="layout-topbar-nav-horizontal"
2608
+ [class.layout-topbar-nav-hidden]="
2609
+ !layoutService.layoutState().topbarMenuVisible
2610
+ "
2611
+ >
2612
+ <app-menu />
2613
+ </div>
2592
2614
  }
2593
- isOverlay = computed(() => this.layoutService.layoutConfig().menuMode === 'overlay', /* @ts-ignore */
2594
- ...(ngDevMode ? [{ debugName: "isOverlay" }] : /* istanbul ignore next */ []));
2615
+
2616
+ <app-topbar-actions />
2617
+ </div>`,
2618
+ }]
2619
+ }], ctorParameters: () => [], propDecorators: { navContainer: [{ type: i0.ViewChild, args: ['navContainer', { isSignal: true }] }] } });
2620
+
2621
+ class AppClassicLayout extends LayoutShellBase {
2595
2622
  isStatic = computed(() => this.layoutService.layoutConfig().menuMode === 'static', /* @ts-ignore */
2596
2623
  ...(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 */
2624
+ /** Topbar mode without its nav row leaves the content sitting under the bar alone. */
2625
+ isTopbarNavHidden = computed(() => this.isTopbarMode() &&
2626
+ !this.layoutService.layoutState().topbarMenuVisible, /* @ts-ignore */
2606
2627
  ...(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: `
2628
+ outsideClickSelectors() {
2629
+ return ['.layout-sidebar', '.layout-menu-button', '.layout-topbar-nav'];
2630
+ }
2631
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppClassicLayout, deps: null, target: i0.ɵɵFactoryTarget.Component });
2632
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.6", type: AppClassicLayout, isStandalone: true, selector: "app-classic-layout", usesInheritance: true, ngImport: i0, template: `
2609
2633
  <div
2610
2634
  class="layout-wrapper"
2611
2635
  [class.layout-overlay]="isOverlay()"
2612
2636
  [class.layout-static]="isStatic()"
2613
2637
  [class.layout-topbar-mode]="isTopbarMode()"
2614
- [class.layout-static-inactive]="isStaticInactive()"
2638
+ [class.layout-static-inactive]="isDesktopCollapsed()"
2615
2639
  [class.layout-overlay-active]="isOverlayActive()"
2616
2640
  [class.layout-mobile-active]="isMobileActive()"
2617
2641
  [class.layout-topbar-nav-hidden]="isTopbarNavHidden()"
@@ -2626,12 +2650,12 @@ class AppLayout {
2626
2650
  </div>
2627
2651
  <div class="layout-mask animate-fadein"></div>
2628
2652
  </div>
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" }] });
2653
+ `, 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.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: AppFooter, selector: "app-footer" }] });
2630
2654
  }
2631
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLayout, decorators: [{
2655
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppClassicLayout, decorators: [{
2632
2656
  type: Component,
2633
2657
  args: [{
2634
- selector: 'app-layout',
2658
+ selector: 'app-classic-layout',
2635
2659
  imports: [AppTopbar, AppSidebar, RouterModule, AppFooter],
2636
2660
  template: `
2637
2661
  <div
@@ -2639,7 +2663,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
2639
2663
  [class.layout-overlay]="isOverlay()"
2640
2664
  [class.layout-static]="isStatic()"
2641
2665
  [class.layout-topbar-mode]="isTopbarMode()"
2642
- [class.layout-static-inactive]="isStaticInactive()"
2666
+ [class.layout-static-inactive]="isDesktopCollapsed()"
2643
2667
  [class.layout-overlay-active]="isOverlayActive()"
2644
2668
  [class.layout-mobile-active]="isMobileActive()"
2645
2669
  [class.layout-topbar-nav-hidden]="isTopbarNavHidden()"
@@ -2654,10 +2678,357 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
2654
2678
  </div>
2655
2679
  <div class="layout-mask animate-fadein"></div>
2656
2680
  </div>
2681
+ `,
2682
+ }]
2683
+ }] });
2684
+
2685
+ class AppModernSidebar {
2686
+ layoutService = inject(LayoutService);
2687
+ /**
2688
+ * Only the in-flow column can be collapsed from here. As an overlay or mobile
2689
+ * drawer the sidebar is opened by the topbar button and dismissed by the mask,
2690
+ * so a second toggle inside it would be dead weight next to the brand.
2691
+ */
2692
+ showMenuButton = computed(() => !this.layoutService.isMobile() && !this.layoutService.isOverlay(), /* @ts-ignore */
2693
+ ...(ngDevMode ? [{ debugName: "showMenuButton" }] : /* istanbul ignore next */ []));
2694
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppModernSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2695
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppModernSidebar, isStandalone: true, selector: "app-modern-sidebar", ngImport: i0, template: `
2696
+ <aside class="layout-modern-sidebar">
2697
+ <div class="layout-modern-brand">
2698
+ <app-brand-logo />
2699
+ @if (showMenuButton()) {
2700
+ <button
2701
+ type="button"
2702
+ class="layout-modern-menu-button"
2703
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2704
+ (click)="layoutService.onMenuToggle()"
2705
+ >
2706
+ <ui-icon name="bars"></ui-icon>
2707
+ </button>
2708
+ }
2709
+ </div>
2710
+
2711
+ <app-menu />
2712
+
2713
+ <div class="layout-modern-sidebar-footer">
2714
+ <app-footer />
2715
+ </div>
2716
+ </aside>
2717
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "component", type: AppBrandLogo, selector: "app-brand-logo" }, { kind: "component", type: AppMenu, selector: "app-menu" }, { kind: "component", type: AppFooter, selector: "app-footer" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2718
+ }
2719
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppModernSidebar, decorators: [{
2720
+ type: Component,
2721
+ args: [{
2722
+ selector: 'app-modern-sidebar',
2723
+ imports: [UiModule, TranslatePipe, AppBrandLogo, AppMenu, AppFooter],
2724
+ template: `
2725
+ <aside class="layout-modern-sidebar">
2726
+ <div class="layout-modern-brand">
2727
+ <app-brand-logo />
2728
+ @if (showMenuButton()) {
2729
+ <button
2730
+ type="button"
2731
+ class="layout-modern-menu-button"
2732
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2733
+ (click)="layoutService.onMenuToggle()"
2734
+ >
2735
+ <ui-icon name="bars"></ui-icon>
2736
+ </button>
2737
+ }
2738
+ </div>
2739
+
2740
+ <app-menu />
2741
+
2742
+ <div class="layout-modern-sidebar-footer">
2743
+ <app-footer />
2744
+ </div>
2745
+ </aside>
2746
+ `,
2747
+ }]
2748
+ }] });
2749
+
2750
+ class AppModernTopbar {
2751
+ destroyRef = inject(DestroyRef);
2752
+ router = inject(Router);
2753
+ layoutService = inject(LayoutService);
2754
+ _pageTitle = signal('', /* @ts-ignore */
2755
+ ...(ngDevMode ? [{ debugName: "_pageTitle" }] : /* istanbul ignore next */ []));
2756
+ pageTitle = this._pageTitle.asReadonly();
2757
+ /** The sidebar is a drawer (not a column) in these modes, so the topbar owns the brand. */
2758
+ showBrand = computed(() => this.layoutService.isMobile() ||
2759
+ this.layoutService.isOverlay() ||
2760
+ !!this.layoutService.layoutState().staticMenuDesktopInactive, /* @ts-ignore */
2761
+ ...(ngDevMode ? [{ debugName: "showBrand" }] : /* istanbul ignore next */ []));
2762
+ constructor() {
2763
+ this.resolvePageTitle();
2764
+ this.router.events
2765
+ .pipe(filter$1((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
2766
+ .subscribe(() => this.resolvePageTitle());
2767
+ }
2768
+ resolvePageTitle() {
2769
+ let route = this.router.routerState.snapshot.root;
2770
+ while (route.firstChild) {
2771
+ route = route.firstChild;
2772
+ }
2773
+ this._pageTitle.set(route.title ?? '');
2774
+ }
2775
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppModernTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2776
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppModernTopbar, isStandalone: true, selector: "app-modern-topbar", ngImport: i0, template: `
2777
+ <header class="layout-modern-topbar">
2778
+ @if (showBrand()) {
2779
+ <button
2780
+ type="button"
2781
+ class="layout-modern-menu-button layout-topbar-action"
2782
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2783
+ (click)="layoutService.onMenuToggle()"
2784
+ >
2785
+ <ui-icon name="bars"></ui-icon>
2786
+ </button>
2787
+
2788
+ <app-brand-logo />
2789
+ }
2790
+
2791
+ <span class="layout-modern-page-title">{{ pageTitle() }}</span>
2792
+
2793
+ <app-topbar-actions />
2794
+ </header>
2795
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "component", type: i1.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "component", type: AppBrandLogo, selector: "app-brand-logo" }, { kind: "component", type: AppTopbarActions, selector: "app-topbar-actions" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2796
+ }
2797
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppModernTopbar, decorators: [{
2798
+ type: Component,
2799
+ args: [{
2800
+ selector: 'app-modern-topbar',
2801
+ imports: [UiModule, TranslatePipe, AppBrandLogo, AppTopbarActions],
2802
+ template: `
2803
+ <header class="layout-modern-topbar">
2804
+ @if (showBrand()) {
2805
+ <button
2806
+ type="button"
2807
+ class="layout-modern-menu-button layout-topbar-action"
2808
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2809
+ (click)="layoutService.onMenuToggle()"
2810
+ >
2811
+ <ui-icon name="bars"></ui-icon>
2812
+ </button>
2813
+
2814
+ <app-brand-logo />
2815
+ }
2816
+
2817
+ <span class="layout-modern-page-title">{{ pageTitle() }}</span>
2818
+
2819
+ <app-topbar-actions />
2820
+ </header>
2657
2821
  `,
2658
2822
  }]
2659
2823
  }], ctorParameters: () => [] });
2660
2824
 
2825
+ class AppModernLayout extends LayoutShellBase {
2826
+ content = viewChild('content', /* @ts-ignore */
2827
+ ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
2828
+ outsideClickSelectors() {
2829
+ return ['.layout-modern-sidebar', '.layout-modern-menu-button'];
2830
+ }
2831
+ /**
2832
+ * The page scrolls inside the content card rather than the window, so the router's
2833
+ * scroll restoration never reaches it — reset it here instead.
2834
+ */
2835
+ onNavigated() {
2836
+ this.content()?.nativeElement.scrollTo({ top: 0 });
2837
+ }
2838
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppModernLayout, deps: null, target: i0.ɵɵFactoryTarget.Component });
2839
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.6", type: AppModernLayout, isStandalone: true, selector: "app-modern-layout", viewQueries: [{ propertyName: "content", first: true, predicate: ["content"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
2840
+ <div
2841
+ class="layout-modern"
2842
+ [class.layout-modern-overlay]="isOverlay()"
2843
+ [class.layout-modern-static-inactive]="isDesktopCollapsed()"
2844
+ [class.layout-modern-overlay-active]="isOverlayActive()"
2845
+ [class.layout-modern-mobile-active]="isMobileActive()"
2846
+ >
2847
+ <app-modern-sidebar />
2848
+ <div class="layout-modern-main">
2849
+ <app-modern-topbar />
2850
+ <div class="layout-modern-content" #content>
2851
+ <router-outlet></router-outlet>
2852
+ </div>
2853
+ </div>
2854
+ <div class="layout-modern-mask animate-fadein"></div>
2855
+ </div>
2856
+ `, isInline: true, dependencies: [{ kind: "component", type: AppModernTopbar, selector: "app-modern-topbar" }, { kind: "component", type: AppModernSidebar, selector: "app-modern-sidebar" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }] });
2857
+ }
2858
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppModernLayout, decorators: [{
2859
+ type: Component,
2860
+ args: [{
2861
+ selector: 'app-modern-layout',
2862
+ imports: [AppModernTopbar, AppModernSidebar, RouterModule],
2863
+ template: `
2864
+ <div
2865
+ class="layout-modern"
2866
+ [class.layout-modern-overlay]="isOverlay()"
2867
+ [class.layout-modern-static-inactive]="isDesktopCollapsed()"
2868
+ [class.layout-modern-overlay-active]="isOverlayActive()"
2869
+ [class.layout-modern-mobile-active]="isMobileActive()"
2870
+ >
2871
+ <app-modern-sidebar />
2872
+ <div class="layout-modern-main">
2873
+ <app-modern-topbar />
2874
+ <div class="layout-modern-content" #content>
2875
+ <router-outlet></router-outlet>
2876
+ </div>
2877
+ </div>
2878
+ <div class="layout-modern-mask animate-fadein"></div>
2879
+ </div>
2880
+ `,
2881
+ }]
2882
+ }], propDecorators: { content: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }] } });
2883
+
2884
+ /**
2885
+ * Layout shell entry point.
2886
+ * Renders the skin selected by `LayoutConfig.layoutTheme` — every skin shares
2887
+ * the same services, menu and topbar components, only the markup and styling differ.
2888
+ */
2889
+ class AppLayout {
2890
+ layoutService = inject(LayoutService);
2891
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
2892
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppLayout, isStandalone: true, selector: "app-layout", ngImport: i0, template: `
2893
+ @if (layoutService.isModernLayout()) {
2894
+ <app-modern-layout />
2895
+ } @else {
2896
+ <app-classic-layout />
2897
+ }
2898
+ `, isInline: true, dependencies: [{ kind: "component", type: AppClassicLayout, selector: "app-classic-layout" }, { kind: "component", type: AppModernLayout, selector: "app-modern-layout" }] });
2899
+ }
2900
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppLayout, decorators: [{
2901
+ type: Component,
2902
+ args: [{
2903
+ selector: 'app-layout',
2904
+ imports: [AppClassicLayout, AppModernLayout],
2905
+ template: `
2906
+ @if (layoutService.isModernLayout()) {
2907
+ <app-modern-layout />
2908
+ } @else {
2909
+ <app-classic-layout />
2910
+ }
2911
+ `,
2912
+ }]
2913
+ }] });
2914
+
2915
+ class AppFloatingConfigurator {
2916
+ layoutService = inject(LayoutService);
2917
+ languageSelectorPanel = inject(LAYOUT_LANGUAGE_SELECTOR_PANEL, {
2918
+ optional: true,
2919
+ });
2920
+ isDarkTheme = computed(() => this.layoutService.layoutConfig().darkTheme, /* @ts-ignore */
2921
+ ...(ngDevMode ? [{ debugName: "isDarkTheme" }] : /* istanbul ignore next */ []));
2922
+ toggleDarkMode() {
2923
+ const currentDarkTheme = this.layoutService.layoutConfig().darkTheme;
2924
+ this.layoutService.updateLayoutConfig({ darkTheme: !currentDarkTheme });
2925
+ }
2926
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFloatingConfigurator, deps: [], target: i0.ɵɵFactoryTarget.Component });
2927
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: AppFloatingConfigurator, isStandalone: true, selector: "app-floating-configurator", ngImport: i0, template: `
2928
+ <div
2929
+ class="floating-configurator fixed flex flex-col md:flex-row gap-2 md:gap-4 top-4 md:top-8 z-50"
2930
+ >
2931
+ <button
2932
+ fButton
2933
+ type="button"
2934
+ (click)="toggleDarkMode()"
2935
+ [rounded]="true"
2936
+ severity="secondary"
2937
+ >
2938
+ <ui-icon pButtonIcon [name]="isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
2939
+ </button>
2940
+ @if (languageSelectorPanel) {
2941
+ <div class="relative">
2942
+ <button
2943
+ fButton
2944
+ fStyleClass="@next"
2945
+ enterFromClass="hidden"
2946
+ enterActiveClass="animate-scalein"
2947
+ leaveToClass="hidden"
2948
+ leaveActiveClass="animate-fadeout"
2949
+ [hideOnOutsideClick]="true"
2950
+ type="button"
2951
+ rounded
2952
+ >
2953
+ <ui-icon pButtonIcon name="globe"></ui-icon>
2954
+ </button>
2955
+ <ng-container *ngComponentOutlet="languageSelectorPanel" />
2956
+ </div>
2957
+ }
2958
+ <div class="relative">
2959
+ <button
2960
+ fButton
2961
+ fStyleClass="@next"
2962
+ enterFromClass="hidden"
2963
+ enterActiveClass="animate-scalein"
2964
+ leaveToClass="hidden"
2965
+ leaveActiveClass="animate-fadeout"
2966
+ [hideOnOutsideClick]="true"
2967
+ type="button"
2968
+ rounded
2969
+ >
2970
+ <ui-icon pButtonIcon name="palette"></ui-icon>
2971
+ </button>
2972
+ <app-configurator />
2973
+ </div>
2974
+ </div>
2975
+ `, 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" }] });
2976
+ }
2977
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: AppFloatingConfigurator, decorators: [{
2978
+ type: Component,
2979
+ args: [{ selector: 'app-floating-configurator', imports: [NgComponentOutlet, UiModule, AppConfigurator], template: `
2980
+ <div
2981
+ class="floating-configurator fixed flex flex-col md:flex-row gap-2 md:gap-4 top-4 md:top-8 z-50"
2982
+ >
2983
+ <button
2984
+ fButton
2985
+ type="button"
2986
+ (click)="toggleDarkMode()"
2987
+ [rounded]="true"
2988
+ severity="secondary"
2989
+ >
2990
+ <ui-icon pButtonIcon [name]="isDarkTheme() ? 'moon' : 'sun'"></ui-icon>
2991
+ </button>
2992
+ @if (languageSelectorPanel) {
2993
+ <div class="relative">
2994
+ <button
2995
+ fButton
2996
+ fStyleClass="@next"
2997
+ enterFromClass="hidden"
2998
+ enterActiveClass="animate-scalein"
2999
+ leaveToClass="hidden"
3000
+ leaveActiveClass="animate-fadeout"
3001
+ [hideOnOutsideClick]="true"
3002
+ type="button"
3003
+ rounded
3004
+ >
3005
+ <ui-icon pButtonIcon name="globe"></ui-icon>
3006
+ </button>
3007
+ <ng-container *ngComponentOutlet="languageSelectorPanel" />
3008
+ </div>
3009
+ }
3010
+ <div class="relative">
3011
+ <button
3012
+ fButton
3013
+ fStyleClass="@next"
3014
+ enterFromClass="hidden"
3015
+ enterActiveClass="animate-scalein"
3016
+ leaveToClass="hidden"
3017
+ leaveActiveClass="animate-fadeout"
3018
+ [hideOnOutsideClick]="true"
3019
+ type="button"
3020
+ rounded
3021
+ >
3022
+ <ui-icon pButtonIcon name="palette"></ui-icon>
3023
+ </button>
3024
+ <app-configurator />
3025
+ </div>
3026
+ </div>
3027
+ `, styles: [".floating-configurator{inset-inline-end:.5rem}@media(min-width:768px){.floating-configurator{inset-inline-end:2rem}}\n"] }]
3028
+ }] });
3029
+
3030
+ // Shell entry point (renders the configured layout skin)
3031
+
2661
3032
  function buildPreset(presetName, primary) {
2662
3033
  return { preset: presetName, primary };
2663
3034
  }
@@ -2673,7 +3044,7 @@ const GreenTheme = {
2673
3044
  };
2674
3045
 
2675
3046
  const LAYOUT_MESSAGES = {
2676
- // components/top-bar/app.profile.ts
3047
+ // components/shared/top-bar/app.profile.ts
2677
3048
  'layout.profile.title': 'Profile',
2678
3049
  'layout.profile.profile.picture.alt': 'Profile picture',
2679
3050
  'layout.profile.copy.sign.up.link': 'Copy SignUp Link',
@@ -2682,11 +3053,11 @@ const LAYOUT_MESSAGES = {
2682
3053
  'layout.profile.link.copied': 'Link Copied',
2683
3054
  'layout.profile.sign.up.link.copied': 'Sign Up Link copied.',
2684
3055
  'layout.profile.failed.copy.sign.up.link': 'Failed to copy signup link.',
2685
- // components/top-bar/app.launcher.ts
3056
+ // components/shared/top-bar/app.launcher.ts
2686
3057
  'layout.launcher.apps': 'Apps',
2687
3058
  'layout.launcher.applications': 'Applications',
2688
3059
  'layout.launcher.no.apps': 'No apps available',
2689
- // components/top-bar/app.company-branch-selector.ts
3060
+ // components/shared/top-bar/app.company-branch-selector.ts
2690
3061
  'layout.company.branch.selector.title': 'Switch Company & Branch',
2691
3062
  'layout.company.branch.selector.select.company': 'Select Company',
2692
3063
  'layout.company.branch.selector.branch': 'Branch',
@@ -2695,15 +3066,17 @@ const LAYOUT_MESSAGES = {
2695
3066
  'layout.company.branch.selector.no.company': 'No Company',
2696
3067
  'layout.company.branch.selector.branch.required': 'Branch Required',
2697
3068
  'layout.company.branch.selector.please.select.branch': 'Please select a branch',
2698
- // components/top-bar/app.searchbar.ts
3069
+ // components/shared/top-bar/app.searchbar.ts
2699
3070
  'layout.topbar.search': 'Search',
2700
3071
  'layout.topbar.search.placeholder': 'Search for content...',
2701
- // components/app.topbar.ts
3072
+ // components/layouts/classic/app.topbar.ts + components/layouts/modern/app.modern-topbar.ts
3073
+ // + components/layouts/modern/app.modern-sidebar.ts
2702
3074
  'layout.topbar.toggle.menu': 'Toggle menu',
3075
+ // components/shared/top-bar/app.topbar-actions.ts
2703
3076
  'layout.topbar.toggle.dark.mode': 'Toggle dark mode',
2704
3077
  'layout.topbar.open.theme.settings': 'Open theme settings',
2705
3078
  'layout.topbar.more.options': 'More options',
2706
- // components/app.configurator.ts
3079
+ // components/shared/app.configurator.ts
2707
3080
  'layout.configurator.primary': 'Primary',
2708
3081
  'layout.configurator.surface': 'Surface',
2709
3082
  'layout.configurator.presets': 'Presets',
@@ -2711,7 +3084,10 @@ const LAYOUT_MESSAGES = {
2711
3084
  'layout.configurator.menu.mode.static': 'Static',
2712
3085
  'layout.configurator.menu.mode.overlay': 'Overlay',
2713
3086
  'layout.configurator.menu.mode.topbar': 'Topbar',
2714
- // components/app.footer.ts
3087
+ 'layout.configurator.layout.theme': 'Layout',
3088
+ 'layout.configurator.layout.theme.classic': 'Classic',
3089
+ 'layout.configurator.layout.theme.modern': 'Modern',
3090
+ // components/shared/app.footer.ts
2715
3091
  'layout.footer.by': 'by',
2716
3092
  // app-menu.config.ts (main app menu configuration)
2717
3093
  'menu.dashboard': 'Dashboard',
@@ -2761,5 +3137,5 @@ const LAYOUT_MESSAGES = {
2761
3137
  * Generated bundle index. Do not edit.
2762
3138
  */
2763
3139
 
2764
- 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 };
3140
+ export { AppBrandLogo, AppClassicLayout, AppCompanyBranchSelector, AppConfigurator, AppFloatingConfigurator, AppFooter, AppLauncher, AppLayout, AppMenu, AppMenuitem, AppModernLayout, AppModernSidebar, AppModernTopbar, AppProfile, AppSearchbar, AppSidebar, AppTopbar, AppTopbarActions, 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, LayoutShellBase, NavyBlueTheme, buildPreset, filterAppsByPermissions, filterMenuByPermissions };
2765
3141
  //# sourceMappingURL=flusys-ng-layout.mjs.map