import { computed, effect, Injectable, signal, TemplateRef } from '@angular/core'; import { MenuItem } from 'primeng/api'; import { BehaviorSubject, Subject } from 'rxjs'; export interface layoutConfig { preset?: string; primary?: string; surface?: string | undefined | null; darkTheme?: boolean; menuMode?: string; } interface IPanelInfo { title: string; } interface LayoutState { fullPageLoading?: boolean; staticMenuDesktopInactive?: boolean; overlayMenuActive?: boolean; configSidebarVisible?: boolean; staticMenuMobileActive?: boolean; menuHoverActive?: boolean; isFixedContentSize?: boolean; isFullPage?: boolean; profilePageRoute?: string; } interface MenuChangeEvent { key: string; routeEvent?: boolean; } @Injectable({ providedIn: 'root', }) export class LayoutService { _config: layoutConfig = { preset: 'Aura', primary: 'surface', surface: null, darkTheme: localStorage.getItem('isDarkTheme') === 'true', menuMode: 'static', }; _state: LayoutState = { fullPageLoading: false, staticMenuDesktopInactive: false, overlayMenuActive: false, configSidebarVisible: false, staticMenuMobileActive: false, menuHoverActive: false, isFixedContentSize: true, isFullPage: false, profilePageRoute: '', }; layoutConfig = signal(this._config); layoutState = signal(this._state); private configUpdate = new Subject(); private overlayOpen = new Subject(); private menuSource = new Subject(); private resetSource = new Subject(); public menuItems = signal([]); public panelInfo = signal({ title: '', }); menuSource$ = this.menuSource.asObservable(); resetSource$ = this.resetSource.asObservable(); configUpdate$ = this.configUpdate.asObservable(); overlayOpen$ = this.overlayOpen.asObservable(); theme = computed(() => (this.layoutConfig()?.darkTheme ? 'light' : 'dark')); isSidebarActive = computed( () => this.layoutState().overlayMenuActive || this.layoutState().staticMenuMobileActive, ); isDarkTheme = computed(() => this.layoutConfig().darkTheme); getPrimary = computed(() => this.layoutConfig().primary); getSurface = computed(() => this.layoutConfig().surface); isOverlay = computed(() => this.layoutConfig().menuMode === 'overlay'); isFixedContentSize = computed(() => this.layoutState().isFixedContentSize); isFullPage = computed(() => this.layoutState().isFullPage); profilePageRoute = computed(() => this.layoutState().profilePageRoute || ''); transitionComplete = signal(false); private initialized = false; constructor() { this.handleDarkModeTransition(this._config); effect(() => { const config = this.layoutConfig(); if (config) { this.onConfigUpdate(); } }); effect(() => { const config = this.layoutConfig(); if (!this.initialized || !config) { this.initialized = true; return; } this.handleDarkModeTransition(config); }); } private handleDarkModeTransition(config: layoutConfig): void { if ((document as any).startViewTransition) { this.startViewTransition(config); } else { this.toggleDarkMode(config); this.onTransitionEnd(); } } private startViewTransition(config: layoutConfig): void { const transition = (document as any).startViewTransition(() => { this.toggleDarkMode(config); }); transition.ready .then(() => { this.onTransitionEnd(); }) .catch(() => {}); } changeIsFixedContentSize(status: boolean) { this.layoutState.update((prev) => ({ ...prev, isFixedContentSize: status, })); } changeIsFullPage(status: boolean) { this.layoutState.update((prev) => ({ ...prev, isFullPage: status, })); } changeFullPageLoading(status: boolean) { this.layoutState.update((prev) => ({ ...prev, fullPageLoading: status, })); } toggleDarkMode(config?: layoutConfig): void { const _config = config || this.layoutConfig(); localStorage.setItem('isDarkTheme', JSON.stringify(_config.darkTheme)); if (_config.darkTheme) { document.documentElement.classList.add('app-dark'); } else { document.documentElement.classList.remove('app-dark'); } } private onTransitionEnd() { this.transitionComplete.set(true); setTimeout(() => { this.transitionComplete.set(false); }); } onMenuToggle() { if (this.isOverlay()) { this.layoutState.update((prev) => ({ ...prev, overlayMenuActive: !this.layoutState().overlayMenuActive, })); if (this.layoutState().overlayMenuActive) { this.overlayOpen.next(null); } } if (this.isDesktop()) { this.layoutState.update((prev) => ({ ...prev, staticMenuDesktopInactive: !this.layoutState().staticMenuDesktopInactive, })); } else { this.layoutState.update((prev) => ({ ...prev, staticMenuMobileActive: !this.layoutState().staticMenuMobileActive, })); if (this.layoutState().staticMenuMobileActive) { this.overlayOpen.next(null); } } } isDesktop() { return window.innerWidth > 991; } isMobile() { return !this.isDesktop(); } onConfigUpdate() { this._config = { ...this.layoutConfig() }; this.configUpdate.next(this.layoutConfig()); } onMenuStateChange(event: MenuChangeEvent) { this.menuSource.next(event); } reset() { this.resetSource.next(true); } setMenuItems(items: MenuItem[]) { this.menuItems.set(items); } setPanelInfo(info: IPanelInfo) { this.panelInfo.set(info); } private headerSlot = new BehaviorSubject | null>(null); headerSlot$ = this.headerSlot.asObservable(); private topbarStartSlot = new BehaviorSubject | null>(null); topbarStartSlot$ = this.topbarStartSlot.asObservable(); private topbarCenterSlot = new BehaviorSubject | null>(null); topbarCenterSlot$ = this.topbarCenterSlot.asObservable(); private topbarEndSlot = new BehaviorSubject | null>(null); topbarEndSlot$ = this.topbarEndSlot.asObservable(); setHeaderSlot(tpl: TemplateRef | null) { // Backward-compatible alias for topbar end slot. this.setTopbarEndSlot(tpl); this.headerSlot.next(tpl); } setTopbarStartSlot(tpl: TemplateRef | null) { this.topbarStartSlot.next(tpl); } setTopbarCenterSlot(tpl: TemplateRef | null) { this.topbarCenterSlot.next(tpl); } setTopbarEndSlot(tpl: TemplateRef | null) { this.topbarEndSlot.next(tpl); } setProfilePageRoute(route: string) { this.layoutState.update((prev) => ({ ...prev, profilePageRoute: route, })); } }