@flusys/ng-layout 4.0.0-rc → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,14 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, PLATFORM_ID, Injectable, DOCUMENT, signal, computed, effect, Component, afterNextRender, InjectionToken, DestroyRef, ElementRef, viewChild, input, Renderer2 } from '@angular/core';
3
- import * as i2$2 from '@flusys/ng-shared';
4
- import { evaluateLogicNode, PermissionValidatorService, TranslatePipe, AngularModule, IconComponent } from '@flusys/ng-shared';
2
+ import { inject, PLATFORM_ID, Injectable, DOCUMENT, signal, computed, effect, Component, afterNextRender, InjectionToken, input, DestroyRef, viewChild, ElementRef, Renderer2 } from '@angular/core';
3
+ import * as i2$3 from '@flusys/ng-shared';
4
+ import { evaluateLogicNode, PermissionValidatorService, TranslatePipe, IconComponent, AngularModule, PrimeModule } from '@flusys/ng-shared';
5
5
  import { isPlatformBrowser, NgComponentOutlet, DOCUMENT as DOCUMENT$1, NgClass } from '@angular/common';
6
- import { APP_CONFIG, TRANSLATE_ADAPTER, DEFAULT_APP_NAME, DEFAULT_AUTHOR, isCompanyFeatureEnabled } from '@flusys/ng-core';
6
+ import { APP_CONFIG, TRANSLATE_ADAPTER, DEFAULT_APP_NAME, DEFAULT_AUTHOR, isCompanyFeatureEnabled, isSignUpEnabled } from '@flusys/ng-core';
7
7
  import { Subject, fromEvent, filter as filter$1 } from 'rxjs';
8
8
  import * as i1 from '@angular/forms';
9
9
  import { FormsModule } from '@angular/forms';
10
10
  import * as i1$2 from '@angular/router';
11
- import { Router, RouterModule, NavigationEnd } from '@angular/router';
11
+ import { Router, NavigationEnd, RouterModule } from '@angular/router';
12
12
  import { updatePreset, updateSurfacePalette, $t, definePreset } from '@primeuix/themes';
13
13
  import Aura from '@primeuix/themes/aura';
14
14
  import Lara from '@primeuix/themes/lara';
@@ -21,11 +21,14 @@ import * as i2$1 from 'primeng/styleclass';
21
21
  import { StyleClassModule } from 'primeng/styleclass';
22
22
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
23
23
  import { filter } from 'rxjs/operators';
24
+ import * as i2$2 from 'primeng/ripple';
25
+ import { RippleModule } from 'primeng/ripple';
24
26
  import { MessageService } from 'primeng/api';
25
27
  import * as i4 from 'primeng/select';
26
28
  import { SelectModule } from 'primeng/select';
27
- import * as i2$3 from 'primeng/ripple';
28
- import { RippleModule } from 'primeng/ripple';
29
+ import * as i3 from 'primeng/iconfield';
30
+ import * as i4$1 from 'primeng/inputicon';
31
+ import * as i5 from 'primeng/inputtext';
29
32
  import Material from '@primeuix/themes/material';
30
33
 
31
34
  /** Filter launcher apps based on user permission codes */
