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
|
256
readme.md
256
readme.md
@ -306,17 +306,119 @@ Submit button component specifically designed for `DeesForm`.
|
||||
### Layout Components
|
||||
|
||||
#### `DeesAppuiBase`
|
||||
Base container component for application layout structure.
|
||||
Base container component for application layout structure with integrated appbar, menu system, and content areas.
|
||||
|
||||
```typescript
|
||||
<dees-appui-base>
|
||||
<dees-appui-mainmenu></dees-appui-mainmenu>
|
||||
<dees-appui-mainselector></dees-appui-mainselector>
|
||||
<dees-appui-maincontent></dees-appui-maincontent>
|
||||
<dees-appui-appbar></dees-appui-appbar>
|
||||
<dees-appui-base
|
||||
// Appbar configuration
|
||||
.appbarMenuItems=${[
|
||||
{
|
||||
name: 'File',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'New', shortcut: 'Cmd+N', iconName: 'file-plus', action: async () => {} },
|
||||
{ name: 'Open', shortcut: 'Cmd+O', iconName: 'folder-open', action: async () => {} },
|
||||
{ divider: true },
|
||||
{ name: 'Save', shortcut: 'Cmd+S', iconName: 'save', action: async () => {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => {} },
|
||||
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => {} }
|
||||
]
|
||||
}
|
||||
]}
|
||||
.appbarBreadcrumbs=${'Dashboard > Overview'}
|
||||
.appbarTheme=${'dark'}
|
||||
.appbarUser=${{
|
||||
name: 'John Doe',
|
||||
status: 'online'
|
||||
}}
|
||||
.appbarShowSearch=${true}
|
||||
.appbarShowWindowControls=${true}
|
||||
|
||||
// Main menu configuration (left sidebar)
|
||||
.mainmenuTabs=${[
|
||||
{ key: 'dashboard', iconName: 'home', action: () => {} },
|
||||
{ key: 'projects', iconName: 'folder', action: () => {} },
|
||||
{ key: 'settings', iconName: 'cog', action: () => {} }
|
||||
]}
|
||||
.mainmenuSelectedTab=${selectedTab}
|
||||
|
||||
// Selector configuration (second sidebar)
|
||||
.mainselectorOptions=${[
|
||||
{ key: 'Overview', action: () => {} },
|
||||
{ key: 'Components', action: () => {} },
|
||||
{ key: 'Services', action: () => {} }
|
||||
]}
|
||||
.mainselectorSelectedOption=${selectedOption}
|
||||
|
||||
// Main content tabs
|
||||
.maincontentTabs=${[
|
||||
{ key: 'tab1', iconName: 'file', action: () => {} }
|
||||
]}
|
||||
|
||||
// Event handlers
|
||||
@appbar-menu-select=${(e) => handleMenuSelect(e.detail)}
|
||||
@appbar-breadcrumb-navigate=${(e) => handleBreadcrumbNav(e.detail)}
|
||||
@appbar-search-click=${() => handleSearch()}
|
||||
@appbar-user-menu-open=${() => handleUserMenu()}
|
||||
@mainmenu-tab-select=${(e) => handleTabSelect(e.detail)}
|
||||
@mainselector-option-select=${(e) => handleOptionSelect(e.detail)}
|
||||
>
|
||||
<div slot="maincontent">
|
||||
<!-- Your main application content goes here -->
|
||||
</div>
|
||||
</dees-appui-base>
|
||||
```
|
||||
|
||||
Key Features:
|
||||
- **Integrated Layout System**: Automatically arranges appbar, sidebars, and content area
|
||||
- **Centralized Configuration**: Pass properties to all child components from one place
|
||||
- **Event Propagation**: All child component events are re-emitted for easy handling
|
||||
- **Responsive Grid**: Uses CSS Grid for flexible, responsive layout
|
||||
- **Slot Support**: Main content area supports custom content via slots
|
||||
|
||||
Layout Structure:
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ AppBar │
|
||||
├────┬──────────────┬─────────────────┬──────────┤
|
||||
│ │ │ │ │
|
||||
│ M │ Selector │ Main Content │ Activity │
|
||||
│ e │ │ │ Log │
|
||||
│ n │ │ │ │
|
||||
│ u │ │ │ │
|
||||
│ │ │ │ │
|
||||
└────┴──────────────┴─────────────────┴──────────┘
|
||||
```
|
||||
|
||||
Grid Configuration:
|
||||
- Main Menu: 60px width
|
||||
- Selector: 240px width
|
||||
- Main Content: Flexible (1fr)
|
||||
- Activity Log: 240px width
|
||||
|
||||
Child Component Access:
|
||||
```typescript
|
||||
// Access child components after firstUpdated
|
||||
const base = document.querySelector('dees-appui-base');
|
||||
base.appbar; // DeesAppuiAppbar instance
|
||||
base.mainmenu; // DeesAppuiMainmenu instance
|
||||
base.mainselector; // DeesAppuiMainselector instance
|
||||
base.maincontent; // DeesAppuiMaincontent instance
|
||||
base.activitylog; // DeesAppuiActivitylog instance
|
||||
```
|
||||
|
||||
Best Practices:
|
||||
1. **Configuration**: Set all properties on the base component for consistency
|
||||
2. **Event Handling**: Listen to events on the base component rather than child components
|
||||
3. **Content**: Use the `maincontent` slot for your application's primary interface
|
||||
4. **State Management**: Manage selected tabs and options at the base component level
|
||||
|
||||
#### `DeesAppuiMainmenu`
|
||||
Main navigation menu component for application-wide navigation.
|
||||
|
||||
@ -378,28 +480,148 @@ Main content area with tab management support.
|
||||
```
|
||||
|
||||
#### `DeesAppuiAppbar`
|
||||
Top application bar with actions and status information.
|
||||
Professional application bar component with hierarchical menus, breadcrumb navigation, and user account management.
|
||||
|
||||
```typescript
|
||||
<dees-appui-appbar
|
||||
title="My Application"
|
||||
.actions=${[
|
||||
.menuItems=${[
|
||||
{
|
||||
icon: 'bell',
|
||||
label: 'Notifications',
|
||||
action: () => showNotifications()
|
||||
name: 'File',
|
||||
action: async () => {}, // No-op for parent menu items
|
||||
submenu: [
|
||||
{
|
||||
name: 'New File',
|
||||
shortcut: 'Cmd+N',
|
||||
iconName: 'file-plus',
|
||||
action: async () => handleNewFile()
|
||||
},
|
||||
{
|
||||
name: 'Open...',
|
||||
shortcut: 'Cmd+O',
|
||||
iconName: 'folder-open',
|
||||
action: async () => handleOpen()
|
||||
},
|
||||
{ divider: true }, // Menu separator
|
||||
{
|
||||
name: 'Save',
|
||||
shortcut: 'Cmd+S',
|
||||
iconName: 'save',
|
||||
action: async () => handleSave(),
|
||||
disabled: true // Disabled state
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: 'user',
|
||||
label: 'Profile',
|
||||
action: () => showProfile()
|
||||
name: 'Edit',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => handleUndo() },
|
||||
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => handleRedo() }
|
||||
]
|
||||
}
|
||||
]}
|
||||
showSearch // Optional: display search bar
|
||||
@search=${handleSearch}
|
||||
.breadcrumbs=${'Project > src > components > AppBar.ts'}
|
||||
.breadcrumbSeparator=${' > '}
|
||||
.showWindowControls=${true}
|
||||
.showSearch=${true}
|
||||
.theme=${'dark'} // Options: 'light' | 'dark'
|
||||
.user=${{
|
||||
name: 'John Doe',
|
||||
avatar: '/path/to/avatar.jpg', // Optional
|
||||
status: 'online' // Options: 'online' | 'offline' | 'busy' | 'away'
|
||||
}}
|
||||
@menu-select=${(e) => handleMenuSelect(e.detail.item)}
|
||||
@breadcrumb-navigate=${(e) => handleBreadcrumbClick(e.detail)}
|
||||
@search-click=${() => handleSearchClick()}
|
||||
@user-menu-open=${() => handleUserMenuOpen()}
|
||||
></dees-appui-appbar>
|
||||
```
|
||||
|
||||
Key Features:
|
||||
- **Hierarchical Menu System**
|
||||
- Top-level text-only menus (following desktop UI standards)
|
||||
- Dropdown submenus with icons and keyboard shortcuts
|
||||
- Support for nested submenus
|
||||
- Menu dividers for visual grouping
|
||||
- Disabled state support
|
||||
|
||||
- **Keyboard Navigation**
|
||||
- Tab navigation between top-level items
|
||||
- Arrow keys for dropdown navigation (Up/Down in dropdowns, Left/Right between top items)
|
||||
- Enter to select items
|
||||
- Escape to close dropdowns
|
||||
- Home/End keys for first/last item
|
||||
|
||||
- **Breadcrumb Navigation**
|
||||
- Customizable breadcrumb trail
|
||||
- Configurable separator
|
||||
- Click events for navigation
|
||||
|
||||
- **User Account Section**
|
||||
- User avatar with fallback to initials
|
||||
- Status indicator (online, offline, busy, away)
|
||||
- Click handler for user menu
|
||||
|
||||
- **Visual Features**
|
||||
- Light and dark theme support
|
||||
- Smooth animations and transitions
|
||||
- Window controls integration
|
||||
- Search icon with click handler
|
||||
- Responsive layout using CSS Grid
|
||||
|
||||
- **Accessibility**
|
||||
- Full ARIA support (menubar, menuitem roles)
|
||||
- Keyboard navigation
|
||||
- Focus management
|
||||
- Screen reader compatible
|
||||
|
||||
Menu Item Interface:
|
||||
```typescript
|
||||
// Regular menu item
|
||||
interface IAppBarMenuItemRegular {
|
||||
name: string; // Display text
|
||||
action: () => Promise<any>; // Click handler
|
||||
iconName?: string; // Optional icon (for dropdown items)
|
||||
shortcut?: string; // Keyboard shortcut display
|
||||
submenu?: IAppBarMenuItem[]; // Nested menu items
|
||||
disabled?: boolean; // Disabled state
|
||||
checked?: boolean; // For checkbox menu items
|
||||
radioGroup?: string; // For radio button menu items
|
||||
}
|
||||
|
||||
// Divider item
|
||||
interface IAppBarMenuDivider {
|
||||
divider: true;
|
||||
}
|
||||
|
||||
// Combined type
|
||||
type IAppBarMenuItem = IAppBarMenuItemRegular | IAppBarMenuDivider;
|
||||
```
|
||||
|
||||
Best Practices:
|
||||
1. **Menu Structure**
|
||||
- Keep top-level menus text-only (no icons)
|
||||
- Use icons in dropdown items for visual clarity
|
||||
- Group related actions with dividers
|
||||
- Provide keyboard shortcuts for common actions
|
||||
|
||||
2. **Navigation**
|
||||
- Use breadcrumbs for deep navigation hierarchies
|
||||
- Keep breadcrumb labels concise
|
||||
- Provide meaningful navigation events
|
||||
|
||||
3. **User Experience**
|
||||
- Show user status when relevant
|
||||
- Provide clear visual feedback
|
||||
- Ensure smooth transitions
|
||||
- Handle edge cases (long menus, small screens)
|
||||
|
||||
4. **Accessibility**
|
||||
- Always provide text labels
|
||||
- Ensure keyboard navigation works
|
||||
- Test with screen readers
|
||||
- Maintain focus management
|
||||
|
||||
#### `DeesMobileNavigation`
|
||||
Responsive navigation component for mobile devices.
|
||||
|
||||
|
@ -361,7 +361,7 @@ export class DeesAppuiBar extends DeesElement {
|
||||
aria-haspopup="${hasSubmenu}"
|
||||
aria-expanded="${isActive}"
|
||||
>
|
||||
${menuItem.iconName ? html`<dees-icon .iconName=${menuItem.iconName}></dees-icon>` : ''}
|
||||
${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''}
|
||||
${menuItem.name}
|
||||
${hasSubmenu ? this.renderDropdown(menuItem.submenu, itemId, isActive) : ''}
|
||||
</div>
|
||||
@ -400,7 +400,7 @@ export class DeesAppuiBar extends DeesElement {
|
||||
role="menuitem"
|
||||
tabindex="${menuItem.disabled ? -1 : 0}"
|
||||
>
|
||||
${menuItem.iconName ? html`<dees-icon .iconName=${menuItem.iconName}></dees-icon>` : ''}
|
||||
${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''}
|
||||
<span>${menuItem.name}</span>
|
||||
${menuItem.shortcut ? html`<span class="shortcut">${menuItem.shortcut}</span>` : ''}
|
||||
</div>
|
||||
|
203
ts_web/elements/dees-appui-base.demo.ts
Normal file
203
ts_web/elements/dees-appui-base.demo.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import type { DeesAppuiBase } from './dees-appui-base.js';
|
||||
import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js';
|
||||
import type { ITab } from './interfaces/tab.js';
|
||||
import type { ISelectionOption } from './interfaces/selectionoption.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => {
|
||||
// Menu items for the appbar
|
||||
const menuItems: IAppBarMenuItem[] = [
|
||||
{
|
||||
name: 'File',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'New Project', shortcut: 'Cmd+N', iconName: 'filePlus', action: async () => console.log('New project') },
|
||||
{ name: 'Open Project...', shortcut: 'Cmd+O', iconName: 'folderOpen', action: async () => console.log('Open project') },
|
||||
{ name: 'Recent Projects', action: async () => {}, submenu: [
|
||||
{ name: 'my-app', action: async () => console.log('Open my-app') },
|
||||
{ name: 'component-lib', action: async () => console.log('Open component-lib') },
|
||||
{ name: 'api-server', action: async () => console.log('Open api-server') },
|
||||
]},
|
||||
{ divider: true },
|
||||
{ name: 'Save All', shortcut: 'Cmd+Shift+S', iconName: 'save', action: async () => console.log('Save all') },
|
||||
{ divider: true },
|
||||
{ name: 'Close Project', action: async () => console.log('Close project') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => console.log('Undo') },
|
||||
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => console.log('Redo') },
|
||||
{ divider: true },
|
||||
{ name: 'Cut', shortcut: 'Cmd+X', iconName: 'scissors', action: async () => console.log('Cut') },
|
||||
{ name: 'Copy', shortcut: 'Cmd+C', iconName: 'copy', action: async () => console.log('Copy') },
|
||||
{ name: 'Paste', shortcut: 'Cmd+V', iconName: 'clipboard', action: async () => console.log('Paste') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'View',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => console.log('Toggle sidebar') },
|
||||
{ name: 'Toggle Terminal', shortcut: 'Cmd+J', iconName: 'terminal', action: async () => console.log('Toggle terminal') },
|
||||
{ divider: true },
|
||||
{ name: 'Zoom In', shortcut: 'Cmd++', iconName: 'zoomIn', action: async () => console.log('Zoom in') },
|
||||
{ name: 'Zoom Out', shortcut: 'Cmd+-', iconName: 'zoomOut', action: async () => console.log('Zoom out') },
|
||||
{ name: 'Reset Zoom', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Help',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
|
||||
{ name: 'Release Notes', iconName: 'fileText', action: async () => console.log('Release notes') },
|
||||
{ divider: true },
|
||||
{ name: 'Report Issue', iconName: 'bug', action: async () => console.log('Report issue') },
|
||||
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Main menu tabs (left sidebar)
|
||||
const mainMenuTabs: ITab[] = [
|
||||
{ key: 'dashboard', iconName: 'home', action: () => console.log('Dashboard selected') },
|
||||
{ key: 'projects', iconName: 'folder', action: () => console.log('Projects selected') },
|
||||
{ key: 'analytics', iconName: 'lineChart', action: () => console.log('Analytics selected') },
|
||||
{ key: 'settings', iconName: 'settings', action: () => console.log('Settings selected') },
|
||||
];
|
||||
|
||||
// Selector options (second sidebar)
|
||||
const selectorOptions: ISelectionOption[] = [
|
||||
{ key: 'Overview', action: () => console.log('Overview selected') },
|
||||
{ key: 'Components', action: () => console.log('Components selected') },
|
||||
{ key: 'Services', action: () => console.log('Services selected') },
|
||||
{ key: 'Database', action: () => console.log('Database selected') },
|
||||
{ key: 'Settings', action: () => console.log('Settings selected') },
|
||||
];
|
||||
|
||||
// Main content tabs
|
||||
const mainContentTabs: ITab[] = [
|
||||
{ key: 'Details', iconName: 'file', action: () => console.log('Details tab') },
|
||||
{ key: 'Logs', iconName: 'list', action: () => console.log('Logs tab') },
|
||||
{ key: 'Metrics', iconName: 'lineChart', action: () => console.log('Metrics tab') },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
const appuiBase = elementArg.querySelector('dees-appui-base') as DeesAppuiBase;
|
||||
|
||||
// Add event listeners for theme toggle
|
||||
const themeButtons = elementArg.querySelectorAll('.theme-toggle dees-button');
|
||||
themeButtons[0].addEventListener('click', () => {
|
||||
if (appuiBase.appbar) {
|
||||
appuiBase.appbar.theme = 'dark';
|
||||
}
|
||||
});
|
||||
themeButtons[1].addEventListener('click', () => {
|
||||
if (appuiBase.appbar) {
|
||||
appuiBase.appbar.theme = 'light';
|
||||
}
|
||||
});
|
||||
|
||||
// Update breadcrumbs dynamically
|
||||
const updateBreadcrumbs = (path: string) => {
|
||||
if (appuiBase.appbar) {
|
||||
appuiBase.appbar.breadcrumbs = path;
|
||||
}
|
||||
};
|
||||
|
||||
// Simulate navigation
|
||||
setTimeout(() => {
|
||||
updateBreadcrumbs('Dashboard > Overview');
|
||||
}, 2000);
|
||||
|
||||
setTimeout(() => {
|
||||
updateBreadcrumbs('Dashboard > Projects > my-app > src > index.ts');
|
||||
}, 4000);
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-appui-base
|
||||
.appbarMenuItems=${menuItems}
|
||||
.appbarBreadcrumbs=${'Dashboard'}
|
||||
.appbarTheme=${'dark'}
|
||||
.appbarUser=${{
|
||||
name: 'Jane Smith',
|
||||
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
|
||||
}}
|
||||
.appbarShowWindowControls=${true}
|
||||
.appbarShowSearch=${true}
|
||||
.mainmenuTabs=${mainMenuTabs}
|
||||
.mainselectorOptions=${selectorOptions}
|
||||
.maincontentTabs=${mainContentTabs}
|
||||
@appbar-menu-select=${(e: CustomEvent) => console.log('Menu selected:', e.detail)}
|
||||
@appbar-breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb:', e.detail)}
|
||||
@appbar-search-click=${() => console.log('Search clicked')}
|
||||
@appbar-user-menu-open=${() => console.log('User menu opened')}
|
||||
@mainmenu-tab-select=${(e: CustomEvent) => console.log('Tab selected:', e.detail)}
|
||||
@mainselector-option-select=${(e: CustomEvent) => console.log('Option selected:', e.detail)}
|
||||
>
|
||||
<div slot="maincontent" style="padding: 40px; color: #ccc;">
|
||||
<h1>Application Content</h1>
|
||||
<p>This is the main content area where your application's primary interface would be displayed.</p>
|
||||
<p>The layout includes:</p>
|
||||
<ul>
|
||||
<li>App bar with menus, breadcrumbs, and user account</li>
|
||||
<li>Main menu (left sidebar) for primary navigation</li>
|
||||
<li>Selector menu (second sidebar) for sub-navigation</li>
|
||||
<li>Main content area (this section)</li>
|
||||
<li>Activity log (right sidebar)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</dees-appui-base>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>Theme</label>
|
||||
<dees-button-group class="theme-toggle">
|
||||
<dees-button>Dark</dees-button>
|
||||
<dees-button>Light</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
@ -6,11 +6,86 @@ import {
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import type { DeesAppuiBar } from './dees-appui-appbar.js';
|
||||
import type { DeesAppuiMainmenu } from './dees-appui-mainmenu.js';
|
||||
import type { DeesAppuiMainselector } from './dees-appui-mainselector.js';
|
||||
import type { DeesAppuiMaincontent } from './dees-appui-maincontent.js';
|
||||
import type { DeesAppuiActivitylog } from './dees-appui-activitylog.js';
|
||||
import { demoFunc } from './dees-appui-base.demo.js';
|
||||
|
||||
// Import child components
|
||||
import './dees-appui-appbar.js';
|
||||
import './dees-appui-mainmenu.js';
|
||||
import './dees-appui-mainselector.js';
|
||||
import './dees-appui-maincontent.js';
|
||||
import './dees-appui-activitylog.js';
|
||||
|
||||
@customElement('dees-appui-base')
|
||||
export class DeesAppuiBase extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-base></dees-appui-base>`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
// Properties for appbar
|
||||
@property({ type: Array })
|
||||
public appbarMenuItems: interfaces.IAppBarMenuItem[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public appbarBreadcrumbs: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public appbarBreadcrumbSeparator: string = ' > ';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public appbarShowWindowControls: boolean = true;
|
||||
|
||||
@property({ type: String })
|
||||
public appbarTheme: 'light' | 'dark' = 'dark';
|
||||
|
||||
@property({ type: Object })
|
||||
public appbarUser?: {
|
||||
name: string;
|
||||
avatar?: string;
|
||||
status?: 'online' | 'offline' | 'busy' | 'away';
|
||||
};
|
||||
|
||||
@property({ type: Boolean })
|
||||
public appbarShowSearch: boolean = false;
|
||||
|
||||
// Properties for mainmenu
|
||||
@property({ type: Array })
|
||||
public mainmenuTabs: interfaces.ITab[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
public mainmenuSelectedTab?: interfaces.ITab;
|
||||
|
||||
// Properties for mainselector
|
||||
@property({ type: Array })
|
||||
public mainselectorOptions: interfaces.ISelectionOption[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
public mainselectorSelectedOption?: interfaces.ISelectionOption;
|
||||
|
||||
// Properties for maincontent
|
||||
@property({ type: Array })
|
||||
public maincontentTabs: interfaces.ITab[] = [];
|
||||
|
||||
// References to child components
|
||||
@state()
|
||||
public appbar?: DeesAppuiBar;
|
||||
|
||||
@state()
|
||||
public mainmenu?: DeesAppuiMainmenu;
|
||||
|
||||
@state()
|
||||
public mainselector?: DeesAppuiMainselector;
|
||||
|
||||
@state()
|
||||
public maincontent?: DeesAppuiMaincontent;
|
||||
|
||||
@state()
|
||||
public activitylog?: DeesAppuiActivitylog;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
@ -19,6 +94,7 @@ export class DeesAppuiBase extends DeesElement {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--dees-color-appui-background, #1a1a1a);
|
||||
}
|
||||
.maingrid {
|
||||
position: absolute;
|
||||
@ -26,7 +102,7 @@ export class DeesAppuiBase extends DeesElement {
|
||||
height: calc(100% - 40px);
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 240px auto 240px;
|
||||
grid-template-columns: 60px 240px 1fr 240px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -35,13 +111,97 @@ export class DeesAppuiBase extends DeesElement {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style></style>
|
||||
<dees-appui-appbar></dees-appui-appbar>
|
||||
<dees-appui-appbar
|
||||
.menuItems=${this.appbarMenuItems}
|
||||
.breadcrumbs=${this.appbarBreadcrumbs}
|
||||
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
|
||||
.showWindowControls=${this.appbarShowWindowControls}
|
||||
.theme=${this.appbarTheme}
|
||||
.user=${this.appbarUser}
|
||||
.showSearch=${this.appbarShowSearch}
|
||||
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
|
||||
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
|
||||
@search-click=${() => this.handleAppbarSearchClick()}
|
||||
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
|
||||
></dees-appui-appbar>
|
||||
<div class="maingrid">
|
||||
<dees-appui-mainmenu></dees-appui-mainmenu>
|
||||
<dees-appui-mainselector></dees-appui-mainselector>
|
||||
<dees-appui-maincontent></dees-appui-maincontent>
|
||||
<dees-appui-mainmenu
|
||||
.tabs=${this.mainmenuTabs}
|
||||
.selectedTab=${this.mainmenuSelectedTab}
|
||||
@tab-select=${(e: CustomEvent) => this.handleMainmenuTabSelect(e)}
|
||||
></dees-appui-mainmenu>
|
||||
<dees-appui-mainselector
|
||||
.selectionOptions=${this.mainselectorOptions}
|
||||
.selectedOption=${this.mainselectorSelectedOption}
|
||||
@option-select=${(e: CustomEvent) => this.handleMainselectorOptionSelect(e)}
|
||||
></dees-appui-mainselector>
|
||||
<dees-appui-maincontent
|
||||
.tabs=${this.maincontentTabs}
|
||||
>
|
||||
<slot name="maincontent"></slot>
|
||||
</dees-appui-maincontent>
|
||||
<dees-appui-activitylog></dees-appui-activitylog>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
// Get references to child components
|
||||
this.appbar = this.shadowRoot.querySelector('dees-appui-appbar');
|
||||
this.mainmenu = this.shadowRoot.querySelector('dees-appui-mainmenu');
|
||||
this.mainselector = this.shadowRoot.querySelector('dees-appui-mainselector');
|
||||
this.maincontent = this.shadowRoot.querySelector('dees-appui-maincontent');
|
||||
this.activitylog = this.shadowRoot.querySelector('dees-appui-activitylog');
|
||||
}
|
||||
|
||||
// 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
|
||||
}));
|
||||
}
|
||||
|
||||
// Event handlers for mainmenu
|
||||
private handleMainmenuTabSelect(e: CustomEvent) {
|
||||
this.mainmenuSelectedTab = e.detail.tab;
|
||||
this.dispatchEvent(new CustomEvent('mainmenu-tab-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
// Event handlers for mainselector
|
||||
private handleMainselectorOptionSelect(e: CustomEvent) {
|
||||
this.mainselectorSelectedOption = e.detail.option;
|
||||
this.dispatchEvent(new CustomEvent('mainselector-option-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -11,24 +11,36 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import './dees-appui-tabs.js';
|
||||
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
|
||||
|
||||
@customElement('dees-appui-maincontent')
|
||||
export class DeesAppuiMaincontent extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-maincontent></dees-appui-maincontent>`;
|
||||
public static demo = () => html`
|
||||
<dees-appui-maincontent
|
||||
.tabs=${[
|
||||
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview') },
|
||||
{ key: 'Details', iconName: 'file', action: () => console.log('Details') },
|
||||
{ key: 'Settings', iconName: 'cog', action: () => console.log('Settings') },
|
||||
]}
|
||||
>
|
||||
<div slot="content" style="padding: 40px; color: #ccc;">
|
||||
<h1>Main Content Area</h1>
|
||||
<p>This is where your application content goes.</p>
|
||||
</div>
|
||||
</dees-appui-maincontent>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
public tabs: interfaces.ITab[] = [
|
||||
{ key: 'option 1', action: () => {} },
|
||||
{ key: 'a very long option', action: () => {} },
|
||||
{ key: 'reminder: set your tabs', action: () => {} },
|
||||
{ key: 'option 4', action: () => {} },
|
||||
{ key: '⚠️ Please set tabs', action: () => console.warn('No tabs configured for maincontent') },
|
||||
];
|
||||
|
||||
@property()
|
||||
public selectedTab = null;
|
||||
@property({ type: Object })
|
||||
public selectedTab: interfaces.ITab | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
@ -52,110 +64,58 @@ export class DeesAppuiMaincontent extends DeesElement {
|
||||
.topbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: #000000;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.topbar .tabsContainer {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 0px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
margin-left: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.topbar .tabsContainer .tab {
|
||||
color: #a0a0a0;
|
||||
white-space: nowrap;
|
||||
margin-right: 30px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 12px;
|
||||
transition: color 0.1s;
|
||||
}
|
||||
|
||||
.topbar .tabsContainer .tab:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.topbar .tabsContainer .tab.selectedTab {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.topbar .tabIndicator {
|
||||
.content-area {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
left: 40px;
|
||||
bottom: 0px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background: #161616;
|
||||
transition: all 0.1s;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-top: 1px solid #444444;
|
||||
}
|
||||
|
||||
.mainicon {
|
||||
top: 60px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
.topbar .tabsContainer {
|
||||
grid-template-columns: repeat(${this.tabs.length}, min-content);
|
||||
}
|
||||
</style>
|
||||
<div class="maincontainer">
|
||||
<div class="topbar">
|
||||
<div class="tabsContainer">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : null}"
|
||||
@click="${() => {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
tabArg.action();
|
||||
}}"
|
||||
>
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="tabIndicator"></div>
|
||||
<dees-appui-tabs
|
||||
.tabs=${this.tabs}
|
||||
.selectedTab=${this.selectedTab}
|
||||
.showTabIndicator=${true}
|
||||
.tabStyle=${'horizontal'}
|
||||
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
|
||||
></dees-appui-tabs>
|
||||
</div>
|
||||
<div class="content-area">
|
||||
<slot></slot>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the indicator
|
||||
*/
|
||||
private updateTabIndicator() {
|
||||
let selectedTab = this.selectedTab;
|
||||
const tabIndex = this.tabs.indexOf(selectedTab);
|
||||
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
|
||||
`.tabsContainer .tab:nth-child(${tabIndex + 1})`
|
||||
);
|
||||
const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabsContainer');
|
||||
const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left"));
|
||||
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabIndicator');
|
||||
tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px';
|
||||
tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px';
|
||||
private handleTabSelect(e: CustomEvent) {
|
||||
this.selectedTab = e.detail.tab;
|
||||
|
||||
// Re-emit the event
|
||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private updateTab(tabArg: interfaces.ITab) {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
this.selectedTab.action();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.updateTab(this.tabs[0]);
|
||||
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
await super.firstUpdated(_changedProperties);
|
||||
// Tab selection is now handled by the dees-appui-tabs component
|
||||
// But we need to ensure the tabs component is ready
|
||||
const tabsComponent = this.shadowRoot.querySelector('dees-appui-tabs') as DeesAppuiTabs;
|
||||
if (tabsComponent) {
|
||||
await tabsComponent.updateComplete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,17 +18,23 @@ import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
*/
|
||||
@customElement('dees-appui-mainmenu')
|
||||
export class DeesAppuiMainmenu extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-mainmenu></dees-appui-mainmenu>`;
|
||||
public static demo = () => html`
|
||||
<dees-appui-mainmenu
|
||||
.tabs=${[
|
||||
{ key: 'Dashboard', iconName: 'home', action: () => console.log('Dashboard') },
|
||||
{ key: 'Projects', iconName: 'folder', action: () => console.log('Projects') },
|
||||
{ key: 'Analytics', iconName: 'lineChart', action: () => console.log('Analytics') },
|
||||
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings') },
|
||||
]}
|
||||
></dees-appui-mainmenu>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
|
||||
// INSTANCE
|
||||
@property()
|
||||
@property({ type: Array })
|
||||
public tabs: interfaces.ITab[] = [
|
||||
{ key: 'option 1', iconName: 'building', action: () => {} },
|
||||
{ key: 'option 2', iconName: 'building', action: () => {} },
|
||||
{ key: 'option 3', iconName: 'building', action: () => {} },
|
||||
{ key: 'option 4', iconName: 'building', action: () => {} },
|
||||
{ key: '⚠️ Please set tabs', iconName: 'alertTriangle', action: () => console.warn('No tabs configured for mainmenu') },
|
||||
];
|
||||
|
||||
@property()
|
||||
@ -105,7 +111,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
this.updateTab(tabArg);
|
||||
}}"
|
||||
>
|
||||
<dees-icon iconFA="${tabArg.iconName as any}"></dees-icon>
|
||||
<dees-icon .icon="${tabArg.iconName ? `lucide:${tabArg.iconName}` : ''}"></dees-icon>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@ -115,7 +121,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async updateTabIndicator() {
|
||||
private updateTabIndicator() {
|
||||
let selectedTab = this.selectedTab;
|
||||
if (!selectedTab) {
|
||||
selectedTab = this.tabs[0];
|
||||
@ -124,7 +130,12 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
|
||||
`.tabsContainer .tab:nth-child(${tabIndex + 1})`
|
||||
);
|
||||
|
||||
if (!selectedTabElement) return;
|
||||
|
||||
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabIndicator');
|
||||
if (!tabIndicator) return;
|
||||
|
||||
const offsetTop = selectedTabElement.offsetTop;
|
||||
tabIndicator.style.opacity = `1`;
|
||||
tabIndicator.style.top = `calc(${offsetTop}px + (var(--menuSize) / 6))`;
|
||||
@ -134,6 +145,13 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
this.selectedTab.action();
|
||||
|
||||
// Emit tab-select event
|
||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||
detail: { tab: tabArg },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
|
@ -19,22 +19,22 @@ import {
|
||||
*/
|
||||
@customElement('dees-appui-mainselector')
|
||||
export class DeesAppuiMainselector extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-mainselector></dees-appui-mainselector>`;
|
||||
public static demo = () => html`
|
||||
<dees-appui-mainselector
|
||||
.selectionOptions=${[
|
||||
{ key: 'Overview', action: () => console.log('Overview') },
|
||||
{ key: 'Components', action: () => console.log('Components') },
|
||||
{ key: 'Services', action: () => console.log('Services') },
|
||||
{ key: 'Database', action: () => console.log('Database') },
|
||||
{ key: 'Settings', action: () => console.log('Settings') },
|
||||
]}
|
||||
></dees-appui-mainselector>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@property()
|
||||
@property({ type: Array })
|
||||
public selectionOptions: interfaces.ISelectionOption[] = [
|
||||
{
|
||||
key: 'Overview',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
key: 'option 1',
|
||||
action: () => {},
|
||||
},
|
||||
{ key: 'option 2', action: () => {} },
|
||||
{ key: 'option 3', action: () => {} },
|
||||
{ key: 'option 4', action: () => {} },
|
||||
{ key: '⚠️ Please set selection options', action: () => console.warn('No selection options configured for mainselector') },
|
||||
];
|
||||
|
||||
@property()
|
||||
@ -152,9 +152,20 @@ export class DeesAppuiMainselector extends DeesElement {
|
||||
private selectOption(optionArg: interfaces.ISelectionOption) {
|
||||
this.selectedOption = optionArg;
|
||||
this.selectedOption.action();
|
||||
|
||||
// Emit option-select event
|
||||
this.dispatchEvent(new CustomEvent('option-select', {
|
||||
detail: { option: optionArg },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.selectOption(this.selectionOptions[0]);
|
||||
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
await super.firstUpdated(_changedProperties);
|
||||
if (this.selectionOptions && this.selectionOptions.length > 0) {
|
||||
await this.updateComplete;
|
||||
this.selectOption(this.selectionOptions[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
247
ts_web/elements/dees-appui-tabs.ts
Normal file
247
ts_web/elements/dees-appui-tabs.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
@customElement('dees-appui-tabs')
|
||||
export class DeesAppuiTabs extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-tabs
|
||||
.tabs=${[
|
||||
{ key: 'Tab 1', action: () => console.log('Tab 1 clicked') },
|
||||
{ key: 'Tab 2', action: () => console.log('Tab 2 clicked') },
|
||||
{ key: 'Tab 3', action: () => console.log('Tab 3 clicked') },
|
||||
]}
|
||||
></dees-appui-tabs>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
public tabs: interfaces.ITab[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
public selectedTab: interfaces.ITab | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showTabIndicator: boolean = true;
|
||||
|
||||
@property({ type: String })
|
||||
public tabStyle: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
position: relative;
|
||||
background: #000000;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.tabsContainer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tabsContainer.horizontal {
|
||||
display: grid;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 0px;
|
||||
margin-left: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tabsContainer.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
color: #a0a0a0;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 0.1s;
|
||||
}
|
||||
|
||||
.horizontal .tab {
|
||||
margin-right: 30px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.vertical .tab {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.vertical .tab:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.tab.selectedTab {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.vertical .tab.selectedTab {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tab dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tabs-wrapper .tabIndicator {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
left: 40px;
|
||||
bottom: 0px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background: #161616;
|
||||
transition: all 0.1s;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-top: 1px solid #444444;
|
||||
}
|
||||
|
||||
.vertical .tabIndicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.tabStyle === 'horizontal' ? html`
|
||||
<style>
|
||||
.tabsContainer.horizontal {
|
||||
grid-template-columns: repeat(${this.tabs.length}, min-content);
|
||||
}
|
||||
</style>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="tabsContainer horizontal">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
||||
@click="${() => this.selectTab(tabArg)}"
|
||||
>
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${this.showTabIndicator ? html`
|
||||
<div class="tabIndicator"></div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="tabsContainer vertical">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
||||
@click="${() => this.selectTab(tabArg)}"
|
||||
>
|
||||
${tabArg.iconName ? html`<dees-icon .iconName=${tabArg.iconName}></dees-icon>` : ''}
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private selectTab(tabArg: interfaces.ITab) {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
tabArg.action();
|
||||
|
||||
// Emit tab-select event
|
||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||
detail: { tab: tabArg },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the indicator position
|
||||
*/
|
||||
private updateTabIndicator() {
|
||||
if (!this.showTabIndicator || this.tabStyle !== 'horizontal' || !this.selectedTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabIndex = this.tabs.indexOf(this.selectedTab);
|
||||
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
|
||||
`.tabs-wrapper .tabsContainer .tab:nth-child(${tabIndex + 1})`
|
||||
);
|
||||
|
||||
if (!selectedTabElement) return;
|
||||
|
||||
const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabsContainer');
|
||||
const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left"));
|
||||
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabIndicator');
|
||||
|
||||
if (tabIndicator) {
|
||||
tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px';
|
||||
tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (this.tabs && this.tabs.length > 0) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('tabs') && this.tabs && this.tabs.length > 0 && !this.selectedTab) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
}
|
||||
|
||||
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
|
||||
this.updateTabIndicator();
|
||||
}
|
||||
}
|
||||
}
|
192
ts_web/elements/dees-appui-view.ts
Normal file
192
ts_web/elements/dees-appui-view.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import './dees-appui-tabs.js';
|
||||
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
|
||||
|
||||
export interface IAppViewTab extends interfaces.ITab {
|
||||
content?: TemplateResult | (() => TemplateResult);
|
||||
}
|
||||
|
||||
export interface IAppView {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
iconName?: string;
|
||||
tabs: IAppViewTab[];
|
||||
menuItems?: interfaces.ISelectionOption[];
|
||||
}
|
||||
|
||||
@customElement('dees-appui-view')
|
||||
export class DeesAppuiView extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-view
|
||||
.viewConfig=${{
|
||||
id: 'demo-view',
|
||||
name: 'Demo View',
|
||||
description: 'A demonstration view',
|
||||
iconName: 'home',
|
||||
tabs: [
|
||||
{
|
||||
key: 'overview',
|
||||
iconName: 'chart-line',
|
||||
action: () => console.log('Overview tab'),
|
||||
content: html`<div style="padding: 20px;">Overview Content</div>`
|
||||
},
|
||||
{
|
||||
key: 'details',
|
||||
iconName: 'file-alt',
|
||||
action: () => console.log('Details tab'),
|
||||
content: html`<div style="padding: 20px;">Details Content</div>`
|
||||
}
|
||||
],
|
||||
menuItems: [
|
||||
{ key: 'General', action: () => console.log('General') },
|
||||
{ key: 'Advanced', action: () => console.log('Advanced') },
|
||||
]
|
||||
}}
|
||||
></dees-appui-view>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@property({ type: Object })
|
||||
public viewConfig: IAppView;
|
||||
|
||||
@state()
|
||||
private selectedTab: IAppViewTab | null = null;
|
||||
|
||||
@state()
|
||||
private tabs: DeesAppuiTabs;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #161616;
|
||||
}
|
||||
|
||||
.view-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
background: #000000;
|
||||
border-bottom: 1px solid #333;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.view-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
dees-appui-tabs {
|
||||
height: 60px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.viewConfig) {
|
||||
return html`<div>No view configuration provided</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="view-container">
|
||||
<div class="view-header">
|
||||
<dees-appui-tabs
|
||||
.tabs=${this.viewConfig.tabs}
|
||||
.selectedTab=${this.selectedTab}
|
||||
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
|
||||
></dees-appui-tabs>
|
||||
</div>
|
||||
<div class="view-content">
|
||||
${this.viewConfig.tabs.map((tab) => {
|
||||
const isActive = tab === this.selectedTab;
|
||||
const content = typeof tab.content === 'function' ? tab.content() : tab.content;
|
||||
return html`
|
||||
<div class="tab-content ${isActive ? 'active' : ''}">
|
||||
${content || html`<slot name="${tab.key}"></slot>`}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
this.tabs = this.shadowRoot.querySelector('dees-appui-tabs');
|
||||
|
||||
if (this.viewConfig?.tabs?.length > 0) {
|
||||
this.selectedTab = this.viewConfig.tabs[0];
|
||||
}
|
||||
}
|
||||
|
||||
private handleTabSelect(e: CustomEvent) {
|
||||
this.selectedTab = e.detail.tab;
|
||||
|
||||
// Re-emit the event with view context
|
||||
this.dispatchEvent(new CustomEvent('view-tab-select', {
|
||||
detail: {
|
||||
view: this.viewConfig,
|
||||
tab: e.detail.tab
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
// Public methods for external control
|
||||
public selectTab(tabKey: string) {
|
||||
const tab = this.viewConfig.tabs.find(t => t.key === tabKey);
|
||||
if (tab) {
|
||||
this.selectedTab = tab;
|
||||
if (this.tabs) {
|
||||
this.tabs.selectedTab = tab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getMenuItems(): interfaces.ISelectionOption[] {
|
||||
return this.viewConfig?.menuItems || [];
|
||||
}
|
||||
|
||||
public getTabs(): IAppViewTab[] {
|
||||
return this.viewConfig?.tabs || [];
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@ export * from './dees-appui-base.js';
|
||||
export * from './dees-appui-maincontent.js';
|
||||
export * from './dees-appui-mainmenu.js';
|
||||
export * from './dees-appui-mainselector.js';
|
||||
export * from './dees-appui-tabs.js';
|
||||
export * from './dees-appui-view.js';
|
||||
export * from './dees-badge.js';
|
||||
export * from './dees-button-exit.js';
|
||||
export * from './dees-button-group.js';
|
||||
|
Reference in New Issue
Block a user