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