@@ -111,7 +114,7 @@ class LayoutPersistenceService {
111
114
  platformId = inject(PLATFORM_ID);
112
115
  isBrowser = isPlatformBrowser(this.platformId);
113
116
  validPresets = ['Aura', 'Lara', 'Nora'];
114
- validMenuModes = ['static', 'overlay'];
117
+ validMenuModes = ['static', 'overlay', 'topbar'];
115
118
  /**
116
119
  * Load configuration from localStorage.
117
120
  * Returns null if no saved config or invalid data.
@@ -223,6 +226,7 @@ class LayoutService {
223
226
  configSidebarVisible: false,
224
227
  staticMenuMobileActive: false,
225
228
  menuHoverActive: false,
229
+ topbarMenuVisible: true,
226
230
  };
227
231
  _layoutConfig = signal({
228
232
  ...this.DEFAULT_CONFIG,
@@ -242,6 +246,7 @@ class LayoutService {
242
246
  companyProfile = this._companyProfile.asReadonly();
243
247
  // Static app info from config
244
248
  appName = this.appConfig?.appName ?? DEFAULT_APP_NAME;
249
+ appLogo = this.appConfig?.appLogo ?? '';
245
250
  authorName = this.appConfig?.author?.name ?? DEFAULT_AUTHOR.name;
246
251
  authorUrl = this.appConfig?.author?.url ?? DEFAULT_AUTHOR.url;
247
252
  // Permission-filtered menu and apps
@@ -255,6 +260,7 @@ class LayoutService {
255
260
  getPrimary = computed(() => this._layoutConfig().primary, ...(ngDevMode ? [{ debugName: "getPrimary" }] : []));
256
261
  getSurface = computed(() => this._layoutConfig().surface, ...(ngDevMode ? [{ debugName: "getSurface" }] : []));
257
262
  isOverlay = computed(() => this._layoutConfig().menuMode === 'overlay', ...(ngDevMode ? [{ debugName: "isOverlay" }] : []));
263
+ isTopbar = computed(() => this._layoutConfig().menuMode === 'topbar', ...(ngDevMode ? [{ debugName: "isTopbar" }] : []));
258
264
  // User profile computed signals
259
265
  userName = computed(() => this._userProfile()?.name ?? this.translateAdapter?.translate('layout.profile.guest') ?? 'Guest', ...(ngDevMode ? [{ debugName: "userName" }] : []));
260
266
  userEmail = computed(() => this._userProfile()?.email ?? '', ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
@@ -266,6 +272,7 @@ class LayoutService {
266
272
  return this.appName;
267
273
  return this._companyProfile()?.name ?? this.appName;
268
274
  }, ...(ngDevMode ? [{ debugName: "companyName" }] : []));
275
+ displayLogo = computed(() => this.companyLogoUrl() ?? this.appLogo, ...(ngDevMode ? [{ debugName: "displayLogo" }] : []));
269
276
  // RxJS Subjects for event communication
270
277
  configUpdate = new Subject();
271
278
  overlayOpen = new Subject();
@@ -310,6 +317,14 @@ class LayoutService {
310
317
  }
311
318
  onMenuToggle() {
312
319
  const state = this._layoutState();
320
+ // Desktop topbar mode: toggle topbar nav visibility
321
+ if (this.isDesktop() && this.isTopbar()) {
322
+ this._layoutState.update((prev) => ({
323
+ ...prev,
324
+ topbarMenuVisible: !prev.topbarMenuVisible,
325
+ }));
326
+ return;
327
+ }
313
328
  // Mobile always uses staticMenuMobileActive regardless of menu mode
314
329
  if (!this.isDesktop()) {
315
330
  const newMobileActive = !state.staticMenuMobileActive;
@@ -382,13 +397,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
382
397
  class AppFooter {
383
398
  layoutService = inject(LayoutService);
384
399
  appName = this.layoutService.appName;
400
+ displayLogo = this.layoutService.appLogo;
385
401
  authorName = this.layoutService.authorName;
386
402
  authorUrl = this.layoutService.authorUrl;
387
403
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppFooter, deps: [], target: i0.ɵɵFactoryTarget.Component });
388
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.5", type: AppFooter, isStandalone: true, selector: "app-footer", ngImport: i0, template: `<div class="layout-footer">
389
- {{ appName }} {{ 'layout.footer.by' | translate }}
390
- <a [href]="authorUrl" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">{{ authorName }}</a>
391
- </div>`, isInline: true, dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }] });
404
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppFooter, isStandalone: true, selector: "app-footer", ngImport: i0, template: `<div class="layout-footer">
405
+ @if (displayLogo) {
406
+ <img [src]="displayLogo" [alt]="appName" class="logo-image" />
407
+ }
408
+ {{ appName }} {{ 'layout.footer.by' | translate }}
409
+ <a
410
+ [href]="authorUrl"
411
+ target="_blank"
412
+ rel="noopener noreferrer"
413
+ class="text-primary font-bold hover:underline"
414
+ >{{ authorName }}</a
415
+ >
416
+ </div>`, isInline: true, dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }] });
392
417
  }
393
418
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppFooter, decorators: [{
394
419
  type: Component,
@@ -396,9 +421,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
396
421
  selector: 'app-footer',
397
422
  imports: [TranslatePipe],
398
423
  template: `<div class="layout-footer">
399
- {{ appName }} {{ 'layout.footer.by' | translate }}
400
- <a [href]="authorUrl" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">{{ authorName }}</a>
401
- </div>`,
424
+ @if (displayLogo) {
425
+ <img [src]="displayLogo" [alt]="appName" class="logo-image" />
426
+ }
427
+ {{ appName }} {{ 'layout.footer.by' | translate }}
428
+ <a
429
+ [href]="authorUrl"
430
+ target="_blank"
431
+ rel="noopener noreferrer"
432
+ class="text-primary font-bold hover:underline"
433
+ >{{ authorName }}</a
434
+ >
435
+ </div>`,
402
436
  }]
403
437
  }] });
404
438
 
@@ -413,10 +447,17 @@ class AppConfigurator {
413
447
  layoutService = inject(LayoutService);
414
448
  presets = Object.keys(presets);
415
449
  showMenuModeButton = signal(!this.router.url.includes('auth'), ...(ngDevMode ? [{ debugName: "showMenuModeButton" }] : []));
416
- menuModeOptions = computed(() => [
417
- { label: this.translate('layout.configurator.menu.mode.static'), value: 'static' },
418
- { label: this.translate('layout.configurator.menu.mode.overlay'), value: 'overlay' },
419
- ], ...(ngDevMode ? [{ debugName: "menuModeOptions" }] : []));
450
+ menuModeOptions = computed(() => {
451
+ const options = [
452
+ { label: this.translate('layout.configurator.menu.mode.static'), value: 'static' },
453
+ { label: this.translate('layout.configurator.menu.mode.overlay'), value: 'overlay' },
454
+ ];
455
+ // Only show topbar mode on desktop
456
+ if (this.layoutService.isDesktop()) {
457
+ options.push({ label: this.translate('layout.configurator.menu.mode.topbar'), value: 'topbar' });
458
+ }
459
+ return options;
460
+ }, ...(ngDevMode ? [{ debugName: "menuModeOptions" }] : []));
420
461
  constructor() {
421
462
  // Use afterNextRender for browser-only initialization (replaces isServer check)
422
463
  afterNextRender(() => {
@@ -904,7 +945,9 @@ const LAYOUT_AUTH_API = new InjectionToken('LAYOUT_AUTH_API');
904
945
  const LAYOUT_NOTIFICATION_BELL = new InjectionToken('LAYOUT_NOTIFICATION_BELL');
905
946
  const LAYOUT_LANGUAGE_SELECTOR = new InjectionToken('LAYOUT_LANGUAGE_SELECTOR');
906
947
  const LAYOUT_LANGUAGE_SELECTOR_PANEL = new InjectionToken('LAYOUT_LANGUAGE_SELECTOR_PANEL');
948
+ const LAYOUT_SEARCHBAR = new InjectionToken('LAYOUT_SEARCHBAR');
907
949
  const LAYOUT_IS_RTL = new InjectionToken('LAYOUT_IS_RTL');
950
+ const LAYOUT_SEARCH_ADAPTER = new InjectionToken('LAYOUT_SEARCH_ADAPTER');
908
951
 
909
952
  class AppFloatingConfigurator {
910
953
  layoutService = inject(LayoutService);
@@ -1008,171 +1051,493 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1008
1051
  `, styles: [".floating-configurator{inset-inline-end:.5rem}@media(min-width:768px){.floating-configurator{inset-inline-end:2rem}}\n"] }]
1009
1052
  }] });
1010
1053
 
1011
- /**
1012
- * View Transitions API type definitions
1013
- * @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
1014
- */
1015
-
1016
- /** Company/branch switcher displayed in top bar */
1017
- class AppCompanyBranchSelector {
1054
+ class AppMenuitem {
1055
+ // Signal inputs
1056
+ item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
1057
+ index = input.required(...(ngDevMode ? [{ debugName: "index" }] : []));
1058
+ parentKey = input('', ...(ngDevMode ? [{ debugName: "parentKey" }] : []));
1059
+ // Injected services
1060
+ router = inject(Router);
1061
+ layoutService = inject(LayoutService);
1018
1062
  destroyRef = inject(DestroyRef);
1019
- authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1020
- authApi = inject(LAYOUT_AUTH_API, { optional: true });
1021
- messageService = inject(MessageService);
1022
- translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1023
- document = inject(DOCUMENT$1);
1024
- elementRef = inject(ElementRef);
1025
- _isActive = signal(false, ...(ngDevMode ? [{ debugName: "_isActive" }] : []));
1026
- isActive = this._isActive.asReadonly();
1063
+ // View references
1064
+ parentLink = viewChild('parentLink', ...(ngDevMode ? [{ debugName: "parentLink" }] : []));
1065
+ // State signals - private writable + public readonly pattern
1066
+ _active = signal(false, ...(ngDevMode ? [{ debugName: "_active" }] : []));
1067
+ active = this._active.asReadonly();
1068
+ // Computed signals
1069
+ key = computed(() => {
1070
+ const parent = this.parentKey();
1071
+ return parent ? `${parent}-${this.index()}` : String(this.index());
1072
+ }, ...(ngDevMode ? [{ debugName: "key" }] : []));
1073
+ routerLink = computed(() => {
1074
+ const menuItem = this.item();
1075
+ return menuItem.routerLink ?? [];
1076
+ }, ...(ngDevMode ? [{ debugName: "routerLink" }] : []));
1077
+ // Computed options for routerLinkActive - use 'exact' for root, 'subset' for others
1078
+ routerLinkActiveOptions = computed(() => {
1079
+ const link = this.routerLink();
1080
+ const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1081
+ return {
1082
+ paths: pathsMatch,
1083
+ queryParams: 'ignored',
1084
+ matrixParams: 'ignored',
1085
+ fragment: 'ignored',
1086
+ };
1087
+ }, ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : []));
1027
1088
  constructor() {
1028
- fromEvent(this.document, 'click')
1029
- .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1030
- .subscribe((event) => {
1031
- const target = event.target;
1032
- if (!this.elementRef.nativeElement.contains(target)) {
1033
- this._isActive.set(false);
1089
+ // Subscribe to menu source changes
1090
+ this.layoutService.menuSource$
1091
+ .pipe(takeUntilDestroyed(this.destroyRef))
1092
+ .subscribe((value) => {
1093
+ Promise.resolve(null).then(() => {
1094
+ const currentKey = this.key();
1095
+ if (value.routeEvent) {
1096
+ this._active.set(value.key === currentKey ||
1097
+ value.key?.startsWith(currentKey + '-'));
1098
+ }
1099
+ else if (value.key !== currentKey &&
1100
+ !value.key?.startsWith(currentKey + '-')) {
1101
+ this._active.set(false);
1102
+ }
1103
+ });
1104
+ });
1105
+ // Subscribe to menu reset
1106
+ this.layoutService.resetSource$
1107
+ .pipe(takeUntilDestroyed(this.destroyRef))
1108
+ .subscribe(() => this._active.set(false));
1109
+ // Subscribe to navigation events
1110
+ this.router.events
1111
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1112
+ .subscribe(() => {
1113
+ if (this.item().routerLink) {
1114
+ this.updateActiveStateFromRoute();
1115
+ }
1116
+ });
1117
+ // Effect to update active state on init when item has routerLink
1118
+ effect(() => {
1119
+ const menuItem = this.item();
1120
+ if (menuItem.routerLink) {
1121
+ this.updateActiveStateFromRoute();
1122
+ }
1123
+ });
1124
+ // Effect to update submenu position when active in topbar mode
1125
+ effect(() => {
1126
+ if (this.active() && this.parentLink()) {
1127
+ Promise.resolve().then(() => this.updateSubmenuPosition());
1034
1128
  }
1035
1129
  });
1036
1130
  }
1037
- currentCompanyName = computed(() => this.authState?.currentCompanyInfo()?.name ?? this.translate('layout.company.branch.selector.no.company'), ...(ngDevMode ? [{ debugName: "currentCompanyName" }] : []));
1038
- currentBranchName = computed(() => this.authState?.currentBranchInfo()?.name ?? null, ...(ngDevMode ? [{ debugName: "currentBranchName" }] : []));
1039
- _companies = signal([], ...(ngDevMode ? [{ debugName: "_companies" }] : []));
1040
- companies = this._companies.asReadonly();
1041
- _branches = signal([], ...(ngDevMode ? [{ debugName: "_branches" }] : []));
1042
- branches = this._branches.asReadonly();
1043
- _selectedCompanyId = signal(null, ...(ngDevMode ? [{ debugName: "_selectedCompanyId" }] : []));
1044
- selectedCompanyId = this._selectedCompanyId.asReadonly();
1045
- _selectedBranchId = signal(null, ...(ngDevMode ? [{ debugName: "_selectedBranchId" }] : []));
1046
- selectedBranchId = this._selectedBranchId.asReadonly();
1047
- _isLoadingCompanies = signal(false, ...(ngDevMode ? [{ debugName: "_isLoadingCompanies" }] : []));
1048
- isLoadingCompanies = this._isLoadingCompanies.asReadonly();
1049
- _isLoadingBranches = signal(false, ...(ngDevMode ? [{ debugName: "_isLoadingBranches" }] : []));
1050
- isLoadingBranches = this._isLoadingBranches.asReadonly();
1051
- _isSwitching = signal(false, ...(ngDevMode ? [{ debugName: "_isSwitching" }] : []));
1052
- isSwitching = this._isSwitching.asReadonly();
1053
- canSwitch = computed(() => {
1054
- const selectedCompany = this.selectedCompanyId();
1055
- if (!selectedCompany)
1056
- return false;
1057
- const currentCompanyId = this.authState?.currentCompanyInfo()?.id;
1058
- const currentBranchId = this.authState?.currentBranchInfo()?.id;
1059
- const selectedBranch = this.selectedBranchId();
1060
- return selectedCompany !== currentCompanyId || selectedBranch !== currentBranchId;
1061
- }, ...(ngDevMode ? [{ debugName: "canSwitch" }] : []));
1062
- onPanelToggle() {
1063
- this._isActive.update((v) => !v);
1064
- if (this.isActive() && this.companies().length === 0) {
1065
- this.loadCompanies();
1131
+ onMouseEnter() {
1132
+ if (this.layoutService.isTopbar() && this.item().children) {
1133
+ this.updateSubmenuPosition();
1066
1134
  }
1067
1135
  }
1068
- loadCompanies() {
1069
- if (!this.authApi)
1136
+ updateSubmenuPosition() {
1137
+ const linkEl = this.parentLink()?.nativeElement;
1138
+ if (!linkEl)
1070
1139
  return;
1071
- this._isLoadingCompanies.set(true);
1072
- this.authApi
1073
- .getUserCompanies()
1074
- .pipe(takeUntilDestroyed(this.destroyRef))
1075
- .subscribe({
1076
- next: (companies) => {
1077
- this._companies.set(companies);
1078
- this._isLoadingCompanies.set(false);
1079
- },
1080
- error: () => {
1081
- // Error toast handled by global interceptor
1082
- this._isLoadingCompanies.set(false);
1083
- },
1084
- });
1085
- }
1086
- onCompanyChange(companyId) {
1087
- this._selectedCompanyId.set(companyId);
1088
- if (!companyId) {
1089
- this._branches.set([]);
1090
- this._selectedBranchId.set(null);
1140
+ const rect = linkEl.getBoundingClientRect();
1141
+ const hostEl = linkEl.closest('[app-menuitem]');
1142
+ if (!hostEl)
1091
1143
  return;
1092
- }
1093
- this.loadBranches(companyId);
1094
- }
1095
- onBranchChange(branchId) {
1096
- this._selectedBranchId.set(branchId);
1144
+ // Position submenu to the left of the parent link and above it
1145
+ const left = rect.left;
1146
+ const top = rect.top + 30; // 8px gap above the menu item
1147
+ hostEl.style.setProperty('--submenu-left', `${left}px`);
1148
+ hostEl.style.setProperty('--submenu-top', `${top}px`);
1097
1149
  }
1098
- loadBranches(companyId) {
1099
- if (!this.authApi)
1150
+ updateActiveStateFromRoute() {
1151
+ const link = this.routerLink();
1152
+ if (!link.length)
1100
1153
  return;
1101
- this._isLoadingBranches.set(true);
1102
- this._selectedBranchId.set(null);
1103
- this.authApi
1104
- .getCompanyBranches(companyId)
1105
- .pipe(takeUntilDestroyed(this.destroyRef))
1106
- .subscribe({
1107
- next: (branches) => {
1108
- this._branches.set(branches);
1109
- this._isLoadingBranches.set(false);
1110
- // Auto-select if only one branch available
1111
- if (branches.length === 1) {
1112
- this._selectedBranchId.set(branches[0].id);
1113
- }
1114
- },
1115
- error: () => {
1116
- // Error toast handled by global interceptor
1117
- this._isLoadingBranches.set(false);
1118
- },
1154
+ // Use 'exact' for root path to avoid matching all routes
1155
+ // Use 'subset' for all other paths to match child routes
1156
+ const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1157
+ const isActive = this.router.isActive(link[0], {
1158
+ paths: pathsMatch,
1159
+ queryParams: 'ignored',
1160
+ matrixParams: 'ignored',
1161
+ fragment: 'ignored',
1119
1162
  });
1120
- }
1121
- onSwitch() {
1122
- const selectedCompany = this.selectedCompanyId();
1123
- const selectedBranch = this.selectedBranchId();
1124
- if (!this.authApi || !selectedCompany)
1125
- return;
1126
- if (this.branches().length > 0 && !selectedBranch) {
1127
- this.messageService.add({
1128
- severity: 'warn',
1129
- summary: this.translate('layout.company.branch.selector.branch.required'),
1130
- detail: this.translate('layout.company.branch.selector.please.select.branch'),
1163
+ if (isActive) {
1164
+ this.layoutService.onMenuStateChange({
1165
+ key: this.key(),
1166
+ routeEvent: true,
1131
1167
  });
1132
- return;
1133
1168
  }
1134
- this._isSwitching.set(true);
1135
- this.authApi
1136
- .switchCompany(selectedCompany, selectedBranch || '')
1137
- .pipe(takeUntilDestroyed(this.destroyRef))
1138
- .subscribe({
1139
- next: () => { },
1140
- error: () => {
1141
- // Error toast handled by global interceptor
1142
- this._isSwitching.set(false);
1143
- },
1144
- });
1145
1169
  }
1146
- translate(key) {
1147
- return this.translateAdapter?.translate(key) ?? key;
1170
+ itemClick() {
1171
+ if (this.item().children && !this.layoutService.isTopbar()) {
1172
+ this._active.update((prev) => !prev);
1173
+ }
1174
+ this.layoutService.onMenuStateChange({ key: this.key() });
1148
1175
  }
1149
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppCompanyBranchSelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
1150
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppCompanyBranchSelector, isStandalone: true, selector: "app-company-branch-selector", host: { classAttribute: "relative" }, ngImport: i0, template: `
1151
- <button
1152
- type="button"
1153
- class="layout-topbar-action"
1154
- [class.layout-topbar-action-highlight]="isActive()"
1155
- pStyleClass="@next"
1156
- enterFromClass="hidden"
1157
- enterActiveClass="animate-scalein"
1158
- leaveToClass="hidden"
1159
- leaveActiveClass="animate-fadeout"
1160
- [hideOnOutsideClick]="true"
1161
- (click)="onPanelToggle()"
1162
- >
1163
- <i class="pi pi-building"></i>
1164
- <span>{{ currentCompanyName() }}</span>
1165
- @if (currentBranchName()) {
1166
- <span class="text-xs opacity-70"> | {{ currentBranchName() }}</span>
1167
- }
1168
- </button>
1169
- <div
1170
- class="hidden absolute top-[3.25rem] z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)] w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[300px] max-w-[360px]"
1171
- style="background-color: var(--surface-overlay); inset-inline-end: 0;"
1172
- >
1173
- <div class="flex flex-col gap-4">
1174
- <span class="text-sm text-muted-color font-semibold"
1175
- >{{ 'layout.company.branch.selector.title' | translate }}</span
1176
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenuitem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1177
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppMenuitem, isStandalone: true, selector: "[app-menuitem]", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, parentKey: { classPropertyName: "parentKey", publicName: "parentKey", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.active-menuitem": "active()" } }, viewQueries: [{ propertyName: "parentLink", first: true, predicate: ["parentLink"], descendants: true, isSignal: true }], ngImport: i0, template: `
1178
+ <ng-container>
1179
+ @if (item().children?.length) {
1180
+ <a
1181
+ (click)="itemClick()"
1182
+ (mouseenter)="onMouseEnter()"
1183
+ #parentLink
1184
+ tabindex="0"
1185
+ pRipple
1186
+ [style.cursor]="layoutService.isTopbar() ? 'default' : 'pointer'"
1187
+ >
1188
+ @if (item().icon) {
1189
+ <lib-icon
1190
+ [icon]="item().icon!"
1191
+ [iconType]="item().iconType"
1192
+ class="layout-menuitem-icon"
1193
+ />
1194
+ }
1195
+ <span class="layout-menuitem-text">{{
1196
+ item().labelKey ? (item().labelKey! | translate) : item().label
1197
+ }}</span>
1198
+ @if (item().children) {
1199
+ @if (layoutService.isTopbar()) {
1200
+ <i class="pi pi-fw pi-chevron-right layout-submenu-toggler"></i>
1201
+ } @else {
1202
+ <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
1203
+ }
1204
+ }
1205
+ </a>
1206
+ }
1207
+ @if (item().routerLink && !item().children?.length) {
1208
+ <a
1209
+ (click)="itemClick()"
1210
+ [routerLink]="routerLink()"
1211
+ routerLinkActive="active-route"
1212
+ [routerLinkActiveOptions]="routerLinkActiveOptions()"
1213
+ tabindex="0"
1214
+ pRipple
1215
+ >
1216
+ @if (item().icon) {
1217
+ <lib-icon
1218
+ [icon]="item().icon!"
1219
+ [iconType]="item().iconType"
1220
+ class="layout-menuitem-icon"
1221
+ />
1222
+ }
1223
+ <span class="layout-menuitem-text">{{
1224
+ item().labelKey ? (item().labelKey! | translate) : item().label
1225
+ }}</span>
1226
+ </a>
1227
+ }
1228
+ @if (item().children) {
1229
+ <ul
1230
+ (mouseenter)="onMouseEnter()"
1231
+ [class.submenu-expanded]="active() && !layoutService.isTopbar()"
1232
+ [class.submenu-collapsed]="!active() && !layoutService.isTopbar()"
1233
+ >
1234
+ @for (
1235
+ child of item().children;
1236
+ track 'menu' + child.id + '' + i;
1237
+ let i = $index
1238
+ ) {
1239
+ <li
1240
+ app-menuitem
1241
+ [item]="child"
1242
+ [index]="i"
1243
+ [parentKey]="key()"
1244
+ ></li>
1245
+ }
1246
+ </ul>
1247
+ }
1248
+ </ng-container>
1249
+ `, isInline: true, styles: [":host ul{overflow:hidden;transition:max-height .4s cubic-bezier(.86,0,.07,1)}:host ul.submenu-collapsed{max-height:0}:host ul.submenu-expanded{max-height:1000px}\n"], dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "component", type: IconComponent, selector: "lib-icon", inputs: ["icon", "iconType"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1$2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "ngmodule", type: RippleModule }, { kind: "directive", type: i2$2.Ripple, selector: "[pRipple]" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1250
+ }
1251
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenuitem, decorators: [{
1252
+ type: Component,
1253
+ args: [{ selector: '[app-menuitem]', imports: [IconComponent, RouterModule, RippleModule, TranslatePipe], template: `
1254
+ <ng-container>
1255
+ @if (item().children?.length) {
1256
+ <a
1257
+ (click)="itemClick()"
1258
+ (mouseenter)="onMouseEnter()"
1259
+ #parentLink
1260
+ tabindex="0"
1261
+ pRipple
1262
+ [style.cursor]="layoutService.isTopbar() ? 'default' : 'pointer'"
1263
+ >
1264
+ @if (item().icon) {
1265
+ <lib-icon
1266
+ [icon]="item().icon!"
1267
+ [iconType]="item().iconType"
1268
+ class="layout-menuitem-icon"
1269
+ />
1270
+ }
1271
+ <span class="layout-menuitem-text">{{
1272
+ item().labelKey ? (item().labelKey! | translate) : item().label
1273
+ }}</span>
1274
+ @if (item().children) {
1275
+ @if (layoutService.isTopbar()) {
1276
+ <i class="pi pi-fw pi-chevron-right layout-submenu-toggler"></i>
1277
+ } @else {
1278
+ <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
1279
+ }
1280
+ }
1281
+ </a>
1282
+ }
1283
+ @if (item().routerLink && !item().children?.length) {
1284
+ <a
1285
+ (click)="itemClick()"
1286
+ [routerLink]="routerLink()"
1287
+ routerLinkActive="active-route"
1288
+ [routerLinkActiveOptions]="routerLinkActiveOptions()"
1289
+ tabindex="0"
1290
+ pRipple
1291
+ >
1292
+ @if (item().icon) {
1293
+ <lib-icon
1294
+ [icon]="item().icon!"
1295
+ [iconType]="item().iconType"
1296
+ class="layout-menuitem-icon"
1297
+ />
1298
+ }
1299
+ <span class="layout-menuitem-text">{{
1300
+ item().labelKey ? (item().labelKey! | translate) : item().label
1301
+ }}</span>
1302
+ </a>
1303
+ }
1304
+ @if (item().children) {
1305
+ <ul
1306
+ (mouseenter)="onMouseEnter()"
1307
+ [class.submenu-expanded]="active() && !layoutService.isTopbar()"
1308
+ [class.submenu-collapsed]="!active() && !layoutService.isTopbar()"
1309
+ >
1310
+ @for (
1311
+ child of item().children;
1312
+ track 'menu' + child.id + '' + i;
1313
+ let i = $index
1314
+ ) {
1315
+ <li
1316
+ app-menuitem
1317
+ [item]="child"
1318
+ [index]="i"
1319
+ [parentKey]="key()"
1320
+ ></li>
1321
+ }
1322
+ </ul>
1323
+ }
1324
+ </ng-container>
1325
+ `, host: {
1326
+ '[class.active-menuitem]': 'active()',
1327
+ }, 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"] }]
1328
+ }], 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 }] }] } });
1329
+
1330
+ /**
1331
+ * Main menu component that displays filtered menu items.
1332
+ * Menu is automatically filtered based on user permissions using the permission checker (logicNode pattern).
1333
+ * The filtering happens internally in LayoutService via computed signal.
1334
+ */
1335
+ class AppMenu {
1336
+ layoutService = inject(LayoutService);
1337
+ /**
1338
+ * Filtered menu items from layout service.
1339
+ * This signal is computed in LayoutService and automatically updates when:
1340
+ * - Raw menu changes (setMenu called)
1341
+ * - Permission state changes (user permissions updated)
1342
+ */
1343
+ menuItems = this.layoutService.menu;
1344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1345
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppMenu, isStandalone: true, selector: "app-menu", ngImport: i0, template: `<div class="layout-menu">
1346
+ <ul>
1347
+ @for (
1348
+ item of menuItems();
1349
+ track 'menu' + item.id + '' + i;
1350
+ let i = $index
1351
+ ) {
1352
+ <li app-menuitem [item]="item" [index]="i"></li>
1353
+ }
1354
+ </ul>
1355
+ </div>`, isInline: true, dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "ngmodule", type: RouterModule }] });
1356
+ }
1357
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenu, decorators: [{
1358
+ type: Component,
1359
+ args: [{
1360
+ selector: 'app-menu',
1361
+ imports: [AppMenuitem, RouterModule],
1362
+ template: `<div class="layout-menu">
1363
+ <ul>
1364
+ @for (
1365
+ item of menuItems();
1366
+ track 'menu' + item.id + '' + i;
1367
+ let i = $index
1368
+ ) {
1369
+ <li app-menuitem [item]="item" [index]="i"></li>
1370
+ }
1371
+ </ul>
1372
+ </div>`,
1373
+ }]
1374
+ }] });
1375
+
1376
+ /**
1377
+ * View Transitions API type definitions
1378
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
1379
+ */
1380
+
1381
+ /** Company/branch switcher displayed in top bar */
1382
+ class AppCompanyBranchSelector {
1383
+ destroyRef = inject(DestroyRef);
1384
+ authState = inject(LAYOUT_AUTH_STATE, { optional: true });
1385
+ authApi = inject(LAYOUT_AUTH_API, { optional: true });
1386
+ messageService = inject(MessageService);
1387
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1388
+ document = inject(DOCUMENT$1);
1389
+ elementRef = inject(ElementRef);
1390
+ _isActive = signal(false, ...(ngDevMode ? [{ debugName: "_isActive" }] : []));
1391
+ isActive = this._isActive.asReadonly();
1392
+ constructor() {
1393
+ fromEvent(this.document, 'click')
1394
+ .pipe(takeUntilDestroyed(this.destroyRef), filter(() => this.isActive()))
1395
+ .subscribe((event) => {
1396
+ const target = event.target;
1397
+ if (!this.elementRef.nativeElement.contains(target)) {
1398
+ this._isActive.set(false);
1399
+ }
1400
+ });
1401
+ }
1402
+ currentCompanyName = computed(() => this.authState?.currentCompanyInfo()?.name ?? this.translate('layout.company.branch.selector.no.company'), ...(ngDevMode ? [{ debugName: "currentCompanyName" }] : []));
1403
+ currentBranchName = computed(() => this.authState?.currentBranchInfo()?.name ?? null, ...(ngDevMode ? [{ debugName: "currentBranchName" }] : []));
1404
+ _companies = signal([], ...(ngDevMode ? [{ debugName: "_companies" }] : []));
1405
+ companies = this._companies.asReadonly();
1406
+ _branches = signal([], ...(ngDevMode ? [{ debugName: "_branches" }] : []));
1407
+ branches = this._branches.asReadonly();
1408
+ _selectedCompanyId = signal(null, ...(ngDevMode ? [{ debugName: "_selectedCompanyId" }] : []));
1409
+ selectedCompanyId = this._selectedCompanyId.asReadonly();
1410
+ _selectedBranchId = signal(null, ...(ngDevMode ? [{ debugName: "_selectedBranchId" }] : []));
1411
+ selectedBranchId = this._selectedBranchId.asReadonly();
1412
+ _isLoadingCompanies = signal(false, ...(ngDevMode ? [{ debugName: "_isLoadingCompanies" }] : []));
1413
+ isLoadingCompanies = this._isLoadingCompanies.asReadonly();
1414
+ _isLoadingBranches = signal(false, ...(ngDevMode ? [{ debugName: "_isLoadingBranches" }] : []));
1415
+ isLoadingBranches = this._isLoadingBranches.asReadonly();
1416
+ _isSwitching = signal(false, ...(ngDevMode ? [{ debugName: "_isSwitching" }] : []));
1417
+ isSwitching = this._isSwitching.asReadonly();
1418
+ canSwitch = computed(() => {
1419
+ const selectedCompany = this.selectedCompanyId();
1420
+ if (!selectedCompany)
1421
+ return false;
1422
+ const currentCompanyId = this.authState?.currentCompanyInfo()?.id;
1423
+ const currentBranchId = this.authState?.currentBranchInfo()?.id;
1424
+ const selectedBranch = this.selectedBranchId();
1425
+ return selectedCompany !== currentCompanyId || selectedBranch !== currentBranchId;
1426
+ }, ...(ngDevMode ? [{ debugName: "canSwitch" }] : []));
1427
+ onPanelToggle() {
1428
+ this._isActive.update((v) => !v);
1429
+ if (this.isActive() && this.companies().length === 0) {
1430
+ this.loadCompanies();
1431
+ }
1432
+ }
1433
+ loadCompanies() {
1434
+ if (!this.authApi)
1435
+ return;
1436
+ this._isLoadingCompanies.set(true);
1437
+ this.authApi
1438
+ .getUserCompanies()
1439
+ .pipe(takeUntilDestroyed(this.destroyRef))
1440
+ .subscribe({
1441
+ next: (companies) => {
1442
+ this._companies.set(companies);
1443
+ this._isLoadingCompanies.set(false);
1444
+ },
1445
+ error: () => {
1446
+ // Error toast handled by global interceptor
1447
+ this._isLoadingCompanies.set(false);
1448
+ },
1449
+ });
1450
+ }
1451
+ onCompanyChange(companyId) {
1452
+ this._selectedCompanyId.set(companyId);
1453
+ if (!companyId) {
1454
+ this._branches.set([]);
1455
+ this._selectedBranchId.set(null);
1456
+ return;
1457
+ }
1458
+ this.loadBranches(companyId);
1459
+ }
1460
+ onBranchChange(branchId) {
1461
+ this._selectedBranchId.set(branchId);
1462
+ }
1463
+ loadBranches(companyId) {
1464
+ if (!this.authApi)
1465
+ return;
1466
+ this._isLoadingBranches.set(true);
1467
+ this._selectedBranchId.set(null);
1468
+ this.authApi
1469
+ .getCompanyBranches(companyId)
1470
+ .pipe(takeUntilDestroyed(this.destroyRef))
1471
+ .subscribe({
1472
+ next: (branches) => {
1473
+ this._branches.set(branches);
1474
+ this._isLoadingBranches.set(false);
1475
+ // Auto-select if only one branch available
1476
+ if (branches.length === 1) {
1477
+ this._selectedBranchId.set(branches[0].id);
1478
+ }
1479
+ },
1480
+ error: () => {
1481
+ // Error toast handled by global interceptor
1482
+ this._isLoadingBranches.set(false);
1483
+ },
1484
+ });
1485
+ }
1486
+ onSwitch() {
1487
+ const selectedCompany = this.selectedCompanyId();
1488
+ const selectedBranch = this.selectedBranchId();
1489
+ if (!this.authApi || !selectedCompany)
1490
+ return;
1491
+ if (this.branches().length > 0 && !selectedBranch) {
1492
+ this.messageService.add({
1493
+ severity: 'warn',
1494
+ summary: this.translate('layout.company.branch.selector.branch.required'),
1495
+ detail: this.translate('layout.company.branch.selector.please.select.branch'),
1496
+ });
1497
+ return;
1498
+ }
1499
+ this._isSwitching.set(true);
1500
+ this.authApi
1501
+ .switchCompany(selectedCompany, selectedBranch || '')
1502
+ .pipe(takeUntilDestroyed(this.destroyRef))
1503
+ .subscribe({
1504
+ next: () => { },
1505
+ error: () => {
1506
+ // Error toast handled by global interceptor
1507
+ this._isSwitching.set(false);
1508
+ },
1509
+ });
1510
+ }
1511
+ translate(key) {
1512
+ return this.translateAdapter?.translate(key) ?? key;
1513
+ }
1514
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppCompanyBranchSelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
1515
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppCompanyBranchSelector, isStandalone: true, selector: "app-company-branch-selector", host: { classAttribute: "relative" }, ngImport: i0, template: `
1516
+ <button
1517
+ type="button"
1518
+ class="layout-topbar-action"
1519
+ [class.layout-topbar-action-highlight]="isActive()"
1520
+ pStyleClass="@next"
1521
+ enterFromClass="hidden"
1522
+ enterActiveClass="animate-scalein"
1523
+ leaveToClass="hidden"
1524
+ leaveActiveClass="animate-fadeout"
1525
+ [hideOnOutsideClick]="true"
1526
+ (click)="onPanelToggle()"
1527
+ >
1528
+ <i class="pi pi-building"></i>
1529
+ <span>{{ currentCompanyName() }}</span>
1530
+ @if (currentBranchName()) {
1531
+ <span class="text-xs opacity-70"> | {{ currentBranchName() }}</span>
1532
+ }
1533
+ </button>
1534
+ <div
1535
+ class="hidden absolute top-[3.25rem] z-50 p-4 border border-surface rounded-border origin-top shadow-[0px_3px_5px_rgba(0,0,0,0.02),0px_0px_2px_rgba(0,0,0,0.05),0px_1px_4px_rgba(0,0,0,0.08)] w-[calc(100vw-2rem)] sm:w-auto sm:min-w-[300px] max-w-[360px]"
1536
+ style="background-color: var(--surface-overlay); inset-inline-end: 0;"
1537
+ >
1538
+ <div class="flex flex-col gap-4">
1539
+ <span class="text-sm text-muted-color font-semibold"
1540
+ >{{ 'layout.company.branch.selector.title' | translate }}</span
1176
1541
  >
1177
1542
 
1178
1543
  <!-- Company Selector -->
@@ -1441,6 +1806,7 @@ class AppProfile {
1441
1806
  layoutService = inject(LayoutService);
1442
1807
  messageService = inject(MessageService);
1443
1808
  translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1809
+ appConfig = inject(APP_CONFIG, { optional: true });
1444
1810
  document = inject(DOCUMENT$1);
1445
1811
  elementRef = inject(ElementRef);
1446
1812
  _isActive = signal(false, ...(ngDevMode ? [{ debugName: "_isActive" }] : []));
@@ -1461,6 +1827,7 @@ class AppProfile {
1461
1827
  userName = computed(() => this.authState?.loginUserData()?.name ?? this.translate('layout.profile.guest'), ...(ngDevMode ? [{ debugName: "userName" }] : []));
1462
1828
  userEmail = computed(() => this.authState?.loginUserData()?.email ?? '', ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
1463
1829
  profilePicture = computed(() => this.layoutService.userProfilePictureUrl() ?? '', ...(ngDevMode ? [{ debugName: "profilePicture" }] : []));
1830
+ signUpEnabled = computed(() => this.appConfig ? isSignUpEnabled(this.appConfig) : false, ...(ngDevMode ? [{ debugName: "signUpEnabled" }] : []));
1464
1831
  logout() {
1465
1832
  if (!this.authApi)
1466
1833
  return;
@@ -1555,13 +1922,15 @@ class AppProfile {
1555
1922
 
1556
1923
  <!-- Menu Items -->
1557
1924
  <div class="flex flex-col gap-1">
1558
- <a
1559
- class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1560
- (click)="copySignUpLink()"
1561
- >
1562
- <i class="pi pi-link text-muted-color"></i>
1563
- <span>{{ 'layout.profile.copy.sign.up.link' | translate }}</span>
1564
- </a>
1925
+ @if (signUpEnabled()) {
1926
+ <a
1927
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1928
+ (click)="copySignUpLink()"
1929
+ >
1930
+ <i class="pi pi-link text-muted-color"></i>
1931
+ <span>{{ 'layout.profile.copy.sign.up.link' | translate }}</span>
1932
+ </a>
1933
+ }
1565
1934
  <a
1566
1935
  class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1567
1936
  (click)="logout()"
@@ -1572,7 +1941,7 @@ class AppProfile {
1572
1941
  </div>
1573
1942
  </div>
1574
1943
  </div>
1575
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i2$2.IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1944
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i2$3.IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1576
1945
  }
1577
1946
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppProfile, decorators: [{
1578
1947
  type: Component,
@@ -1630,13 +1999,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1630
1999
 
1631
2000
  <!-- Menu Items -->
1632
2001
  <div class="flex flex-col gap-1">
1633
- <a
1634
- class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1635
- (click)="copySignUpLink()"
1636
- >
1637
- <i class="pi pi-link text-muted-color"></i>
1638
- <span>{{ 'layout.profile.copy.sign.up.link' | translate }}</span>
1639
- </a>
2002
+ @if (signUpEnabled()) {
2003
+ <a
2004
+ class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
2005
+ (click)="copySignUpLink()"
2006
+ >
2007
+ <i class="pi pi-link text-muted-color"></i>
2008
+ <span>{{ 'layout.profile.copy.sign.up.link' | translate }}</span>
2009
+ </a>
2010
+ }
1640
2011
  <a
1641
2012
  class="flex items-center gap-3 p-3 rounded-border cursor-pointer no-underline text-color hover:bg-emphasis transition-all duration-200"
1642
2013
  (click)="logout()"
@@ -1651,6 +2022,356 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1651
2022
  }]
1652
2023
  }], ctorParameters: () => [] });
1653
2024
 
2025
+ class AppSearchbar {
2026
+ layoutService = inject(LayoutService);
2027
+ router = inject(Router);
2028
+ searchAdapter = inject(LAYOUT_SEARCH_ADAPTER, {
2029
+ optional: true,
2030
+ });
2031
+ destroyRef = inject(DestroyRef);
2032
+ document = inject(DOCUMENT$1);
2033
+ searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
2034
+ suggestionsOpen = signal(false, ...(ngDevMode ? [{ debugName: "suggestionsOpen" }] : []));
2035
+ suggestions = signal([], ...(ngDevMode ? [{ debugName: "suggestions" }] : []));
2036
+ focusedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : []));
2037
+ isSearchExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isSearchExpanded" }] : []));
2038
+ isResponsive = signal(typeof window !== 'undefined' ? window.innerWidth < 768 : false, ...(ngDevMode ? [{ debugName: "isResponsive" }] : []));
2039
+ searchWrapper = viewChild('searchWrapper', ...(ngDevMode ? [{ debugName: "searchWrapper" }] : []));
2040
+ searchInput = viewChild('searchInput', ...(ngDevMode ? [{ debugName: "searchInput" }] : []));
2041
+ searchTimeout;
2042
+ constructor() {
2043
+ // Track responsive breakpoint (mobile < 768px)
2044
+ if (typeof window !== 'undefined') {
2045
+ fromEvent(window, 'resize')
2046
+ .pipe(takeUntilDestroyed(this.destroyRef))
2047
+ .subscribe(() => {
2048
+ this.isResponsive.set(window.innerWidth < 768);
2049
+ });
2050
+ }
2051
+ // Close dropdown/search on outside click
2052
+ fromEvent(this.document, 'click')
2053
+ .pipe(takeUntilDestroyed(this.destroyRef))
2054
+ .subscribe((event) => {
2055
+ const wrapper = this.searchWrapper()?.nativeElement;
2056
+ const target = event.target;
2057
+ if (wrapper && !wrapper.contains(target)) {
2058
+ if (this.suggestionsOpen()) {
2059
+ this.closeSuggestions();
2060
+ }
2061
+ if (this.isSearchExpanded() &&
2062
+ (this.layoutService.isTopbar() || this.isResponsive())) {
2063
+ this.closeSearch();
2064
+ }
2065
+ }
2066
+ });
2067
+ // Debounced search for suggestions
2068
+ effect(() => {
2069
+ const query = this.searchTerm();
2070
+ clearTimeout(this.searchTimeout);
2071
+ if (!this.searchAdapter || !query.trim()) {
2072
+ this.suggestions.set([]);
2073
+ this.focusedIndex.set(-1);
2074
+ return;
2075
+ }
2076
+ this.searchTimeout = setTimeout(() => {
2077
+ this.searchAdapter.getSuggestions(query)
2078
+ .pipe(takeUntilDestroyed(this.destroyRef))
2079
+ .subscribe((results) => {
2080
+ this.suggestions.set(results);
2081
+ this.focusedIndex.set(-1);
2082
+ this.suggestionsOpen.set(results.length > 0);
2083
+ });
2084
+ }, 300);
2085
+ });
2086
+ }
2087
+ onFocus() {
2088
+ if (this.suggestions().length > 0) {
2089
+ this.suggestionsOpen.set(true);
2090
+ }
2091
+ }
2092
+ handleKeydown(event) {
2093
+ const items = this.suggestions();
2094
+ const current = this.focusedIndex();
2095
+ switch (event.key) {
2096
+ case 'ArrowDown':
2097
+ event.preventDefault();
2098
+ this.focusedIndex.set(current < items.length - 1 ? current + 1 : 0);
2099
+ this.suggestionsOpen.set(items.length > 0);
2100
+ break;
2101
+ case 'ArrowUp':
2102
+ event.preventDefault();
2103
+ this.focusedIndex.set(current > 0 ? current - 1 : items.length - 1);
2104
+ break;
2105
+ case 'Escape':
2106
+ event.preventDefault();
2107
+ this.closeSuggestions();
2108
+ break;
2109
+ }
2110
+ }
2111
+ handleEnter() {
2112
+ const current = this.focusedIndex();
2113
+ const items = this.suggestions();
2114
+ if (current >= 0 && current < items.length) {
2115
+ this.selectSuggestion(items[current]);
2116
+ }
2117
+ else if (items.length > 0) {
2118
+ this.selectSuggestion(items[0]);
2119
+ }
2120
+ else {
2121
+ this.onSearch();
2122
+ }
2123
+ }
2124
+ selectSuggestion(item) {
2125
+ if (item.routerLink) {
2126
+ this.router.navigate(item.routerLink);
2127
+ }
2128
+ this.searchTerm.set('');
2129
+ this.closeSuggestions();
2130
+ if (this.layoutService.isTopbar() || this.isResponsive()) {
2131
+ this.closeSearch();
2132
+ }
2133
+ }
2134
+ closeSuggestions() {
2135
+ this.suggestionsOpen.set(false);
2136
+ this.focusedIndex.set(-1);
2137
+ }
2138
+ toggleSearch() {
2139
+ this.isSearchExpanded.set(!this.isSearchExpanded());
2140
+ if (this.isSearchExpanded()) {
2141
+ setTimeout(() => {
2142
+ this.searchInput()?.nativeElement?.focus();
2143
+ }, 100);
2144
+ }
2145
+ }
2146
+ closeSearch() {
2147
+ this.isSearchExpanded.set(false);
2148
+ this.closeSuggestions();
2149
+ }
2150
+ onSearch() {
2151
+ // Implement global search if needed
2152
+ }
2153
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppSearchbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2154
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppSearchbar, isStandalone: true, selector: "app-searchbar", viewQueries: [{ propertyName: "searchWrapper", first: true, predicate: ["searchWrapper"], descendants: true, isSignal: true }, { propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
2155
+ <div
2156
+ class="search-container h-full"
2157
+ #searchWrapper
2158
+ [class.topbar-mode]="layoutService.isTopbar() || isResponsive()"
2159
+ >
2160
+ @if (layoutService.isTopbar() || isResponsive()) {
2161
+ <!-- Topbar/Mobile Icon Mode -->
2162
+ <button
2163
+ type="button"
2164
+ pButton
2165
+ pRipple
2166
+ class="search-toggle-btn"
2167
+ icon="pi pi-search"
2168
+ (click)="toggleSearch()"
2169
+ [attr.aria-label]="'layout.topbar.search' | translate"
2170
+ severity="secondary"
2171
+ text
2172
+ ></button>
2173
+
2174
+ @if (isSearchExpanded()) {
2175
+ <div class="search-dropdown-panel">
2176
+ <p-iconfield class="search-iconfield">
2177
+ <p-inputicon class="pi pi-search search-icon" />
2178
+ <input
2179
+ type="text"
2180
+ pInputText
2181
+ class="search-input w-full"
2182
+ [placeholder]="'layout.topbar.search.placeholder' | translate"
2183
+ [(ngModel)]="searchTerm"
2184
+ (keyup.enter)="handleEnter()"
2185
+ (keydown)="handleKeydown($event)"
2186
+ (focus)="onFocus()"
2187
+ [attr.aria-label]="'layout.topbar.search' | translate"
2188
+ #searchInput
2189
+ autocomplete="off"
2190
+ />
2191
+ </p-iconfield>
2192
+
2193
+ @if (suggestionsOpen() && suggestions().length > 0) {
2194
+ <div class="search-suggestions">
2195
+ <ul class="suggestions-list">
2196
+ @for (
2197
+ item of suggestions();
2198
+ track item.label;
2199
+ let idx = $index
2200
+ ) {
2201
+ <li
2202
+ class="suggestion-item"
2203
+ [class.focused]="idx === focusedIndex()"
2204
+ (click)="selectSuggestion(item)"
2205
+ (mouseenter)="focusedIndex.set(idx)"
2206
+ role="option"
2207
+ [attr.aria-selected]="idx === focusedIndex()"
2208
+ >
2209
+ @if (item.icon) {
2210
+ <i [class]="item.icon" class="suggestion-icon"></i>
2211
+ }
2212
+ <span class="suggestion-label">{{ item.label }}</span>
2213
+ </li>
2214
+ }
2215
+ </ul>
2216
+ </div>
2217
+ }
2218
+ </div>
2219
+ }
2220
+ } @else {
2221
+ <!-- Desktop Full Input Mode -->
2222
+ <p-iconfield class="search-iconfield">
2223
+ <p-inputicon class="pi pi-search search-icon" (click)="onSearch()" />
2224
+ <input
2225
+ type="text"
2226
+ pInputText
2227
+ class="search-input w-full"
2228
+ [placeholder]="'layout.topbar.search.placeholder' | translate"
2229
+ [(ngModel)]="searchTerm"
2230
+ (keyup.enter)="handleEnter()"
2231
+ (keydown)="handleKeydown($event)"
2232
+ (focus)="onFocus()"
2233
+ [attr.aria-label]="'layout.topbar.search' | translate"
2234
+ #searchInput
2235
+ />
2236
+ </p-iconfield>
2237
+
2238
+ @if (suggestionsOpen() && suggestions().length > 0) {
2239
+ <div class="search-dropdown">
2240
+ <ul class="suggestions-list">
2241
+ @for (item of suggestions(); track item.label; let idx = $index) {
2242
+ <li
2243
+ class="suggestion-item"
2244
+ [class.focused]="idx === focusedIndex()"
2245
+ (click)="selectSuggestion(item)"
2246
+ (mouseenter)="focusedIndex.set(idx)"
2247
+ role="option"
2248
+ [attr.aria-selected]="idx === focusedIndex()"
2249
+ >
2250
+ @if (item.icon) {
2251
+ <i [class]="item.icon" class="suggestion-icon"></i>
2252
+ }
2253
+ <span class="suggestion-label">{{ item.label }}</span>
2254
+ </li>
2255
+ }
2256
+ </ul>
2257
+ </div>
2258
+ }
2259
+ }
2260
+ </div>
2261
+ `, 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(--border-radius);box-shadow:0 4px 20px #00000026;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(--border-radius);box-shadow:0 4px 16px #0000001f;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(--border-radius);margin-top:0}.suggestion-item{padding:.75rem}}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i3.IconField, selector: "p-iconfield, p-iconField, p-icon-field", inputs: ["hostName", "iconPosition", "styleClass"] }, { kind: "component", type: i4$1.InputIcon, selector: "p-inputicon, p-inputIcon", inputs: ["hostName", "styleClass"] }, { kind: "directive", type: i5.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "directive", type: i2$2.Ripple, selector: "[pRipple]" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2262
+ }
2263
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppSearchbar, decorators: [{
2264
+ type: Component,
2265
+ args: [{ selector: 'app-searchbar', imports: [AngularModule, PrimeModule, TranslatePipe], template: `
2266
+ <div
2267
+ class="search-container h-full"
2268
+ #searchWrapper
2269
+ [class.topbar-mode]="layoutService.isTopbar() || isResponsive()"
2270
+ >
2271
+ @if (layoutService.isTopbar() || isResponsive()) {
2272
+ <!-- Topbar/Mobile Icon Mode -->
2273
+ <button
2274
+ type="button"
2275
+ pButton
2276
+ pRipple
2277
+ class="search-toggle-btn"
2278
+ icon="pi pi-search"
2279
+ (click)="toggleSearch()"
2280
+ [attr.aria-label]="'layout.topbar.search' | translate"
2281
+ severity="secondary"
2282
+ text
2283
+ ></button>
2284
+
2285
+ @if (isSearchExpanded()) {
2286
+ <div class="search-dropdown-panel">
2287
+ <p-iconfield class="search-iconfield">
2288
+ <p-inputicon class="pi pi-search search-icon" />
2289
+ <input
2290
+ type="text"
2291
+ pInputText
2292
+ class="search-input w-full"
2293
+ [placeholder]="'layout.topbar.search.placeholder' | translate"
2294
+ [(ngModel)]="searchTerm"
2295
+ (keyup.enter)="handleEnter()"
2296
+ (keydown)="handleKeydown($event)"
2297
+ (focus)="onFocus()"
2298
+ [attr.aria-label]="'layout.topbar.search' | translate"
2299
+ #searchInput
2300
+ autocomplete="off"
2301
+ />
2302
+ </p-iconfield>
2303
+
2304
+ @if (suggestionsOpen() && suggestions().length > 0) {
2305
+ <div class="search-suggestions">
2306
+ <ul class="suggestions-list">
2307
+ @for (
2308
+ item of suggestions();
2309
+ track item.label;
2310
+ let idx = $index
2311
+ ) {
2312
+ <li
2313
+ class="suggestion-item"
2314
+ [class.focused]="idx === focusedIndex()"
2315
+ (click)="selectSuggestion(item)"
2316
+ (mouseenter)="focusedIndex.set(idx)"
2317
+ role="option"
2318
+ [attr.aria-selected]="idx === focusedIndex()"
2319
+ >
2320
+ @if (item.icon) {
2321
+ <i [class]="item.icon" class="suggestion-icon"></i>
2322
+ }
2323
+ <span class="suggestion-label">{{ item.label }}</span>
2324
+ </li>
2325
+ }
2326
+ </ul>
2327
+ </div>
2328
+ }
2329
+ </div>
2330
+ }
2331
+ } @else {
2332
+ <!-- Desktop Full Input Mode -->
2333
+ <p-iconfield class="search-iconfield">
2334
+ <p-inputicon class="pi pi-search search-icon" (click)="onSearch()" />
2335
+ <input
2336
+ type="text"
2337
+ pInputText
2338
+ class="search-input w-full"
2339
+ [placeholder]="'layout.topbar.search.placeholder' | translate"
2340
+ [(ngModel)]="searchTerm"
2341
+ (keyup.enter)="handleEnter()"
2342
+ (keydown)="handleKeydown($event)"
2343
+ (focus)="onFocus()"
2344
+ [attr.aria-label]="'layout.topbar.search' | translate"
2345
+ #searchInput
2346
+ />
2347
+ </p-iconfield>
2348
+
2349
+ @if (suggestionsOpen() && suggestions().length > 0) {
2350
+ <div class="search-dropdown">
2351
+ <ul class="suggestions-list">
2352
+ @for (item of suggestions(); track item.label; let idx = $index) {
2353
+ <li
2354
+ class="suggestion-item"
2355
+ [class.focused]="idx === focusedIndex()"
2356
+ (click)="selectSuggestion(item)"
2357
+ (mouseenter)="focusedIndex.set(idx)"
2358
+ role="option"
2359
+ [attr.aria-selected]="idx === focusedIndex()"
2360
+ >
2361
+ @if (item.icon) {
2362
+ <i [class]="item.icon" class="suggestion-icon"></i>
2363
+ }
2364
+ <span class="suggestion-label">{{ item.label }}</span>
2365
+ </li>
2366
+ }
2367
+ </ul>
2368
+ </div>
2369
+ }
2370
+ }
2371
+ </div>
2372
+ `, 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(--border-radius);box-shadow:0 4px 20px #00000026;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(--border-radius);box-shadow:0 4px 16px #0000001f;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(--border-radius);margin-top:0}.suggestion-item{padding:.75rem}}\n"] }]
2373
+ }], ctorParameters: () => [], propDecorators: { searchWrapper: [{ type: i0.ViewChild, args: ['searchWrapper', { isSignal: true }] }], searchInput: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
2374
+
1654
2375
  class AppTopbar {
1655
2376
  appConfig = inject(APP_CONFIG);
1656
2377
  document = inject(DOCUMENT$1);
@@ -1662,7 +2383,12 @@ class AppTopbar {
1662
2383
  languageSelectorComponent = inject(LAYOUT_LANGUAGE_SELECTOR, {
1663
2384
  optional: true,
1664
2385
  });
2386
+ searchAdapter = inject(LAYOUT_SEARCH_ADAPTER, {
2387
+ optional: true,
2388
+ });
2389
+ hasSearchAdapter = computed(() => !!this.searchAdapter, ...(ngDevMode ? [{ debugName: "hasSearchAdapter" }] : []));
1665
2390
  configContainer = viewChild('configContainer', ...(ngDevMode ? [{ debugName: "configContainer" }] : []));
2391
+ navContainer = viewChild('navContainer', ...(ngDevMode ? [{ debugName: "navContainer" }] : []));
1666
2392
  activePanel = signal(null, ...(ngDevMode ? [{ debugName: "activePanel" }] : []));
1667
2393
  constructor() {
1668
2394
  fromEvent(this.document, 'click')
@@ -1670,8 +2396,17 @@ class AppTopbar {
1670
2396
  .subscribe((event) => {
1671
2397
  this.handleOutsideClick(event);
1672
2398
  });
2399
+ effect(() => {
2400
+ const nav = this.navContainer();
2401
+ if (nav && this.layoutService.isTopbar()) {
2402
+ fromEvent(nav.nativeElement, 'scroll')
2403
+ .pipe(takeUntilDestroyed(this.destroyRef))
2404
+ .subscribe(() => this.recalculateHoveredSubmenu(nav.nativeElement));
2405
+ }
2406
+ });
1673
2407
  }
1674
2408
  companyName = this.layoutService.companyName;
2409
+ displayLogo = this.layoutService.displayLogo;
1675
2410
  enableCompanyFeature = isCompanyFeatureEnabled(this.appConfig);
1676
2411
  toggleDarkMode() {
1677
2412
  const currentDarkTheme = this.layoutService.layoutConfig().darkTheme;
@@ -1687,22 +2422,67 @@ class AppTopbar {
1687
2422
  this.activePanel.set(null);
1688
2423
  }
1689
2424
  }
2425
+ recalculateHoveredSubmenu(navEl) {
2426
+ const hoveredItems = navEl.querySelectorAll('li:hover');
2427
+ hoveredItems.forEach((li) => {
2428
+ const link = li.querySelector(':scope > a');
2429
+ if (!link)
2430
+ return;
2431
+ const rect = link.getBoundingClientRect();
2432
+ const top = rect.bottom + 12;
2433
+ const left = rect.left - 12;
2434
+ li.style.setProperty('--submenu-left', `${left}px`);
2435
+ li.style.setProperty('--submenu-top', `${top}px`);
2436
+ });
2437
+ }
1690
2438
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1691
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppTopbar, isStandalone: true, selector: "app-topbar", viewQueries: [{ propertyName: "configContainer", first: true, predicate: ["configContainer"], descendants: true, isSignal: true }], ngImport: i0, template: ` <div class="layout-topbar">
1692
- <div class="layout-topbar-logo-container">
1693
- <button
1694
- class="layout-menu-button layout-topbar-action"
1695
- [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
1696
- (click)="layoutService.onMenuToggle()"
1697
- >
1698
- <i class="pi pi-bars"></i>
1699
- </button>
2439
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppTopbar, isStandalone: true, selector: "app-topbar", viewQueries: [{ propertyName: "configContainer", first: true, predicate: ["configContainer"], descendants: true, isSignal: true }, { propertyName: "navContainer", first: true, predicate: ["navContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `<div class="layout-topbar">
2440
+ <div
2441
+ class="layout-topbar-logo-container"
2442
+ [class.layout-topbar-logo-container-desktop]="layoutService.isDesktop()"
2443
+ >
2444
+ @if (!layoutService.isTopbar() && layoutService.isMobile()) {
2445
+ <button
2446
+ class="layout-menu-button layout-topbar-action"
2447
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2448
+ (click)="layoutService.onMenuToggle()"
2449
+ >
2450
+ <i class="pi pi-bars"></i>
2451
+ </button>
2452
+ }
1700
2453
  <a class="layout-topbar-logo" routerLink="/">
2454
+ @if (displayLogo()) {
2455
+ <img [src]="displayLogo()" [alt]="companyName()" class="logo-image" />
2456
+ }
1701
2457
  <span>{{ companyName() }}</span>
1702
2458
  </a>
2459
+ @if (!layoutService.isTopbar() && layoutService.isDesktop()) {
2460
+ <button
2461
+ class="layout-menu-button layout-topbar-action layout-topbar-action-right"
2462
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2463
+ (click)="layoutService.onMenuToggle()"
2464
+ >
2465
+ <i class="pi pi-bars"></i>
2466
+ </button>
2467
+ }
1703
2468
  </div>
1704
2469
 
2470
+ @if (layoutService.isTopbar() && layoutService.isDesktop()) {
2471
+ <div
2472
+ #navContainer
2473
+ class="layout-topbar-nav-horizontal"
2474
+ [class.layout-topbar-nav-hidden]="
2475
+ !layoutService.layoutState().topbarMenuVisible
2476
+ "
2477
+ >
2478
+ <app-menu />
2479
+ </div>
2480
+ }
2481
+
1705
2482
  <div class="layout-topbar-actions">
2483
+ @if (hasSearchAdapter()) {
2484
+ <app-searchbar />
2485
+ }
1706
2486
  <div class="layout-config-menu">
1707
2487
  <button
1708
2488
  type="button"
@@ -1766,7 +2546,7 @@ class AppTopbar {
1766
2546
  </div>
1767
2547
  </div>
1768
2548
  </div>
1769
- </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "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: "pipe", type: TranslatePipe, name: "translate" }] });
2549
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: StyleClassModule }, { kind: "directive", type: i2$1.StyleClass, selector: "[pStyleClass]", inputs: ["pStyleClass", "enterFromClass", "enterActiveClass", "enterToClass", "leaveFromClass", "leaveActiveClass", "leaveToClass", "hideOnOutsideClick", "toggleClass", "hideOnEscape", "hideOnResize", "resizeSelector"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { 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" }] });
1770
2550
  }
1771
2551
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppTopbar, decorators: [{
1772
2552
  type: Component,
@@ -1778,25 +2558,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1778
2558
  NgComponentOutlet,
1779
2559
  TranslatePipe,
1780
2560
  AppConfigurator,
2561
+ AppMenu,
1781
2562
  AppProfile,
1782
2563
  AppCompanyBranchSelector,
1783
2564
  AppLauncher,
2565
+ AppSearchbar,
1784
2566
  ],
1785
- template: ` <div class="layout-topbar">
1786
- <div class="layout-topbar-logo-container">
1787
- <button
1788
- class="layout-menu-button layout-topbar-action"
1789
- [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
1790
- (click)="layoutService.onMenuToggle()"
1791
- >
1792
- <i class="pi pi-bars"></i>
1793
- </button>
2567
+ template: `<div class="layout-topbar">
2568
+ <div
2569
+ class="layout-topbar-logo-container"
2570
+ [class.layout-topbar-logo-container-desktop]="layoutService.isDesktop()"
2571
+ >
2572
+ @if (!layoutService.isTopbar() && layoutService.isMobile()) {
2573
+ <button
2574
+ class="layout-menu-button layout-topbar-action"
2575
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2576
+ (click)="layoutService.onMenuToggle()"
2577
+ >
2578
+ <i class="pi pi-bars"></i>
2579
+ </button>
2580
+ }
1794
2581
  <a class="layout-topbar-logo" routerLink="/">
2582
+ @if (displayLogo()) {
2583
+ <img [src]="displayLogo()" [alt]="companyName()" class="logo-image" />
2584
+ }
1795
2585
  <span>{{ companyName() }}</span>
1796
2586
  </a>
2587
+ @if (!layoutService.isTopbar() && layoutService.isDesktop()) {
2588
+ <button
2589
+ class="layout-menu-button layout-topbar-action layout-topbar-action-right"
2590
+ [attr.aria-label]="'layout.topbar.toggle.menu' | translate"
2591
+ (click)="layoutService.onMenuToggle()"
2592
+ >
2593
+ <i class="pi pi-bars"></i>
2594
+ </button>
2595
+ }
1797
2596
  </div>
1798
2597
 
2598
+ @if (layoutService.isTopbar() && layoutService.isDesktop()) {
2599
+ <div
2600
+ #navContainer
2601
+ class="layout-topbar-nav-horizontal"
2602
+ [class.layout-topbar-nav-hidden]="
2603
+ !layoutService.layoutState().topbarMenuVisible
2604
+ "
2605
+ >
2606
+ <app-menu />
2607
+ </div>
2608
+ }
2609
+
1799
2610
  <div class="layout-topbar-actions">
2611
+ @if (hasSearchAdapter()) {
2612
+ <app-searchbar />
2613
+ }
1800
2614
  <div class="layout-config-menu">
1801
2615
  <button
1802
2616
  type="button"
@@ -1862,270 +2676,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1862
2676
  </div>
1863
2677
  </div>`,
1864
2678
  }]
1865
- }], ctorParameters: () => [], propDecorators: { configContainer: [{ type: i0.ViewChild, args: ['configContainer', { isSignal: true }] }] } });
1866
-
1867
- class AppMenuitem {
1868
- // Signal inputs
1869
- item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
1870
- index = input.required(...(ngDevMode ? [{ debugName: "index" }] : []));
1871
- parentKey = input('', ...(ngDevMode ? [{ debugName: "parentKey" }] : []));
1872
- // Injected services
1873
- router = inject(Router);
1874
- layoutService = inject(LayoutService);
1875
- destroyRef = inject(DestroyRef);
1876
- // State signals - private writable + public readonly pattern
1877
- _active = signal(false, ...(ngDevMode ? [{ debugName: "_active" }] : []));
1878
- active = this._active.asReadonly();
1879
- // Computed signals
1880
- key = computed(() => {
1881
- const parent = this.parentKey();
1882
- return parent ? `${parent}-${this.index()}` : String(this.index());
1883
- }, ...(ngDevMode ? [{ debugName: "key" }] : []));
1884
- routerLink = computed(() => {
1885
- const menuItem = this.item();
1886
- return menuItem.routerLink ?? [];
1887
- }, ...(ngDevMode ? [{ debugName: "routerLink" }] : []));
1888
- // Computed options for routerLinkActive - use 'exact' for root, 'subset' for others
1889
- routerLinkActiveOptions = computed(() => {
1890
- const link = this.routerLink();
1891
- const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1892
- return {
1893
- paths: pathsMatch,
1894
- queryParams: 'ignored',
1895
- matrixParams: 'ignored',
1896
- fragment: 'ignored',
1897
- };
1898
- }, ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : []));
1899
- constructor() {
1900
- // Subscribe to menu source changes
1901
- this.layoutService.menuSource$
1902
- .pipe(takeUntilDestroyed(this.destroyRef))
1903
- .subscribe((value) => {
1904
- Promise.resolve(null).then(() => {
1905
- const currentKey = this.key();
1906
- if (value.routeEvent) {
1907
- this._active.set(value.key === currentKey ||
1908
- value.key?.startsWith(currentKey + '-'));
1909
- }
1910
- else if (value.key !== currentKey &&
1911
- !value.key?.startsWith(currentKey + '-')) {
1912
- this._active.set(false);
1913
- }
1914
- });
1915
- });
1916
- // Subscribe to menu reset
1917
- this.layoutService.resetSource$
1918
- .pipe(takeUntilDestroyed(this.destroyRef))
1919
- .subscribe(() => this._active.set(false));
1920
- // Subscribe to navigation events
1921
- this.router.events
1922
- .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
1923
- .subscribe(() => {
1924
- if (this.item().routerLink) {
1925
- this.updateActiveStateFromRoute();
1926
- }
1927
- });
1928
- // Effect to update active state on init when item has routerLink
1929
- effect(() => {
1930
- const menuItem = this.item();
1931
- if (menuItem.routerLink) {
1932
- this.updateActiveStateFromRoute();
1933
- }
1934
- });
1935
- }
1936
- updateActiveStateFromRoute() {
1937
- const link = this.routerLink();
1938
- if (!link.length)
1939
- return;
1940
- // Use 'exact' for root path to avoid matching all routes
1941
- // Use 'subset' for all other paths to match child routes
1942
- const pathsMatch = link[0] === '/' ? 'exact' : 'subset';
1943
- const isActive = this.router.isActive(link[0], {
1944
- paths: pathsMatch,
1945
- queryParams: 'ignored',
1946
- matrixParams: 'ignored',
1947
- fragment: 'ignored',
1948
- });
1949
- if (isActive) {
1950
- this.layoutService.onMenuStateChange({
1951
- key: this.key(),
1952
- routeEvent: true,
1953
- });
1954
- }
1955
- }
1956
- itemClick() {
1957
- if (this.item().children) {
1958
- this._active.update((prev) => !prev);
1959
- }
1960
- this.layoutService.onMenuStateChange({ key: this.key() });
1961
- }
1962
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenuitem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1963
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppMenuitem, isStandalone: true, selector: "[app-menuitem]", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, parentKey: { classPropertyName: "parentKey", publicName: "parentKey", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.active-menuitem": "active()" } }, ngImport: i0, template: `
1964
- <ng-container>
1965
- @if (item().children?.length) {
1966
- <a (click)="itemClick()" tabindex="0" pRipple>
1967
- @if (item().icon) {
1968
- <lib-icon
1969
- [icon]="item().icon!"
1970
- [iconType]="item().iconType"
1971
- class="layout-menuitem-icon"
1972
- />
1973
- }
1974
- <span class="layout-menuitem-text">{{ item().labelKey ? (item().labelKey! | translate) : item().label }}</span>
1975
- @if (item().children) {
1976
- <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
1977
- }
1978
- </a>
1979
- }
1980
- @if (item().routerLink && !item().children?.length) {
1981
- <a
1982
- (click)="itemClick()"
1983
- [routerLink]="routerLink()"
1984
- routerLinkActive="active-route"
1985
- [routerLinkActiveOptions]="routerLinkActiveOptions()"
1986
- tabindex="0"
1987
- pRipple
1988
- >
1989
- @if (item().icon) {
1990
- <lib-icon
1991
- [icon]="item().icon!"
1992
- [iconType]="item().iconType"
1993
- class="layout-menuitem-icon"
1994
- />
1995
- }
1996
- <span class="layout-menuitem-text">{{ item().labelKey ? (item().labelKey! | translate) : item().label }}</span>
1997
- </a>
1998
- }
1999
- @if (item().children) {
2000
- <ul
2001
- [class.submenu-expanded]="active()"
2002
- [class.submenu-collapsed]="!active()"
2003
- >
2004
- @for (
2005
- child of item().children;
2006
- track 'menu' + child.id + '' + i;
2007
- let i = $index
2008
- ) {
2009
- <li
2010
- app-menuitem
2011
- [item]="child"
2012
- [index]="i"
2013
- [parentKey]="key()"
2014
- ></li>
2015
- }
2016
- </ul>
2017
- }
2018
- </ng-container>
2019
- `, isInline: true, styles: [":host ul{overflow:hidden;transition:max-height .4s cubic-bezier(.86,0,.07,1)}:host ul.submenu-collapsed{max-height:0}:host ul.submenu-expanded{max-height:1000px}\n"], dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "component", type: IconComponent, selector: "lib-icon", inputs: ["icon", "iconType"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1$2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "ngmodule", type: RippleModule }, { kind: "directive", type: i2$3.Ripple, selector: "[pRipple]" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2020
- }
2021
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenuitem, decorators: [{
2022
- type: Component,
2023
- args: [{ selector: '[app-menuitem]', imports: [IconComponent, RouterModule, RippleModule, TranslatePipe], template: `
2024
- <ng-container>
2025
- @if (item().children?.length) {
2026
- <a (click)="itemClick()" tabindex="0" pRipple>
2027
- @if (item().icon) {
2028
- <lib-icon
2029
- [icon]="item().icon!"
2030
- [iconType]="item().iconType"
2031
- class="layout-menuitem-icon"
2032
- />
2033
- }
2034
- <span class="layout-menuitem-text">{{ item().labelKey ? (item().labelKey! | translate) : item().label }}</span>
2035
- @if (item().children) {
2036
- <i class="pi pi-fw pi-angle-down layout-submenu-toggler"></i>
2037
- }
2038
- </a>
2039
- }
2040
- @if (item().routerLink && !item().children?.length) {
2041
- <a
2042
- (click)="itemClick()"
2043
- [routerLink]="routerLink()"
2044
- routerLinkActive="active-route"
2045
- [routerLinkActiveOptions]="routerLinkActiveOptions()"
2046
- tabindex="0"
2047
- pRipple
2048
- >
2049
- @if (item().icon) {
2050
- <lib-icon
2051
- [icon]="item().icon!"
2052
- [iconType]="item().iconType"
2053
- class="layout-menuitem-icon"
2054
- />
2055
- }
2056
- <span class="layout-menuitem-text">{{ item().labelKey ? (item().labelKey! | translate) : item().label }}</span>
2057
- </a>
2058
- }
2059
- @if (item().children) {
2060
- <ul
2061
- [class.submenu-expanded]="active()"
2062
- [class.submenu-collapsed]="!active()"
2063
- >
2064
- @for (
2065
- child of item().children;
2066
- track 'menu' + child.id + '' + i;
2067
- let i = $index
2068
- ) {
2069
- <li
2070
- app-menuitem
2071
- [item]="child"
2072
- [index]="i"
2073
- [parentKey]="key()"
2074
- ></li>
2075
- }
2076
- </ul>
2077
- }
2078
- </ng-container>
2079
- `, host: {
2080
- '[class.active-menuitem]': 'active()',
2081
- }, 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"] }]
2082
- }], 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 }] }] } });
2083
-
2084
- /**
2085
- * Main menu component that displays filtered menu items.
2086
- * Menu is automatically filtered based on user permissions using the permission checker (logicNode pattern).
2087
- * The filtering happens internally in LayoutService via computed signal.
2088
- */
2089
- class AppMenu {
2090
- layoutService = inject(LayoutService);
2091
- /**
2092
- * Filtered menu items from layout service.
2093
- * This signal is computed in LayoutService and automatically updates when:
2094
- * - Raw menu changes (setMenu called)
2095
- * - Permission state changes (user permissions updated)
2096
- */
2097
- menuItems = this.layoutService.menu;
2098
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
2099
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: AppMenu, isStandalone: true, selector: "app-menu", ngImport: i0, template: `<div class="layout-menu">
2100
- <ul>
2101
- @for (
2102
- item of menuItems();
2103
- track 'menu' + item.id + '' + i;
2104
- let i = $index
2105
- ) {
2106
- <li app-menuitem [item]="item" [index]="i"></li>
2107
- }
2108
- </ul>
2109
- </div>`, isInline: true, dependencies: [{ kind: "component", type: AppMenuitem, selector: "[app-menuitem]", inputs: ["item", "index", "parentKey"] }, { kind: "ngmodule", type: RouterModule }] });
2110
- }
2111
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppMenu, decorators: [{
2112
- type: Component,
2113
- args: [{
2114
- selector: 'app-menu',
2115
- imports: [AppMenuitem, RouterModule],
2116
- template: `<div class="layout-menu">
2117
- <ul>
2118
- @for (
2119
- item of menuItems();
2120
- track 'menu' + item.id + '' + i;
2121
- let i = $index
2122
- ) {
2123
- <li app-menuitem [item]="item" [index]="i"></li>
2124
- }
2125
- </ul>
2126
- </div>`,
2127
- }]
2128
- }] });
2679
+ }], ctorParameters: () => [], propDecorators: { configContainer: [{ type: i0.ViewChild, args: ['configContainer', { isSignal: true }] }], navContainer: [{ type: i0.ViewChild, args: ['navContainer', { isSignal: true }] }] } });
2129
2680
 
2130
2681
  class AppSidebar {
2131
2682
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -2177,11 +2728,14 @@ class AppLayout {
2177
2728
  isOutsideClicked(event) {
2178
2729
  const sidebarEl = this.document.querySelector('.layout-sidebar');
2179
2730
  const topbarEl = this.document.querySelector('.layout-menu-button');
2731
+ const topbarNavEl = this.document.querySelector('.layout-topbar-nav');
2180
2732
  const eventTarget = event.target;
2181
2733
  return !(sidebarEl?.isSameNode(eventTarget) ||
2182
2734
  sidebarEl?.contains(eventTarget) ||
2183
2735
  topbarEl?.isSameNode(eventTarget) ||
2184
- topbarEl?.contains(eventTarget));
2736
+ topbarEl?.contains(eventTarget) ||
2737
+ topbarNavEl?.isSameNode(eventTarget) ||
2738
+ topbarNavEl?.contains(eventTarget));
2185
2739
  }
2186
2740
  hideMenu() {
2187
2741
  this.layoutService.updateLayoutState({
@@ -2207,9 +2761,11 @@ class AppLayout {
2207
2761
  return {
2208
2762
  'layout-overlay': config.menuMode === 'overlay',
2209
2763
  'layout-static': config.menuMode === 'static',
2764
+ 'layout-topbar-mode': config.menuMode === 'topbar',
2210
2765
  'layout-static-inactive': state.staticMenuDesktopInactive && config.menuMode === 'static',
2211
2766
  'layout-overlay-active': state.overlayMenuActive,
2212
2767
  'layout-mobile-active': state.staticMenuMobileActive,
2768
+ 'layout-topbar-nav-hidden': !state.topbarMenuVisible && config.menuMode === 'topbar',
2213
2769
  };
2214
2770
  }, ...(ngDevMode ? [{ debugName: "containerClass" }] : []));
2215
2771
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AppLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -2301,6 +2857,10 @@ const LAYOUT_MESSAGES = {
2301
2857
  'menu.event.manager': 'Event Manager',
2302
2858
  'menu.notifications': 'Notifications',
2303
2859
  'menu.localization': 'Localization',
2860
+ 'menu.projects': 'Projects',
2861
+ 'menu.projects.active': 'Active Projects',
2862
+ 'menu.projects.archived': 'Archived Projects',
2863
+ 'menu.projects.reports': 'Project Reports',
2304
2864
  // Profile
2305
2865
  'layout.profile.title': 'Profile',
2306
2866
  'layout.profile.profile.picture.alt': 'Profile picture',
@@ -2330,6 +2890,7 @@ const LAYOUT_MESSAGES = {
2330
2890
  'layout.configurator.menu.mode': 'Menu Mode',
2331
2891
  'layout.configurator.menu.mode.static': 'Static',
2332
2892
  'layout.configurator.menu.mode.overlay': 'Overlay',
2893
+ 'layout.configurator.menu.mode.topbar': 'Topbar',
2333
2894
  // Topbar accessibility
2334
2895
  'layout.topbar.toggle.menu': 'Toggle menu',
2335
2896
  'layout.topbar.toggle.dark.mode': 'Toggle dark mode',
@@ -2348,6 +2909,8 @@ const LAYOUT_MESSAGES = {
2348
2909
  'notification.empty': 'No notifications',
2349
2910
  // Home/Dashboard
2350
2911
  'home.where.users.from': 'Where users are from',
2912
+ 'layout.topbar.search': 'Search',
2913
+ 'layout.topbar.search.placeholder': 'Search for content...',
2351
2914
  };
2352
2915
 
2353
2916
  // Components
@@ -2356,5 +2919,5 @@ const LAYOUT_MESSAGES = {
2356
2919
  * Generated bundle index. Do not edit.
2357
2920
  */
2358
2921
 
2359
- export { AppCompanyBranchSelector, AppConfigurator, AppFloatingConfigurator, AppFooter, AppLauncher, AppLayout, AppMenu, AppMenuitem, AppProfile, AppSidebar, AppTopbar, GreenTheme, LAYOUT_AUTH_API, LAYOUT_AUTH_STATE, LAYOUT_IS_RTL, LAYOUT_LANGUAGE_SELECTOR, LAYOUT_LANGUAGE_SELECTOR_PANEL, LAYOUT_MESSAGES, LAYOUT_NOTIFICATION_BELL, LayoutPersistenceService, LayoutService, NavyBlueTheme, filterAppsByPermissions, filterMenuByPermissions };
2922
+ 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, filterAppsByPermissions, filterMenuByPermissions };
2360
2923
  //# sourceMappingURL=flusys-ng-layout.mjs.map