2025-12-08 12:04:01 +00:00
|
|
|
import * as plugins from '../../00plugins.js';
|
|
|
|
|
import * as interfaces from '../../interfaces/index.js';
|
|
|
|
|
import { zIndexLayers } from '../../00zindex.js';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
DeesElement,
|
|
|
|
|
type TemplateResult,
|
|
|
|
|
property,
|
|
|
|
|
customElement,
|
|
|
|
|
html,
|
|
|
|
|
css,
|
|
|
|
|
cssManager,
|
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
|
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
2025-12-08 16:16:25 +00:00
|
|
|
import { demoFunc } from './dees-appui-mainmenu.demo.js';
|
2025-12-08 12:04:01 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* the most left menu
|
|
|
|
|
* usually used as organization selector
|
|
|
|
|
*/
|
|
|
|
|
@customElement('dees-appui-mainmenu')
|
|
|
|
|
export class DeesAppuiMainmenu extends DeesElement {
|
2025-12-08 16:16:25 +00:00
|
|
|
public static demo = demoFunc;
|
2025-12-08 12:04:01 +00:00
|
|
|
|
|
|
|
|
// INSTANCE
|
|
|
|
|
|
|
|
|
|
// Logo properties
|
|
|
|
|
@property({ type: String })
|
|
|
|
|
accessor logoIcon: string = '';
|
|
|
|
|
|
|
|
|
|
@property({ type: String })
|
|
|
|
|
accessor logoText: string = '';
|
|
|
|
|
|
|
|
|
|
// Menu groups (new way)
|
|
|
|
|
@property({ type: Array })
|
|
|
|
|
accessor menuGroups: interfaces.IMenuGroup[] = [];
|
|
|
|
|
|
|
|
|
|
// Bottom tabs (pinned to bottom)
|
|
|
|
|
@property({ type: Array })
|
|
|
|
|
accessor bottomTabs: interfaces.ITab[] = [];
|
|
|
|
|
|
|
|
|
|
// Legacy tabs property (for backward compatibility)
|
|
|
|
|
@property({ type: Array })
|
|
|
|
|
accessor tabs: interfaces.ITab[] = [];
|
|
|
|
|
|
|
|
|
|
@property()
|
|
|
|
|
accessor selectedTab: interfaces.ITab;
|
|
|
|
|
|
2025-12-08 15:40:12 +00:00
|
|
|
@property({ type: Boolean, reflect: true })
|
|
|
|
|
accessor collapsed: boolean = false;
|
|
|
|
|
|
2025-12-08 12:04:01 +00:00
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
css`
|
|
|
|
|
:host {
|
2025-12-08 15:40:12 +00:00
|
|
|
--menu-width-expanded: 200px;
|
|
|
|
|
--menu-width-collapsed: 56px;
|
|
|
|
|
--tooltip-bg: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
|
|
|
--tooltip-fg: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
2025-12-08 16:17:52 +00:00
|
|
|
position: relative;
|
2025-12-08 12:04:01 +00:00
|
|
|
display: block;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mainContainer {
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#ccc')};
|
|
|
|
|
z-index: ${zIndexLayers.fixed.appBar};
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
position: relative;
|
2025-12-08 15:40:12 +00:00
|
|
|
width: var(--menu-width-expanded);
|
2025-12-08 12:04:01 +00:00
|
|
|
height: 100%;
|
|
|
|
|
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
|
|
|
|
user-select: none;
|
|
|
|
|
border-right: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
|
|
|
|
font-family: 'Geist Sans', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
2025-12-08 15:40:12 +00:00
|
|
|
transition: width 0.25s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:host([collapsed]) .mainContainer {
|
|
|
|
|
width: var(--menu-width-collapsed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Floating collapse toggle button */
|
|
|
|
|
.collapse-toggle {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: -12px;
|
|
|
|
|
top: 24px;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#3f3f46')};
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: ${cssManager.bdTheme('#737373', '#a1a1aa')};
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.2s ease, background 0.15s ease;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.collapse-toggle:hover {
|
|
|
|
|
background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
|
|
|
|
|
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:host(:hover) .collapse-toggle {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.collapse-toggle dees-icon {
|
|
|
|
|
font-size: 14px;
|
2025-12-08 12:04:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Logo Section */
|
|
|
|
|
.logoSection {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
2025-12-08 14:50:53 +00:00
|
|
|
height: 48px;
|
|
|
|
|
padding: 0 14px;
|
2025-12-08 12:04:01 +00:00
|
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
|
|
|
|
flex-shrink: 0;
|
2025-12-08 14:50:53 +00:00
|
|
|
box-sizing: border-box;
|
2025-12-08 12:04:01 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 15:40:12 +00:00
|
|
|
.logoSection .logoIcon {
|
2025-12-08 12:04:01 +00:00
|
|
|
font-size: 22px;
|
|
|
|
|
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
2025-12-08 15:40:12 +00:00
|
|
|
flex-shrink: 0;
|
2025-12-08 12:04:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.logoSection .logoText {
|
2025-12-08 15:40:12 +00:00
|
|
|
flex: 1;
|
2025-12-08 12:04:01 +00:00
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
2025-12-08 15:40:12 +00:00
|
|
|
transition: opacity 0.2s ease, width 0.25s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:host([collapsed]) .logoSection {
|
|
|
|
|
justify-content: center;
|
2025-12-08 16:16:25 +00:00
|
|
|
padding: 0;
|
|
|
|
|
gap: 0;
|
2025-12-08 15:40:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:host([collapsed]) .logoSection .logoText {
|
2025-12-08 16:16:25 +00:00
|
|
|
display: none;
|
2025-12-08 12:04:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Middle Section (scrollable) */
|
|
|
|
|
.menuSection {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menuSection::-webkit-scrollbar {
|
|
|
|
|
width: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menuSection::-webkit-scrollbar-track {
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menuSection::-webkit-scrollbar-thumb {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.15)', 'rgba(255, 255, 255, 0.15)')};
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menuSection::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.25)', 'rgba(255, 255, 255, 0.25)')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Menu Group */
|
|
|
|
|
.menuGroup {
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menuGroup:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.groupHeader {
|
|
|
|
|
padding: 8px 12px 6px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: ${cssManager.bdTheme('#737373', '#737373')};
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
2025-12-08 15:40:12 +00:00
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
transition: opacity 0.2s ease, max-height 0.25s ease;
|
|
|
|
|
max-height: 30px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:host([collapsed]) .groupHeader {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
max-height: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
2025-12-08 12:04:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.groupTabs {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 15:40:12 +00:00
|
|
|
:host([collapsed]) .menuGroup {
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 12:04:01 +00:00
|
|
|
/* Tab Item */
|
|
|
|
|
.tab {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
color: ${cssManager.bdTheme('#525252', '#a3a3a3')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab:hover {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')};
|
|
|
|
|
color: ${cssManager.bdTheme('#262626', '#e5e5e5')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab:active {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab.selectedTab {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
|
|
|
|
|
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab.selectedTab::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
width: 3px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
|
|
|
|
border-radius: 0 2px 2px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab dees-icon {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab.selectedTab dees-icon {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab .tabLabel {
|
|
|
|
|
flex: 1;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
2025-12-08 15:40:12 +00:00
|
|
|
transition: opacity 0.2s ease, width 0.25s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Collapsed tab styles */
|
|
|
|
|
:host([collapsed]) .tab {
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
gap: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:host([collapsed]) .tab .tabLabel {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
width: 0;
|
|
|
|
|
position: absolute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:host([collapsed]) .tab.selectedTab::before {
|
|
|
|
|
left: -4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Tooltip for collapsed state */
|
|
|
|
|
.tab-tooltip {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 100%;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
margin-left: 12px;
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
background: var(--tooltip-bg);
|
|
|
|
|
color: var(--tooltip-fg);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
transition: opacity 0.15s ease;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-tooltip::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -4px;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
border: 4px solid transparent;
|
|
|
|
|
border-right-color: var(--tooltip-bg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:host([collapsed]) .tab:hover .tab-tooltip {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transition-delay: 1s;
|
2025-12-08 12:04:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Bottom Section */
|
|
|
|
|
.bottomSection {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
}
|
2025-12-08 15:40:12 +00:00
|
|
|
|
|
|
|
|
:host([collapsed]) .bottomSection {
|
|
|
|
|
padding: 8px 4px;
|
|
|
|
|
}
|
2025-12-08 12:04:01 +00:00
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
|
|
|
|
// Get all tabs for selection (from groups or legacy tabs)
|
|
|
|
|
const allTabs = this.getAllTabs();
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<div class="mainContainer" @contextmenu=${(eventArg: MouseEvent) => {
|
|
|
|
|
DeesContextmenu.openContextMenuWithOptions(eventArg, [{
|
|
|
|
|
name: 'app settings',
|
|
|
|
|
action: async () => {},
|
|
|
|
|
iconName: 'gear',
|
|
|
|
|
}])
|
|
|
|
|
}}>
|
|
|
|
|
${this.logoIcon || this.logoText ? html`
|
|
|
|
|
<div class="logoSection">
|
2025-12-08 15:40:12 +00:00
|
|
|
${this.logoIcon ? html`<dees-icon class="logoIcon" .icon="${this.logoIcon}"></dees-icon>` : ''}
|
2025-12-08 12:04:01 +00:00
|
|
|
${this.logoText ? html`<span class="logoText">${this.logoText}</span>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
<div class="menuSection">
|
|
|
|
|
${this.menuGroups.length > 0 ? this.renderMenuGroups() : this.renderLegacyTabs()}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${this.bottomTabs.length > 0 ? html`
|
|
|
|
|
<div class="bottomSection">
|
|
|
|
|
${this.bottomTabs.map((tabArg) => this.renderTab(tabArg))}
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
2025-12-08 16:16:25 +00:00
|
|
|
<button class="collapse-toggle" @click="${() => this.toggleCollapse()}">
|
|
|
|
|
<dees-icon .icon="${this.collapsed ? 'lucide:chevronRight' : 'lucide:chevronLeft'}"></dees-icon>
|
|
|
|
|
</button>
|
2025-12-08 12:04:01 +00:00
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderMenuGroups(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
${this.menuGroups.map((group) => html`
|
|
|
|
|
<div class="menuGroup">
|
|
|
|
|
${group.name ? html`<div class="groupHeader">${group.name}</div>` : ''}
|
|
|
|
|
<div class="groupTabs">
|
|
|
|
|
${group.tabs.map((tabArg) => this.renderTab(tabArg))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`)}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderLegacyTabs(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="menuGroup">
|
|
|
|
|
<div class="groupTabs">
|
|
|
|
|
${this.tabs.map((tabArg) => this.renderTab(tabArg))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderTab(tabArg: interfaces.ITab): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<div
|
|
|
|
|
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
|
|
|
|
@click="${() => {
|
|
|
|
|
this.updateTab(tabArg);
|
|
|
|
|
}}"
|
|
|
|
|
>
|
|
|
|
|
<dees-icon .icon="${tabArg.iconName || ''}"></dees-icon>
|
|
|
|
|
<span class="tabLabel">${tabArg.key}</span>
|
2025-12-08 15:40:12 +00:00
|
|
|
<span class="tab-tooltip">${tabArg.key}</span>
|
2025-12-08 12:04:01 +00:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getAllTabs(): interfaces.ITab[] {
|
|
|
|
|
if (this.menuGroups.length > 0) {
|
|
|
|
|
const groupTabs = this.menuGroups.flatMap(group => group.tabs);
|
|
|
|
|
return [...groupTabs, ...this.bottomTabs];
|
|
|
|
|
}
|
|
|
|
|
return [...this.tabs, ...this.bottomTabs];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateTab(tabArg: interfaces.ITab) {
|
|
|
|
|
this.selectedTab = tabArg;
|
|
|
|
|
this.selectedTab.action();
|
|
|
|
|
|
|
|
|
|
// Emit tab-select event
|
|
|
|
|
this.dispatchEvent(new CustomEvent('tab-select', {
|
|
|
|
|
detail: { tab: tabArg },
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
firstUpdated() {
|
|
|
|
|
const allTabs = this.getAllTabs();
|
|
|
|
|
if (allTabs.length > 0) {
|
|
|
|
|
this.updateTab(allTabs[0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-08 15:40:12 +00:00
|
|
|
|
|
|
|
|
public toggleCollapse(): void {
|
|
|
|
|
this.collapsed = !this.collapsed;
|
|
|
|
|
this.dispatchEvent(new CustomEvent('collapse-change', {
|
|
|
|
|
detail: { collapsed: this.collapsed },
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
2025-12-08 12:04:01 +00:00
|
|
|
}
|