import * as plugins from '../../00plugins.js'; import * as interfaces from '../../interfaces/index.js'; import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import '../../dees-icon/dees-icon.js'; import { DeesElement, type TemplateResult, property, state, customElement, html, css, cssManager, } from '@design.estate/dees-element'; import { demoFunc } from './dees-appui-secondarymenu.demo.js'; /** * Secondary navigation menu for sub-navigation within MainMenu views * Supports collapsible groups, badges, and dynamic headings */ @customElement('dees-appui-secondarymenu') export class DeesAppuiSecondarymenu extends DeesElement { public static demo = demoFunc; // INSTANCE /** Dynamic heading - typically shows the selected MainMenu item */ @property({ type: String }) accessor heading: string = 'Menu'; /** Grouped items with collapse support */ @property({ type: Array }) accessor groups: interfaces.ISecondaryMenuGroup[] = []; /** Legacy flat list support for backward compatibility */ @property({ type: Array }) accessor selectionOptions: (interfaces.ISelectionOption | { divider: true })[] = []; /** Currently selected item */ @property({ type: Object }) accessor selectedItem: interfaces.ISecondaryMenuItem | null = null; /** Internal state for collapsed groups */ @state() accessor collapsedGroups: Set = new Set(); /** Horizontal collapse state */ @property({ type: Boolean, reflect: true }) accessor collapsed: boolean = false; public static styles = [ cssManager.defaultStyles, css` :host { --sidebar-width-expanded: 240px; --sidebar-width-collapsed: 56px; --sidebar-bg: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; --sidebar-fg: ${cssManager.bdTheme('#525252', '#a3a3a3')}; --sidebar-fg-muted: ${cssManager.bdTheme('#737373', '#737373')}; --sidebar-fg-active: ${cssManager.bdTheme('#0a0a0a', '#fafafa')}; --sidebar-border: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')}; --sidebar-hover: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')}; --sidebar-active: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')}; --sidebar-accent: ${cssManager.bdTheme('#0a0a0a', '#fafafa')}; --tooltip-bg: ${cssManager.bdTheme('#18181b', '#fafafa')}; --tooltip-fg: ${cssManager.bdTheme('#fafafa', '#18181b')}; /* Badge colors */ --badge-default-bg: ${cssManager.bdTheme('#f4f4f5', '#27272a')}; --badge-default-fg: ${cssManager.bdTheme('#3f3f46', '#a1a1aa')}; --badge-success-bg: ${cssManager.bdTheme('#dcfce7', '#14532d')}; --badge-success-fg: ${cssManager.bdTheme('#166534', '#4ade80')}; --badge-warning-bg: ${cssManager.bdTheme('#fef3c7', '#451a03')}; --badge-warning-fg: ${cssManager.bdTheme('#92400e', '#fbbf24')}; --badge-error-bg: ${cssManager.bdTheme('#fee2e2', '#450a0a')}; --badge-error-fg: ${cssManager.bdTheme('#991b1b', '#f87171')}; display: block; height: 100%; width: var(--sidebar-width-expanded); background: var(--sidebar-bg); border-right: 1px solid var(--sidebar-border); font-family: 'Geist Sans', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; user-select: none; transition: width 0.25s ease; } :host([collapsed]) { width: var(--sidebar-width-collapsed); } .maincontainer { display: flex; flex-direction: column; height: 100%; overflow: hidden; position: relative; } /* 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; } /* Header Section */ .header { display: flex; align-items: center; justify-content: space-between; height: 48px; padding: 0 16px; border-bottom: 1px solid var(--sidebar-border); flex-shrink: 0; box-sizing: border-box; } .header .heading { flex: 1; font-size: 14px; font-weight: 600; color: var(--sidebar-fg-active); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity 0.2s ease, width 0.25s ease; } :host([collapsed]) .header { justify-content: center; padding: 0 8px; } :host([collapsed]) .header .heading { opacity: 0; width: 0; overflow: hidden; } /* Scrollable Menu Section */ .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: 4px; } :host([collapsed]) .menuGroup { padding: 0 4px; } .groupHeader { display: flex; align-items: center; justify-content: space-between; padding: 8px 8px; cursor: pointer; border-radius: 6px; transition: background 0.15s ease, opacity 0.2s ease, max-height 0.25s ease; max-height: 40px; } .groupHeader:hover { background: var(--sidebar-hover); } .groupHeader .groupTitle { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 600; color: var(--sidebar-fg-muted); text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; overflow: hidden; } .groupHeader .groupTitle dees-icon { font-size: 14px; opacity: 0.7; } .groupHeader .chevron { font-size: 12px; transition: transform 0.2s ease; color: var(--sidebar-fg-muted); } .groupHeader.collapsed .chevron { transform: rotate(-90deg); } /* Hide group headers when horizontally collapsed */ :host([collapsed]) .groupHeader { opacity: 0; max-height: 0; padding: 0; margin: 0; pointer-events: none; } /* Group Items Container */ .groupItems { overflow: hidden; transition: max-height 0.25s ease, opacity 0.2s ease; max-height: 500px; opacity: 1; } .groupItems.collapsed { max-height: 0; opacity: 0; } /* Always show items when horizontally collapsed (regardless of group collapse state) */ :host([collapsed]) .groupItems { max-height: none; opacity: 1; } /* Menu Item */ .menuItem { position: relative; display: flex; align-items: center; gap: 10px; padding: 8px 12px; margin: 2px 0; font-size: 13px; font-weight: 450; border-radius: 6px; cursor: pointer; transition: all 0.15s ease; color: var(--sidebar-fg); } .menuItem:hover { background: var(--sidebar-hover); color: var(--sidebar-fg-active); } .menuItem:active { background: var(--sidebar-active); } .menuItem.selected { background: var(--sidebar-active); color: var(--sidebar-fg-active); font-weight: 500; } .menuItem.selected::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 16px; background: var(--sidebar-accent); border-radius: 0 2px 2px 0; } .menuItem dees-icon { font-size: 16px; opacity: 0.7; flex-shrink: 0; } .menuItem.selected dees-icon { opacity: 1; } .menuItem .itemLabel { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity 0.2s ease, width 0.25s ease; } /* Collapsed menu item styles */ :host([collapsed]) .menuItem { justify-content: center; padding: 8px; gap: 0; } :host([collapsed]) .menuItem .itemLabel { opacity: 0; width: 0; position: absolute; } :host([collapsed]) .menuItem.selected::before { left: -4px; } /* Tooltip for collapsed state */ .item-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); } .item-tooltip::before { content: ''; position: absolute; left: -4px; top: 50%; transform: translateY(-50%); border: 4px solid transparent; border-right-color: var(--tooltip-bg); } :host([collapsed]) .menuItem:hover .item-tooltip { opacity: 1; transition-delay: 1s; } /* Badge Styles */ .badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 6px; font-size: 10px; font-weight: 600; border-radius: 9px; flex-shrink: 0; } .badge.default { background: var(--badge-default-bg); color: var(--badge-default-fg); } .badge.success { background: var(--badge-success-bg); color: var(--badge-success-fg); } .badge.warning { background: var(--badge-warning-bg); color: var(--badge-warning-fg); } .badge.error { background: var(--badge-error-bg); color: var(--badge-error-fg); } :host([collapsed]) .badge { display: none; } /* Divider */ .divider { height: 1px; background: var(--sidebar-border); margin: 8px 12px; } /* Legacy options container */ .legacyOptions { padding: 0 8px; } `, ]; public render(): TemplateResult { return html`
${this.heading}
`; } private renderGroups(): TemplateResult { return html` ${this.groups.map((group) => html` `)} `; } private renderMenuItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): TemplateResult { const isSelected = this.selectedItem?.key === item.key; return html` `; } private renderLegacyOptions(): TemplateResult { return html`
${this.selectionOptions.map((option) => { if ('divider' in option && option.divider) { return html`
`; } const item = option as interfaces.ISelectionOption; return this.renderMenuItem({ key: item.key, iconName: item.iconName, action: item.action, }); })}
`; } private toggleGroup(groupName: string): void { const newCollapsed = new Set(this.collapsedGroups); if (newCollapsed.has(groupName)) { newCollapsed.delete(groupName); } else { newCollapsed.add(groupName); } this.collapsedGroups = newCollapsed; } public toggleCollapse(): void { this.collapsed = !this.collapsed; this.dispatchEvent(new CustomEvent('collapse-change', { detail: { collapsed: this.collapsed }, bubbles: true, composed: true })); } private selectItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): void { this.selectedItem = item; item.action(); this.dispatchEvent(new CustomEvent('item-select', { detail: { item, group }, bubbles: true, composed: true })); } private handleContextMenu(event: MouseEvent, item: interfaces.ISecondaryMenuItem): void { DeesContextmenu.openContextMenuWithOptions(event, [ { name: 'View details', action: async () => {}, iconName: 'lucide:eye', }, { name: 'Edit', action: async () => {}, iconName: 'lucide:pencil', }, ]); } async firstUpdated(_changedProperties: Map) { await super.firstUpdated(_changedProperties); // Initialize collapsed state from group defaults if (this.groups.length > 0) { const initialCollapsed = new Set(); this.groups.forEach(group => { if (group.collapsed) { initialCollapsed.add(group.name); } }); this.collapsedGroups = initialCollapsed; // Auto-select first item if none selected if (!this.selectedItem && this.groups[0]?.items.length > 0) { this.selectItem(this.groups[0].items[0], this.groups[0]); } } else if (this.selectionOptions.length > 0) { // Legacy mode: select first non-divider option const firstOption = this.selectionOptions.find(opt => !('divider' in opt)) as interfaces.ISelectionOption; if (firstOption && !this.selectedItem) { this.selectItem({ key: firstOption.key, iconName: firstOption.iconName, action: firstOption.action, }); } } } } declare global { interface HTMLElementTagNameMap { 'dees-appui-secondarymenu': DeesAppuiSecondarymenu; } }