@flusys/ng-layout 6.3.0 → 6.4.1

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