Files
dees-catalog/readme.appui-architecture.md
Juergen Kunz 5b4319432c feat: Enhance dees-appui components with dynamic tab and menu configurations
- 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.
2025-06-17 08:41:36 +00:00

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:

  1. Identify Views: Map existing routes/pages to views
  2. Extract Components: Move page-specific components into view folders
  3. Define View Configs: Create IAppView configurations
  4. Update Navigation: Replace existing routing with view navigation
  5. Migrate State: Move page state to ViewManager
  6. 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