From 5b4319432ccd2ff76de5ccac388caeabdeb8dcd2 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 17 Jun 2025 08:41:36 +0000 Subject: [PATCH] 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. --- readme.appui-architecture.md | 513 +++++++++++++++++++++ readme.md | 256 +++++++++- ts_web/elements/dees-appui-appbar.ts | 4 +- ts_web/elements/dees-appui-base.demo.ts | 203 ++++++++ ts_web/elements/dees-appui-base.ts | 172 ++++++- ts_web/elements/dees-appui-maincontent.ts | 146 +++--- ts_web/elements/dees-appui-mainmenu.ts | 34 +- ts_web/elements/dees-appui-mainselector.ts | 41 +- ts_web/elements/dees-appui-tabs.ts | 247 ++++++++++ ts_web/elements/dees-appui-view.ts | 192 ++++++++ ts_web/elements/index.ts | 2 + 11 files changed, 1669 insertions(+), 141 deletions(-) create mode 100644 readme.appui-architecture.md create mode 100644 ts_web/elements/dees-appui-base.demo.ts create mode 100644 ts_web/elements/dees-appui-tabs.ts create mode 100644 ts_web/elements/dees-appui-view.ts diff --git a/readme.appui-architecture.md b/readme.appui-architecture.md new file mode 100644 index 0000000..7f1f7ac --- /dev/null +++ b/readme.appui-architecture.md @@ -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` + + + + `; + } +} +``` + +### 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` + + ` + }, + { + key: 'metrics', + iconName: 'tachometer-alt', + action: () => console.log('Metrics selected'), + content: () => html` + + ` + }, + { + key: 'alerts', + iconName: 'bell', + action: () => console.log('Alerts selected'), + content: () => html` + + ` + } + ], + 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 = new Map(); + private activeView: IAppView | null = null; + private viewCache: Map = 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 = new Set(); + + async loadView(viewId: string): Promise { + 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 = 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` +
+ ${tab.content()} +
+ ` + })) + }; +}; + +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, + componentName: string +) => { + let loaded = false; + + return () => { + if (!loaded) { + importFn().then(() => { + loaded = true; + }); + return html``; + } + + return html`<${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; + onDeactivate?: () => Promise; + 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 \ No newline at end of file diff --git a/readme.md b/readme.md index c5d2d64..66d3452 100644 --- a/readme.md +++ b/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 - - - - - + {}, + 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)} +> +
+ +
``` +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 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()} > ``` +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; // 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. diff --git a/ts_web/elements/dees-appui-appbar.ts b/ts_web/elements/dees-appui-appbar.ts index 1846d24..64aae91 100644 --- a/ts_web/elements/dees-appui-appbar.ts +++ b/ts_web/elements/dees-appui-appbar.ts @@ -361,7 +361,7 @@ export class DeesAppuiBar extends DeesElement { aria-haspopup="${hasSubmenu}" aria-expanded="${isActive}" > - ${menuItem.iconName ? html`` : ''} + ${menuItem.iconName ? html`` : ''} ${menuItem.name} ${hasSubmenu ? this.renderDropdown(menuItem.submenu, itemId, isActive) : ''} @@ -400,7 +400,7 @@ export class DeesAppuiBar extends DeesElement { role="menuitem" tabindex="${menuItem.disabled ? -1 : 0}" > - ${menuItem.iconName ? html`` : ''} + ${menuItem.iconName ? html`` : ''} ${menuItem.name} ${menuItem.shortcut ? html`${menuItem.shortcut}` : ''} diff --git a/ts_web/elements/dees-appui-base.demo.ts b/ts_web/elements/dees-appui-base.demo.ts new file mode 100644 index 0000000..9a35582 --- /dev/null +++ b/ts_web/elements/dees-appui-base.demo.ts @@ -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` + { + 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); + }}> + + +
+ 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)} + > +
+

Application Content

+

This is the main content area where your application's primary interface would be displayed.

+

The layout includes:

+
    +
  • App bar with menus, breadcrumbs, and user account
  • +
  • Main menu (left sidebar) for primary navigation
  • +
  • Selector menu (second sidebar) for sub-navigation
  • +
  • Main content area (this section)
  • +
  • Activity log (right sidebar)
  • +
