Files
dees-catalog/ts_web/elements/00group-appui/dees-appui-mainmenu/dees-appui-mainmenu.ts

502 lines
13 KiB
TypeScript
Raw Normal View History

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';
/**
* the most left menu
* usually used as organization selector
*/
@customElement('dees-appui-mainmenu')
export class DeesAppuiMainmenu extends DeesElement {
public static demo = () => html`
<style>
.demo-mainmenu-container {
height: 500px;
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
}
</style>
<div class="demo-mainmenu-container">
<dees-appui-mainmenu
.logoIcon=${'lucide:box'}
.logoText=${'Acme App'}
.menuGroups=${[
{
tabs: [
{ key: 'Dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard') },
{ key: 'Inbox', iconName: 'lucide:inbox', action: () => console.log('Inbox') },
]
},
{
name: 'Workspace',
tabs: [
{ key: 'Projects', iconName: 'lucide:folder', action: () => console.log('Projects') },
{ key: 'Tasks', iconName: 'lucide:checkSquare', action: () => console.log('Tasks') },
{ key: 'Documents', iconName: 'lucide:fileText', action: () => console.log('Documents') },
]
},
{
name: 'Analytics',
tabs: [
{ key: 'Reports', iconName: 'lucide:barChart3', action: () => console.log('Reports') },
{ key: 'Insights', iconName: 'lucide:lightbulb', action: () => console.log('Insights') },
]
}
]}
.bottomTabs=${[
{ key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') },
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help') },
]}
></dees-appui-mainmenu>
</div>
`;
// 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;
@property({ type: Boolean, reflect: true })
accessor collapsed: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
--menu-width-expanded: 200px;
--menu-width-collapsed: 56px;
--tooltip-bg: ${cssManager.bdTheme('#18181b', '#fafafa')};
--tooltip-fg: ${cssManager.bdTheme('#fafafa', '#18181b')};
display: block;
height: 100%;
}
.mainContainer {
color: ${cssManager.bdTheme('#666', '#ccc')};
z-index: ${zIndexLayers.fixed.appBar};
display: flex;
flex-direction: column;
position: relative;
width: var(--menu-width-expanded);
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;
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;
}
/* Logo Section */
.logoSection {
display: flex;
align-items: center;
gap: 10px;
height: 48px;
padding: 0 14px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
flex-shrink: 0;
box-sizing: border-box;
}
.logoSection .logoIcon {
font-size: 22px;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
flex-shrink: 0;
}
.logoSection .logoText {
flex: 1;
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 0.2s ease, width 0.25s ease;
}
:host([collapsed]) .logoSection {
justify-content: center;
padding: 0 8px;
}
:host([collapsed]) .logoSection .logoText {
opacity: 0;
width: 0;
overflow: hidden;
}
/* 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;
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;
}
.groupTabs {
display: flex;
flex-direction: column;
gap: 2px;
}
:host([collapsed]) .menuGroup {
padding: 0 4px;
}
/* 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;
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;
}
/* Bottom Section */
.bottomSection {
flex-shrink: 0;
padding: 8px;
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
display: flex;
flex-direction: column;
gap: 2px;
}
:host([collapsed]) .bottomSection {
padding: 8px 4px;
}
`,
];
public render(): TemplateResult {
// Get all tabs for selection (from groups or legacy tabs)
const allTabs = this.getAllTabs();
return html`
<button class="collapse-toggle" @click="${() => this.toggleCollapse()}">
<dees-icon .icon="${this.collapsed ? 'lucide:chevronRight' : 'lucide:chevronLeft'}"></dees-icon>
</button>
<div class="mainContainer" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [{
name: 'app settings',
action: async () => {},
iconName: 'gear',
}])
}}>
${this.logoIcon || this.logoText ? html`
<div class="logoSection">
${this.logoIcon ? html`<dees-icon class="logoIcon" .icon="${this.logoIcon}"></dees-icon>` : ''}
${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>
`;
}
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>
<span class="tab-tooltip">${tabArg.key}</span>
</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]);
}
}
public toggleCollapse(): void {
this.collapsed = !this.collapsed;
this.dispatchEvent(new CustomEvent('collapse-change', {
detail: { collapsed: this.collapsed },
bubbles: true,
composed: true
}));
}
}