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:
Juergen Kunz
2025-06-17 08:41:36 +00:00
parent e33f4e7a70
commit 5b4319432c
11 changed files with 1669 additions and 141 deletions

View File

@ -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>

View 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>
`;
};

View File

@ -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
}));
}
}

View File

@ -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;
}
}
}

View File

@ -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() {

View File

@ -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]);
}
}
}

View 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();
}
}
}

View 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 || [];
}
}

View File

@ -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';