import { DeesElement, type TemplateResult, property, customElement, html, css, cssManager, state, } from '@design.estate/dees-element'; import * as interfaces from '../../interfaces/index.js'; import * as plugins from '../../00plugins.js'; import type { DeesAppuiBar } from '../dees-appui-appbar/index.js'; import type { DeesAppuiMainmenu } from '../dees-appui-mainmenu/dees-appui-mainmenu.js'; import type { DeesAppuiSecondarymenu } from '../dees-appui-secondarymenu/dees-appui-secondarymenu.js'; import type { DeesAppuiMaincontent } from '../dees-appui-maincontent/dees-appui-maincontent.js'; import type { DeesAppuiActivitylog } from '../dees-appui-activitylog/dees-appui-activitylog.js'; import { demoFunc } from './dees-appui-base.demo.js'; // New module imports import { ViewRegistry } from './view.registry.js'; import { AppRouter } from './app.router.js'; import { StateManager } from './state.manager.js'; // Import child components import '../dees-appui-appbar/index.js'; import '../dees-appui-mainmenu/dees-appui-mainmenu.js'; import '../dees-appui-secondarymenu/dees-appui-secondarymenu.js'; import '../dees-appui-maincontent/dees-appui-maincontent.js'; import '../dees-appui-activitylog/dees-appui-activitylog.js'; @customElement('dees-appui-base') export class DeesAppuiBase extends DeesElement { public static demo = demoFunc; // Properties for appbar @property({ type: Array }) accessor appbarMenuItems: interfaces.IAppBarMenuItem[] = []; @property({ type: String }) accessor appbarBreadcrumbs: string = ''; @property({ type: String }) accessor appbarBreadcrumbSeparator: string = ' > '; @property({ type: Boolean }) accessor appbarShowWindowControls: boolean = true; @property({ type: Object }) accessor appbarUser: { name: string; email?: string; avatar?: string; status?: 'online' | 'offline' | 'busy' | 'away'; } | undefined = undefined; @property({ type: Array }) accessor appbarProfileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = []; @property({ type: Boolean }) accessor appbarShowSearch: boolean = false; // Properties for mainmenu @property({ type: String }) accessor mainmenuLogoIcon: string = ''; @property({ type: String }) accessor mainmenuLogoText: string = ''; @property({ type: Array }) accessor mainmenuGroups: interfaces.IMenuGroup[] = []; @property({ type: Array }) accessor mainmenuBottomTabs: interfaces.ITab[] = []; @property({ type: Array }) accessor mainmenuTabs: interfaces.ITab[] = []; @property({ type: Object }) accessor mainmenuSelectedTab: interfaces.ITab | undefined = undefined; // Properties for secondarymenu @property({ type: String }) accessor secondarymenuHeading: string = 'Menu'; @property({ type: Array }) accessor secondarymenuGroups: interfaces.ISecondaryMenuGroup[] = []; @property({ type: Object }) accessor secondarymenuSelectedItem: interfaces.ISecondaryMenuItem | undefined = undefined; /** Legacy support for flat options (backward compatibility) */ @property({ type: Array }) accessor secondarymenuOptions: (interfaces.ISelectionOption | { divider: true })[] = []; // Collapse states @property({ type: Boolean }) accessor mainmenuCollapsed: boolean = false; @property({ type: Boolean }) accessor secondarymenuCollapsed: boolean = false; // Properties for maincontent @property({ type: Array }) accessor maincontentTabs: interfaces.ITab[] = []; // References to child components @state() accessor appbar: DeesAppuiBar | undefined = undefined; @state() accessor mainmenu: DeesAppuiMainmenu | undefined = undefined; @state() accessor secondarymenu: DeesAppuiSecondarymenu | undefined = undefined; @state() accessor maincontent: DeesAppuiMaincontent | undefined = undefined; @state() accessor activitylog: DeesAppuiActivitylog | undefined = undefined; // NEW: Unified config property @property({ type: Object }) accessor config: interfaces.IAppConfig | undefined = undefined; // NEW: Current view state @state() accessor currentView: interfaces.IViewDefinition | undefined = undefined; // NEW: Internal services (not reactive, managed internally) private viewRegistry: ViewRegistry = new ViewRegistry(); private router: AppRouter | null = null; private stateManager: StateManager | null = null; public static styles = [ cssManager.defaultStyles, css` :host { position: absolute; height: 100%; width: 100%; background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')}; } .maingrid { position: absolute; top: 40px; height: calc(100% - 40px); width: 100%; display: grid; grid-template-columns: auto auto 1fr 240px; grid-template-rows: 1fr; } /* Z-index layering for proper stacking (position: relative required for z-index to work) */ .maingrid > dees-appui-mainmenu { position: relative; z-index: 3; } .maingrid > dees-appui-secondarymenu { position: relative; z-index: 2; } .maingrid > dees-appui-maincontent { position: relative; z-index: 1; } .maingrid > dees-appui-activitylog { position: relative; z-index: 1; } /* View container for dynamically loaded views */ .view-container { display: contents; } .view-container:empty { display: none; } `, ]; // INSTANCE public render(): TemplateResult { return html` this.handleAppbarMenuSelect(e)} @breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)} @search-click=${() => this.handleAppbarSearchClick()} @user-menu-open=${() => this.handleAppbarUserMenuOpen()} @profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)} >
this.handleMainmenuTabSelect(e)} @collapse-change=${(e: CustomEvent) => this.handleMainmenuCollapseChange(e)} > this.handleSecondarymenuItemSelect(e)} @collapse-change=${(e: CustomEvent) => this.handleSecondarymenuCollapseChange(e)} >
`; } async firstUpdated() { // Get references to child components this.appbar = this.shadowRoot.querySelector('dees-appui-appbar'); this.mainmenu = this.shadowRoot.querySelector('dees-appui-mainmenu'); this.secondarymenu = this.shadowRoot.querySelector('dees-appui-secondarymenu'); this.maincontent = this.shadowRoot.querySelector('dees-appui-maincontent'); this.activitylog = this.shadowRoot.querySelector('dees-appui-activitylog'); // Initialize from config if provided if (this.config) { this.applyConfig(this.config); // Restore state if enabled if (this.config.statePersistence?.enabled) { this.loadState(); } // Initialize router after state restore this.router?.init(); } } async disconnectedCallback() { await super.disconnectedCallback(); this.router?.destroy(); } // Event handlers for appbar private handleAppbarMenuSelect(e: CustomEvent) { this.dispatchEvent(new CustomEvent('appbar-menu-select', { detail: e.detail, bubbles: true, composed: true })); } private handleAppbarBreadcrumbNavigate(e: CustomEvent) { this.dispatchEvent(new CustomEvent('appbar-breadcrumb-navigate', { detail: e.detail, bubbles: true, composed: true })); } private handleAppbarSearchClick() { this.dispatchEvent(new CustomEvent('appbar-search-click', { bubbles: true, composed: true })); } private handleAppbarUserMenuOpen() { this.dispatchEvent(new CustomEvent('appbar-user-menu-open', { bubbles: true, composed: true })); } private handleAppbarProfileMenuSelect(e: CustomEvent) { this.dispatchEvent(new CustomEvent('appbar-profile-menu-select', { detail: e.detail, bubbles: true, composed: true })); } // Event handlers for mainmenu private handleMainmenuTabSelect(e: CustomEvent) { this.mainmenuSelectedTab = e.detail.tab; // Update secondary menu heading based on main menu selection this.secondarymenuHeading = e.detail.tab.key; this.dispatchEvent(new CustomEvent('mainmenu-tab-select', { detail: e.detail, bubbles: true, composed: true })); } // Event handlers for secondarymenu private handleSecondarymenuItemSelect(e: CustomEvent) { this.secondarymenuSelectedItem = e.detail.item; this.dispatchEvent(new CustomEvent('secondarymenu-item-select', { detail: e.detail, bubbles: true, composed: true })); } // Event handlers for collapse state changes private handleMainmenuCollapseChange(e: CustomEvent) { this.mainmenuCollapsed = e.detail.collapsed; this.dispatchEvent(new CustomEvent('mainmenu-collapse-change', { detail: e.detail, bubbles: true, composed: true })); } private handleSecondarymenuCollapseChange(e: CustomEvent) { this.secondarymenuCollapsed = e.detail.collapsed; this.dispatchEvent(new CustomEvent('secondarymenu-collapse-change', { detail: e.detail, bubbles: true, composed: true })); } // ========================================== // NEW: Public methods for unified config API // ========================================== /** * Configure the app shell with a unified config object */ public configure(config: interfaces.IAppConfig): void { this.config = config; this.applyConfig(config); } /** * Navigate to a view by ID */ public navigateToView(viewId: string): boolean { if (this.router) { return this.router.navigate(viewId); } // Fallback for non-routed mode const view = this.viewRegistry.get(viewId); if (view) { this.loadView(view); return true; } return false; } /** * Get the current view */ public getCurrentView(): interfaces.IViewDefinition | undefined { return this.currentView; } /** * Get UI state for serialization */ public getUIState(): interfaces.IAppUIState { return { currentViewId: this.currentView?.id, mainMenuCollapsed: this.mainmenuCollapsed, secondaryMenuCollapsed: this.secondarymenuCollapsed, secondaryMenuSelectedKey: this.secondarymenuSelectedItem?.key, collapsedGroups: [], // TODO: Get from secondarymenu if needed timestamp: Date.now(), }; } /** * Restore UI state from a state object */ public restoreUIState(state: interfaces.IAppUIState): void { if (state.mainMenuCollapsed !== undefined) { this.mainmenuCollapsed = state.mainMenuCollapsed; } if (state.secondaryMenuCollapsed !== undefined) { this.secondarymenuCollapsed = state.secondaryMenuCollapsed; } if (state.currentViewId) { this.navigateToView(state.currentViewId); } } /** * Save current UI state */ public saveState(): void { this.stateManager?.save(this.getUIState()); } /** * Load and restore saved UI state */ public loadState(): boolean { const state = this.stateManager?.load(); if (state) { this.restoreUIState(state); return true; } return false; } /** * Get access to the view registry */ public getViewRegistry(): ViewRegistry { return this.viewRegistry; } /** * Get access to the router */ public getRouter(): AppRouter | null { return this.router; } // ========================================== // NEW: Private helper methods // ========================================== private applyConfig(config: interfaces.IAppConfig): void { // Register views if (config.views) { this.viewRegistry.clear(); this.viewRegistry.registerAll(config.views); } // Apply branding if (config.branding) { this.mainmenuLogoIcon = config.branding.logoIcon || ''; this.mainmenuLogoText = config.branding.logoText || ''; } // Apply app bar config if (config.appBar) { this.appbarMenuItems = config.appBar.menuItems || []; this.appbarBreadcrumbs = config.appBar.breadcrumbs || ''; this.appbarBreadcrumbSeparator = config.appBar.breadcrumbSeparator || ' > '; this.appbarShowWindowControls = config.appBar.showWindowControls ?? true; this.appbarShowSearch = config.appBar.showSearch ?? false; this.appbarUser = config.appBar.user; this.appbarProfileMenuItems = config.appBar.profileMenuItems || []; } // Build main menu from view references if (config.mainMenu) { this.mainmenuGroups = this.buildMainMenuGroups(config); this.mainmenuBottomTabs = this.buildBottomTabs(config); } // Initialize state manager if (config.statePersistence) { this.stateManager = new StateManager(config.statePersistence); } // Initialize router if (config.routing && config.routing.mode !== 'none') { this.router = new AppRouter(config.routing, this.viewRegistry); this.router.onRouteChange((viewId) => { const view = this.viewRegistry.get(viewId); if (view) { this.loadView(view); } }); } // Bind event callbacks if (config.onViewChange) { this.addEventListener('view-change', ((e: CustomEvent) => { config.onViewChange!(e.detail.viewId, e.detail.view); }) as EventListener); } if (config.onSearch) { this.addEventListener('appbar-search-click', () => { config.onSearch!(); }); } } private buildMainMenuGroups(config: interfaces.IAppConfig): interfaces.IMenuGroup[] { if (!config.mainMenu?.sections) return []; return config.mainMenu.sections.map((section) => ({ name: section.name, tabs: section.views .map((viewId) => { const view = this.viewRegistry.get(viewId); if (!view) { console.warn(`View "${viewId}" not found in registry`); return null; } return { key: view.name, iconName: view.iconName, action: () => this.navigateToView(viewId), }; }) .filter(Boolean) as interfaces.ITab[], })); } private buildBottomTabs(config: interfaces.IAppConfig): interfaces.ITab[] { if (!config.mainMenu?.bottomItems) return []; return config.mainMenu.bottomItems .map((viewId) => { const view = this.viewRegistry.get(viewId); if (!view) { console.warn(`View "${viewId}" not found in registry`); return null; } return { key: view.name, iconName: view.iconName, action: () => this.navigateToView(viewId), }; }) .filter(Boolean) as interfaces.ITab[]; } private loadView(view: interfaces.IViewDefinition): void { const previousView = this.currentView; this.currentView = view; // Update secondary menu if (view.secondaryMenu) { this.secondarymenuGroups = view.secondaryMenu; this.secondarymenuHeading = view.name; } // Update content tabs if (view.contentTabs) { this.maincontentTabs = view.contentTabs; } // Render view content into the view container const viewContainer = this.maincontent?.shadowRoot?.querySelector('.view-container') || this.shadowRoot?.querySelector('.view-container'); if (viewContainer) { this.viewRegistry.renderView(view.id, viewContainer as HTMLElement); } // Save state if configured this.stateManager?.update({ currentViewId: view.id }); // Dispatch event this.dispatchEvent( new CustomEvent('view-change', { detail: { viewId: view.id, view, previousView }, bubbles: true, composed: true, }) ); } }