- Updated dees-appui-mainmenu to accept dynamic tabs with actions and icons. - Modified dees-appui-mainselector to support dynamic selection options. - Introduced dees-appui-tabs for improved tab navigation with customizable styles. - Added dees-appui-view to manage views with tabs and content dynamically. - Implemented event dispatching for tab and option selections. - Created a comprehensive architecture documentation for dees-appui system. - Added demo implementations for dees-appui-base and other components. - Improved responsiveness and user interaction feedback across components.
13 KiB
13 KiB
Building Applications with dees-appui Architecture
Overview
The dees-appui system provides a comprehensive framework for building desktop-style web applications with a consistent layout, navigation, and view management system. This document outlines the architecture and best practices for building applications using these components.
Core Architecture
Component Hierarchy
dees-appui-base
├── dees-appui-appbar (top menu bar)
├── dees-appui-mainmenu (left sidebar - primary navigation)
├── dees-appui-mainselector (second sidebar - contextual navigation)
├── dees-appui-maincontent (main content area)
│ └── dees-appui-view (view container)
│ └── dees-appui-tabs (tab navigation within views)
└── dees-appui-activitylog (right sidebar - optional)
View-Based Architecture
The system is built around the concept of Views - self-contained modules that represent different sections of your application. Each view can have:
- Its own tabs for sub-navigation
- Menu items for the selector (contextual navigation)
- Content areas with dynamic loading
- State management
- Event handling
Implementation Plan
Phase 1: Application Shell Setup
// app-shell.ts
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import type { IAppView } from '@design.estate/dees-catalog';
@customElement('my-app-shell')
export class MyAppShell extends LitElement {
@property({ type: Array })
views: IAppView[] = [];
@property({ type: String })
activeViewId: string = '';
render() {
const activeView = this.views.find(v => v.id === this.activeViewId);
return html`
<dees-appui-base
.appbarMenuItems=${this.getAppBarMenuItems()}
.appbarBreadcrumbs=${this.getBreadcrumbs()}
.appbarTheme=${'dark'}
.appbarUser=${{ name: 'User', status: 'online' }}
.mainmenuTabs=${this.getMainMenuTabs()}
.mainselectorOptions=${activeView?.menuItems || []}
@mainmenu-tab-select=${this.handleMainMenuSelect}
@mainselector-option-select=${this.handleSelectorSelect}
>
<dees-appui-view
slot="maincontent"
.viewConfig=${activeView}
@view-tab-select=${this.handleViewTabSelect}
></dees-appui-view>
</dees-appui-base>
`;
}
}
Phase 2: View Definition
// views/dashboard-view.ts
export const dashboardView: IAppView = {
id: 'dashboard',
name: 'Dashboard',
description: 'System overview and metrics',
iconName: 'home',
tabs: [
{
key: 'overview',
iconName: 'chart-line',
action: () => console.log('Overview selected'),
content: () => html`
<dashboard-overview></dashboard-overview>
`
},
{
key: 'metrics',
iconName: 'tachometer-alt',
action: () => console.log('Metrics selected'),
content: () => html`
<dashboard-metrics></dashboard-metrics>
`
},
{
key: 'alerts',
iconName: 'bell',
action: () => console.log('Alerts selected'),
content: () => html`
<dashboard-alerts></dashboard-alerts>
`
}
],
menuItems: [
{ key: 'Time Range', action: () => showTimeRangeSelector() },
{ key: 'Refresh Rate', action: () => showRefreshSettings() },
{ key: 'Export Data', action: () => exportDashboardData() }
]
};
Phase 3: View Management System
// services/view-manager.ts
export class ViewManager {
private views: Map<string, IAppView> = new Map();
private activeView: IAppView | null = null;
private viewCache: Map<string, any> = new Map();
registerView(view: IAppView) {
this.views.set(view.id, view);
}
async activateView(viewId: string) {
const view = this.views.get(viewId);
if (!view) throw new Error(`View ${viewId} not found`);
// Deactivate current view
if (this.activeView) {
await this.deactivateView(this.activeView.id);
}
// Activate new view
this.activeView = view;
// Update navigation
this.updateMainSelector(view.menuItems);
this.updateBreadcrumbs(view);
// Load view data if needed
if (!this.viewCache.has(viewId)) {
await this.loadViewData(view);
}
return view;
}
private async loadViewData(view: IAppView) {
// Implement lazy loading of view data
const viewData = await import(`./views/${view.id}/data.js`);
this.viewCache.set(view.id, viewData);
}
}
Phase 4: Navigation Integration
// navigation/app-navigation.ts
export class AppNavigation {
constructor(
private viewManager: ViewManager,
private appShell: MyAppShell
) {}
setupMainMenu(): ITab[] {
return [
{
key: 'dashboard',
iconName: 'home',
action: () => this.navigateToView('dashboard')
},
{
key: 'projects',
iconName: 'folder',
action: () => this.navigateToView('projects')
},
{
key: 'analytics',
iconName: 'chart-bar',
action: () => this.navigateToView('analytics')
},
{
key: 'settings',
iconName: 'cog',
action: () => this.navigateToView('settings')
}
];
}
async navigateToView(viewId: string) {
const view = await this.viewManager.activateView(viewId);
this.appShell.activeViewId = viewId;
// Update URL
window.history.pushState(
{ viewId },
view.name,
`/${viewId}`
);
}
handleBrowserNavigation() {
window.addEventListener('popstate', (event) => {
if (event.state?.viewId) {
this.navigateToView(event.state.viewId);
}
});
}
}
Phase 5: Dynamic View Loading
// views/view-loader.ts
export class ViewLoader {
private loadedViews: Set<string> = new Set();
async loadView(viewId: string): Promise<IAppView> {
if (this.loadedViews.has(viewId)) {
return this.getViewConfig(viewId);
}
// Dynamic import
const viewModule = await import(`./views/${viewId}/index.js`);
const viewConfig = viewModule.default as IAppView;
// Register custom elements if needed
if (viewModule.registerElements) {
await viewModule.registerElements();
}
this.loadedViews.add(viewId);
return viewConfig;
}
async preloadViews(viewIds: string[]) {
const promises = viewIds.map(id => this.loadView(id));
await Promise.all(promises);
}
}
Best Practices
1. View Organization
src/
├── views/
│ ├── dashboard/
│ │ ├── index.ts # View configuration
│ │ ├── data.ts # Data fetching/management
│ │ ├── components/ # View-specific components
│ │ │ ├── dashboard-overview.ts
│ │ │ ├── dashboard-metrics.ts
│ │ │ └── dashboard-alerts.ts
│ │ └── styles.ts # View-specific styles
│ ├── projects/
│ │ └── ...
│ └── settings/
│ └── ...
├── services/
│ ├── view-manager.ts
│ ├── navigation.ts
│ └── state-manager.ts
└── app-shell.ts
2. State Management
// services/state-manager.ts
export class StateManager {
private viewStates: Map<string, any> = new Map();
saveViewState(viewId: string, state: any) {
this.viewStates.set(viewId, {
...this.getViewState(viewId),
...state,
lastUpdated: Date.now()
});
}
getViewState(viewId: string): any {
return this.viewStates.get(viewId) || {};
}
// Persist to localStorage
persistState() {
const serialized = JSON.stringify(
Array.from(this.viewStates.entries())
);
localStorage.setItem('app-state', serialized);
}
restoreState() {
const saved = localStorage.getItem('app-state');
if (saved) {
const entries = JSON.parse(saved);
this.viewStates = new Map(entries);
}
}
}
3. View Communication
// events/view-events.ts
export class ViewEventBus {
private eventTarget = new EventTarget();
emit(eventName: string, detail: any) {
this.eventTarget.dispatchEvent(
new CustomEvent(eventName, { detail })
);
}
on(eventName: string, handler: (detail: any) => void) {
this.eventTarget.addEventListener(eventName, (e: CustomEvent) => {
handler(e.detail);
});
}
// Cross-view communication
sendMessage(fromView: string, toView: string, message: any) {
this.emit('view-message', {
from: fromView,
to: toView,
message
});
}
}
4. Responsive Design
// views/responsive-view.ts
export const createResponsiveView = (config: IAppView): IAppView => {
return {
...config,
tabs: config.tabs.map(tab => ({
...tab,
content: () => html`
<div class="view-content ${getDeviceClass()}">
${tab.content()}
</div>
`
}))
};
};
function getDeviceClass(): string {
const width = window.innerWidth;
if (width < 768) return 'mobile';
if (width < 1024) return 'tablet';
return 'desktop';
}
5. Performance Optimization
// optimization/lazy-components.ts
export const lazyComponent = (
importFn: () => Promise<any>,
componentName: string
) => {
let loaded = false;
return () => {
if (!loaded) {
importFn().then(() => {
loaded = true;
});
return html`<dees-spinner></dees-spinner>`;
}
return html`<${componentName}></${componentName}>`;
};
};
// Usage in view
tabs: [
{
key: 'heavy-component',
content: lazyComponent(
() => import('./components/heavy-component.js'),
'heavy-component'
)
}
]
Advanced Features
1. View Permissions
interface IAppViewWithPermissions extends IAppView {
requiredPermissions?: string[];
visibleTo?: (user: User) => boolean;
}
class PermissionManager {
canAccessView(view: IAppViewWithPermissions, user: User): boolean {
if (view.visibleTo) {
return view.visibleTo(user);
}
if (view.requiredPermissions) {
return view.requiredPermissions.every(
perm => user.permissions.includes(perm)
);
}
return true;
}
}
2. View Lifecycle Hooks
interface IAppViewLifecycle extends IAppView {
onActivate?: () => Promise<void>;
onDeactivate?: () => Promise<void>;
onTabChange?: (oldTab: string, newTab: string) => void;
onDestroy?: () => void;
}
3. Dynamic Menu Generation
class DynamicMenuBuilder {
buildMainMenu(views: IAppView[], user: User): ITab[] {
return views
.filter(view => this.canShowInMenu(view, user))
.map(view => ({
key: view.id,
iconName: view.iconName || 'file',
action: () => this.navigation.navigateToView(view.id)
}));
}
buildSelectorMenu(view: IAppView, context: any): ISelectionOption[] {
const baseItems = view.menuItems || [];
const contextItems = this.getContextualItems(view, context);
return [...baseItems, ...contextItems];
}
}
Migration Strategy
For existing applications:
- Identify Views: Map existing routes/pages to views
- Extract Components: Move page-specific components into view folders
- Define View Configs: Create IAppView configurations
- Update Navigation: Replace existing routing with view navigation
- Migrate State: Move page state to ViewManager
- Test & Optimize: Ensure smooth transitions and performance
Example Application Structure
// main.ts
import { ViewManager } from './services/view-manager.js';
import { AppNavigation } from './services/navigation.js';
import { dashboardView } from './views/dashboard/index.js';
import { projectsView } from './views/projects/index.js';
import { settingsView } from './views/settings/index.js';
const app = new MyAppShell();
const viewManager = new ViewManager();
const navigation = new AppNavigation(viewManager, app);
// Register views
viewManager.registerView(dashboardView);
viewManager.registerView(projectsView);
viewManager.registerView(settingsView);
// Setup navigation
app.views = [dashboardView, projectsView, settingsView];
navigation.setupMainMenu();
navigation.handleBrowserNavigation();
// Initial navigation
navigation.navigateToView('dashboard');
document.body.appendChild(app);
This architecture provides:
- Modularity: Each view is self-contained
- Scalability: Easy to add new views
- Performance: Lazy loading and caching
- Consistency: Unified navigation and layout
- Flexibility: Customizable per view
- Maintainability: Clear separation of concerns