+
+
+ +
+
+ + + Dark + Light + +
+
+
+
+ `; +}; \ No newline at end of file diff --git a/ts_web/elements/dees-appui-base.ts b/ts_web/elements/dees-appui-base.ts index deff60b..418e16f 100644 --- a/ts_web/elements/dees-appui-base.ts +++ b/ts_web/elements/dees-appui-base.ts @@ -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``; + 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` - + this.handleAppbarMenuSelect(e)} + @breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)} + @search-click=${() => this.handleAppbarSearchClick()} + @user-menu-open=${() => this.handleAppbarUserMenuOpen()} + >
- - - + this.handleMainmenuTabSelect(e)} + > + this.handleMainselectorOptionSelect(e)} + > + + +
`; } + + async firstUpdated() { + // Get references to child components + this.appbar = this.shadowRoot.querySelector('dees-appui-appbar'); + this.mainmenu = this.shadowRoot.querySelector('dees-appui-mainmenu'); + this.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 + })); + } } diff --git a/ts_web/elements/dees-appui-maincontent.ts b/ts_web/elements/dees-appui-maincontent.ts index 7ffda85..78f3be0 100644 --- a/ts_web/elements/dees-appui-maincontent.ts +++ b/ts_web/elements/dees-appui-maincontent.ts @@ -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``; + public static demo = () => html` + console.log('Overview') }, + { key: 'Details', iconName: 'file', action: () => console.log('Details') }, + { key: 'Settings', iconName: 'cog', action: () => console.log('Settings') }, + ]} + > +
+

Main Content Area

+

This is where your application content goes.

+
+
+ `; // 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` -
-
- ${this.tabs.map((tabArg) => { - return html` -
- ${tabArg.key} -
- `; - })} -
-
+ this.handleTabSelect(e)} + > +
+
+ +
`; } - /** - * 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) { + 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; + } } } diff --git a/ts_web/elements/dees-appui-mainmenu.ts b/ts_web/elements/dees-appui-mainmenu.ts index e32ac77..6a424fa 100644 --- a/ts_web/elements/dees-appui-mainmenu.ts +++ b/ts_web/elements/dees-appui-mainmenu.ts @@ -18,17 +18,23 @@ import { DeesContextmenu } from './dees-contextmenu.js'; */ @customElement('dees-appui-mainmenu') export class DeesAppuiMainmenu extends DeesElement { - public static demo = () => html``; + public static demo = () => html` + 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') }, + ]} + > + `; // 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); }}" > - + `; })} @@ -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() { diff --git a/ts_web/elements/dees-appui-mainselector.ts b/ts_web/elements/dees-appui-mainselector.ts index 2e7f10c..782eaf3 100644 --- a/ts_web/elements/dees-appui-mainselector.ts +++ b/ts_web/elements/dees-appui-mainselector.ts @@ -19,22 +19,22 @@ import { */ @customElement('dees-appui-mainselector') export class DeesAppuiMainselector extends DeesElement { - public static demo = () => html``; + public static demo = () => html` + 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') }, + ]} + > + `; // 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) { + await super.firstUpdated(_changedProperties); + if (this.selectionOptions && this.selectionOptions.length > 0) { + await this.updateComplete; + this.selectOption(this.selectionOptions[0]); + } } } diff --git a/ts_web/elements/dees-appui-tabs.ts b/ts_web/elements/dees-appui-tabs.ts new file mode 100644 index 0000000..f29dd6a --- /dev/null +++ b/ts_web/elements/dees-appui-tabs.ts @@ -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` + console.log('Tab 1 clicked') }, + { key: 'Tab 2', action: () => console.log('Tab 2 clicked') }, + { key: 'Tab 3', action: () => console.log('Tab 3 clicked') }, + ]} + > + `; + + // 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` + +
+
+ ${this.tabs.map((tabArg) => { + return html` +
+ ${tabArg.key} +
+ `; + })} +
+ ${this.showTabIndicator ? html` +
+ ` : ''} +
+ ` : html` +
+ ${this.tabs.map((tabArg) => { + return html` +
+ ${tabArg.iconName ? html`` : ''} + ${tabArg.key} +
+ `; + })} +
+ `} +
+ +
+ `; + } + + 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) { + 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(); + } + } +} \ No newline at end of file diff --git a/ts_web/elements/dees-appui-view.ts b/ts_web/elements/dees-appui-view.ts new file mode 100644 index 0000000..e215ee7 --- /dev/null +++ b/ts_web/elements/dees-appui-view.ts @@ -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` + console.log('Overview tab'), + content: html`
Overview Content
` + }, + { + key: 'details', + iconName: 'file-alt', + action: () => console.log('Details tab'), + content: html`
Details Content
` + } + ], + menuItems: [ + { key: 'General', action: () => console.log('General') }, + { key: 'Advanced', action: () => console.log('Advanced') }, + ] + }} + >
+ `; + + // 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`
No view configuration provided
`; + } + + return html` +
+
+ this.handleTabSelect(e)} + > +
+
+ ${this.viewConfig.tabs.map((tab) => { + const isActive = tab === this.selectedTab; + const content = typeof tab.content === 'function' ? tab.content() : tab.content; + return html` +
+ ${content || html``} +
+ `; + })} +
+
+ `; + } + + 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 || []; + } +} \ No newline at end of file diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index 7ef7f53..3aa428b 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -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';