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'; import { themeDefaultStyles } from '../../00theme.js'; /** * Secondary navigation menu for sub-navigation within MainMenu views * * Supports 8 item types: * 1. Tab - selectable, stays highlighted (default) * 2. Action - executes without selection (blue) * 3. Danger Action - red styling with optional confirmation * 4. Filter - checkbox toggle * 5. Multi-Filter - collapsible box with multiple checkboxes * 6. Divider - visual separator * 7. Header - non-interactive label * 8. Link - opens URL */ @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 - supports new ISecondaryMenuGroup */ @property({ type: Array }) accessor groups: interfaces.ISecondaryMenuGroup[] = []; /** Legacy flat list support for backward compatibility */ @property({ type: Array }) accessor selectionOptions: (interfaces.IMenuItem | { divider: true })[] = []; /** Currently selected tab item */ @property({ type: Object }) accessor selectedItem: interfaces.ISecondaryMenuItemTab | null = null; /** Internal state for collapsed groups */ @state() accessor collapsedGroups: Set = new Set(); /** Internal state for collapsed multi-filters */ @state() accessor collapsedMultiFilters: Set = new Set(); /** Render counter to force re-renders when items are mutated */ @state() private accessor renderCounter: number = 0; /** Horizontal collapse state */ @property({ type: Boolean, reflect: true }) accessor collapsed: boolean = false; public static styles = [ themeDefaultStyles, cssManager.defaultStyles, css` /* TODO: Migrate hardcoded values to --dees-* CSS variables */ :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')}; /* Action colors */ --action-primary: ${cssManager.bdTheme('#2563eb', '#3b82f6')}; --action-primary-hover: ${cssManager.bdTheme('#1d4ed8', '#60a5fa')}; --action-danger: ${cssManager.bdTheme('#dc2626', '#ef4444')}; --action-danger-hover: ${cssManager.bdTheme('#b91c1c', '#f87171')}; position: relative; 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; overscroll-behavior: contain; 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 12px; cursor: pointer; border-radius: 6px; transition: background 0.15s ease, opacity 0.2s ease, max-height 0.25s ease; max-height: 40px; } .groupHeader:hover { border: 1px solid ${cssManager.bdTheme('rgba(140, 120, 100, 0.06)', 'rgba(180, 160, 140, 0.08)')}; padding: 7px 11px; } .groupHeader:not(.collapsed) { background: ${cssManager.bdTheme('rgba(140, 120, 100, 0.06)', 'rgba(180, 160, 140, 0.08)')}; border: none; padding: 8px 12px; } .groupHeader .groupTitle { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 600; color: ${cssManager.bdTheme('#78716c', '#b5a99a')}; text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; overflow: hidden; } .groupHeader .groupTitle dees-icon { font-size: 16px; color: ${cssManager.bdTheme('#78716c', '#b5a99a')}; } .groupHeader .chevron { font-size: 12px; transition: transform 0.2s ease; color: ${cssManager.bdTheme('#78716c', '#b5a99a')}; } .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, margin 0.25s ease; max-height: 1000px; opacity: 1; margin-bottom: 12px; } .groupItems.collapsed { max-height: 0; opacity: 0; margin-bottom: 0; } /* Always show items when horizontally collapsed (regardless of group collapse state) */ :host([collapsed]) .groupItems { max-height: none; opacity: 1; } /* Menu Item Base */ .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.disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; } .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; } /* Action Item Styles */ .menuItem.action-primary { color: var(--action-primary); } .menuItem.action-primary:hover { color: var(--action-primary-hover); background: ${cssManager.bdTheme('rgba(37, 99, 235, 0.08)', 'rgba(59, 130, 246, 0.12)')}; } .menuItem.action-primary dees-icon { opacity: 1; } .menuItem.action-danger { color: var(--action-danger); } .menuItem.action-danger:hover { color: var(--action-danger-hover); background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.08)', 'rgba(239, 68, 68, 0.12)')}; } .menuItem.action-danger dees-icon { opacity: 1; } /* Filter Item Styles */ .menuItem.filter { justify-content: space-between; } .menuItem.filter .filter-checkbox { width: 16px; height: 16px; border: 2px solid ${cssManager.bdTheme('#d4d4d4', '#525252')}; border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; flex-shrink: 0; } .menuItem.filter .filter-checkbox.checked { background: var(--sidebar-accent); border-color: var(--sidebar-accent); } .menuItem.filter .filter-checkbox dees-icon { font-size: 12px; color: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; opacity: 1; } .menuItem.filter.active { color: var(--sidebar-fg-active); } /* Multi-Filter Container */ .multiFilter { margin: 4px 0; border: 1px solid var(--sidebar-border); border-radius: 8px; overflow: hidden; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(255, 255, 255, 0.02)')}; } .multiFilter-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; cursor: pointer; transition: background 0.15s ease; } .multiFilter-header:hover { background: var(--sidebar-hover); } .multiFilter-header .multiFilter-title { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 500; color: var(--sidebar-fg-active); } .multiFilter-header .multiFilter-title dees-icon { font-size: 16px; opacity: 0.7; } .multiFilter-header .multiFilter-count { font-size: 11px; color: var(--sidebar-fg-muted); background: var(--badge-default-bg); padding: 2px 6px; border-radius: 4px; } .multiFilter-header .chevron { font-size: 12px; transition: transform 0.2s ease; color: var(--sidebar-fg-muted); } .multiFilter-header.collapsed .chevron { transform: rotate(-90deg); } .multiFilter-options { border-top: 1px solid var(--sidebar-border); overflow: hidden; transition: max-height 0.25s ease, opacity 0.2s ease; max-height: 500px; opacity: 1; } .multiFilter-options.collapsed { max-height: 0; opacity: 0; border-top: none; } .multiFilter-option { display: flex; align-items: center; gap: 10px; padding: 8px 12px; cursor: pointer; transition: background 0.15s ease; font-size: 13px; color: var(--sidebar-fg); } .multiFilter-option:hover { background: var(--sidebar-hover); color: var(--sidebar-fg-active); } .multiFilter-option .option-checkbox { width: 16px; height: 16px; border: 2px solid ${cssManager.bdTheme('#d4d4d4', '#525252')}; border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; flex-shrink: 0; } .multiFilter-option .option-checkbox.checked { background: var(--sidebar-accent); border-color: var(--sidebar-accent); } .multiFilter-option .option-checkbox dees-icon { font-size: 12px; color: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; } .multiFilter-option dees-icon.option-icon { font-size: 14px; opacity: 0.7; } /* Divider */ .menuDivider { height: 1px; background: var(--sidebar-border); margin: 8px 12px; } :host([collapsed]) .menuDivider { margin: 8px 4px; } /* Header/Label */ .menuHeader { padding: 12px 12px 4px 12px; font-size: 10px; font-weight: 600; color: var(--sidebar-fg-muted); text-transform: uppercase; letter-spacing: 0.5px; } :host([collapsed]) .menuHeader { display: none; } /* Link Item */ .menuItem.link .external-icon { font-size: 12px; opacity: 0.5; margin-left: auto; } /* 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; } :host([collapsed]) .menuItem .filter-checkbox, :host([collapsed]) .menuItem .external-icon { display: none; } :host([collapsed]) .multiFilter { display: none; } /* 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; } /* Legacy options container */ .legacyOptions { padding: 0 8px; } /* Divider (legacy) */ .divider { height: 1px; background: var(--sidebar-border); margin: 8px 12px; } `, ]; public render(): TemplateResult { return html`
${this.heading}
`; } private renderGroups(): TemplateResult { return html` ${this.groups.map((group) => html` `)} `; } private renderItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): TemplateResult { // Check for hidden items if ('hidden' in item && item.hidden) { return html``; } // Determine item type const itemType = 'type' in item ? item.type : 'tab'; switch (itemType) { case 'action': return this.renderActionItem(item as interfaces.ISecondaryMenuItemAction); case 'filter': return this.renderFilterItem(item as interfaces.ISecondaryMenuItemFilter); case 'multiFilter': return this.renderMultiFilterItem(item as interfaces.ISecondaryMenuItemMultiFilter); case 'divider': return this.renderDivider(); case 'header': return this.renderHeader(item as interfaces.ISecondaryMenuItemHeader); case 'link': return this.renderLinkItem(item as interfaces.ISecondaryMenuItemLink); case 'tab': default: return this.renderTabItem(item as interfaces.ISecondaryMenuItemTab, group); } } private renderTabItem(item: interfaces.ISecondaryMenuItemTab, group?: interfaces.ISecondaryMenuGroup): TemplateResult { const isSelected = this.selectedItem?.key === item.key; const isDisabled = item.disabled === true; return html` `; } private renderActionItem(item: interfaces.ISecondaryMenuItemAction): TemplateResult { const variant = item.variant || 'primary'; const isDisabled = item.disabled === true; return html` `; } private renderFilterItem(item: interfaces.ISecondaryMenuItemFilter): TemplateResult { const isDisabled = item.disabled === true; return html` `; } private renderMultiFilterItem(item: interfaces.ISecondaryMenuItemMultiFilter): TemplateResult { const isCollapsed = this.collapsedMultiFilters.has(item.key); const checkedCount = item.options.filter(opt => opt.checked).length; return html`
${item.iconName ? html`` : ''} ${item.key} ${checkedCount > 0 ? html`${checkedCount}` : ''}
${item.options.map(option => html`
${option.checked ? html`` : ''}
${option.iconName ? html`` : ''} ${option.label}
`)}
`; } private renderDivider(): TemplateResult { return html``; } private renderHeader(item: interfaces.ISecondaryMenuItemHeader): TemplateResult { return html``; } private renderLinkItem(item: interfaces.ISecondaryMenuItemLink): TemplateResult { const isExternal = item.external ?? item.href.startsWith('http'); const isDisabled = item.disabled === true; return html` `; } private renderLegacyOptions(): TemplateResult { return html`
${this.selectionOptions.map((option) => { if ('divider' in option && option.divider) { return html`
`; } const item = option as interfaces.IMenuItem; // Convert legacy IMenuItem to ISecondaryMenuItemTab const tabItem: interfaces.ISecondaryMenuItemTab = { key: item.key, iconName: item.iconName, action: item.action, badge: item.badge, badgeVariant: item.badgeVariant, }; return this.renderTabItem(tabItem); })}
`; } // Helper to normalize icon names private normalizeIcon(iconName: string): string { return iconName.startsWith('lucide:') ? iconName : `lucide:${iconName}`; } 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; } private toggleMultiFilter(filterKey: string): void { const newCollapsed = new Set(this.collapsedMultiFilters); if (newCollapsed.has(filterKey)) { newCollapsed.delete(filterKey); } else { newCollapsed.add(filterKey); } this.collapsedMultiFilters = newCollapsed; } public toggleCollapse(): void { this.collapsed = !this.collapsed; this.dispatchEvent(new CustomEvent('collapse-change', { detail: { collapsed: this.collapsed }, bubbles: true, composed: true })); } private selectTabItem(item: interfaces.ISecondaryMenuItemTab, group?: interfaces.ISecondaryMenuGroup): void { this.selectedItem = item; item.action(); this.dispatchEvent(new CustomEvent('item-select', { detail: { item, group }, bubbles: true, composed: true })); } private async handleActionClick(item: interfaces.ISecondaryMenuItemAction): Promise { // Handle confirmation if required if (item.confirmMessage) { const confirmed = window.confirm(item.confirmMessage); if (!confirmed) { return; } } await item.action(); this.dispatchEvent(new CustomEvent('action-click', { detail: { item }, bubbles: true, composed: true })); } private handleFilterToggle(item: interfaces.ISecondaryMenuItemFilter): void { const newActive = !item.active; // Update the item's active state item.active = newActive; item.onToggle(newActive); // Force re-render by incrementing the render counter this.renderCounter++; this.dispatchEvent(new CustomEvent('filter-toggle', { detail: { item, active: newActive }, bubbles: true, composed: true })); } private handleMultiFilterOptionToggle(item: interfaces.ISecondaryMenuItemMultiFilter, optionKey: string): void { // Update the option's checked state const option = item.options.find(opt => opt.key === optionKey); if (option) { option.checked = !option.checked; } // Calculate the new selected keys const selectedKeys = item.options .filter(opt => opt.checked) .map(opt => opt.key); item.onChange(selectedKeys); // Force re-render by incrementing the render counter this.renderCounter++; this.dispatchEvent(new CustomEvent('multifilter-change', { detail: { item, selectedKeys }, bubbles: true, composed: true })); } private handleLinkClick(item: interfaces.ISecondaryMenuItemLink): void { const isExternal = item.external ?? item.href.startsWith('http'); if (isExternal) { window.open(item.href, '_blank', 'noopener,noreferrer'); } else { window.location.href = item.href; } this.dispatchEvent(new CustomEvent('link-click', { detail: { item }, bubbles: true, composed: true })); } private handleContextMenu(event: MouseEvent, item: interfaces.ISecondaryMenuItemTab): 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(); const initialMultiFilterCollapsed = new Set(); this.groups.forEach(group => { if (group.collapsed) { initialCollapsed.add(group.name); } // Check for collapsed multi-filters group.items.forEach(item => { if ('type' in item && item.type === 'multiFilter') { const multiFilter = item as interfaces.ISecondaryMenuItemMultiFilter; if (multiFilter.collapsed) { initialMultiFilterCollapsed.add(multiFilter.key); } } }); }); this.collapsedGroups = initialCollapsed; this.collapsedMultiFilters = initialMultiFilterCollapsed; // Auto-select first tab item if none selected if (!this.selectedItem) { for (const group of this.groups) { for (const item of group.items) { const itemType = 'type' in item ? item.type : 'tab'; if (itemType === 'tab' || itemType === undefined) { const tabItem = item as interfaces.ISecondaryMenuItemTab; if (!tabItem.disabled) { this.selectTabItem(tabItem, group); return; } } } } } } else if (this.selectionOptions.length > 0) { // Legacy mode: select first non-divider option const firstOption = this.selectionOptions.find(opt => !('divider' in opt)) as interfaces.IMenuItem; if (firstOption && !this.selectedItem) { const tabItem: interfaces.ISecondaryMenuItemTab = { key: firstOption.key, iconName: firstOption.iconName, action: firstOption.action, }; this.selectTabItem(tabItem); } } } } declare global { interface HTMLElementTagNameMap { 'dees-appui-secondarymenu': DeesAppuiSecondarymenu; } }