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.
This commit is contained in:
513
readme.appui-architecture.md
Normal file
513
readme.appui-architecture.md
Normal file
@ -0,0 +1,513 @@
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
interface IAppViewLifecycle extends IAppView {
|
||||
onActivate?: () => Promise<void>;
|
||||
onDeactivate?: () => Promise<void>;
|
||||
onTabChange?: (oldTab: string, newTab: string) => void;
|
||||
onDestroy?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Dynamic Menu Generation
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
Reference in New Issue
Block a user