1088 lines
32 KiB
TypeScript
1088 lines
32 KiB
TypeScript
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<string> = new Set();
|
|
|
|
/** Internal state for collapsed multi-filters */
|
|
@state()
|
|
accessor collapsedMultiFilters: Set<string> = 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;
|
|
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: ${cssManager.bdTheme('rgba(140, 120, 100, 0.06)', 'rgba(180, 160, 140, 0.08)')};
|
|
}
|
|
|
|
.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: 14px;
|
|
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;
|
|
max-height: 1000px;
|
|
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 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`
|
|
<div class="maincontainer">
|
|
<div class="header">
|
|
<span class="heading">${this.heading}</span>
|
|
</div>
|
|
<div class="menuSection">
|
|
${this.groups.length > 0
|
|
? this.renderGroups()
|
|
: this.renderLegacyOptions()}
|
|
</div>
|
|
</div>
|
|
<button class="collapse-toggle" @click="${() => this.toggleCollapse()}">
|
|
<dees-icon .icon="${this.collapsed ? 'lucide:chevronRight' : 'lucide:chevronLeft'}"></dees-icon>
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
private renderGroups(): TemplateResult {
|
|
return html`
|
|
${this.groups.map((group) => html`
|
|
<div class="menuGroup">
|
|
<div
|
|
class="groupHeader ${this.collapsedGroups.has(group.name) ? 'collapsed' : ''}"
|
|
@click="${() => this.toggleGroup(group.name)}"
|
|
>
|
|
<span class="groupTitle">
|
|
${group.iconName ? html`<dees-icon .icon="${this.normalizeIcon(group.iconName)}"></dees-icon>` : ''}
|
|
${group.name}
|
|
</span>
|
|
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
|
|
</div>
|
|
<div class="groupItems ${this.collapsedGroups.has(group.name) ? 'collapsed' : ''}">
|
|
${group.items.map((item) => this.renderItem(item, group))}
|
|
</div>
|
|
</div>
|
|
`)}
|
|
`;
|
|
}
|
|
|
|
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`
|
|
<div
|
|
class="menuItem ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}"
|
|
@click="${() => !isDisabled && this.selectTabItem(item, group)}"
|
|
@contextmenu="${(e: MouseEvent) => this.handleContextMenu(e, item)}"
|
|
>
|
|
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
|
<span class="itemLabel">${item.key}</span>
|
|
${item.badge !== undefined ? html`
|
|
<span class="badge ${item.badgeVariant || 'default'}">${item.badge}</span>
|
|
` : ''}
|
|
<span class="item-tooltip">${item.key}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderActionItem(item: interfaces.ISecondaryMenuItemAction): TemplateResult {
|
|
const variant = item.variant || 'primary';
|
|
const isDisabled = item.disabled === true;
|
|
|
|
return html`
|
|
<div
|
|
class="menuItem action-${variant} ${isDisabled ? 'disabled' : ''}"
|
|
@click="${() => !isDisabled && this.handleActionClick(item)}"
|
|
>
|
|
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
|
<span class="itemLabel">${item.key}</span>
|
|
<span class="item-tooltip">${item.key}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderFilterItem(item: interfaces.ISecondaryMenuItemFilter): TemplateResult {
|
|
const isDisabled = item.disabled === true;
|
|
|
|
return html`
|
|
<div
|
|
class="menuItem filter ${item.active ? 'active' : ''} ${isDisabled ? 'disabled' : ''}"
|
|
@click="${() => !isDisabled && this.handleFilterToggle(item)}"
|
|
>
|
|
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
|
<span class="itemLabel">${item.key}</span>
|
|
<div class="filter-checkbox ${item.active ? 'checked' : ''}">
|
|
${item.active ? html`<dees-icon .icon="${'lucide:check'}"></dees-icon>` : ''}
|
|
</div>
|
|
<span class="item-tooltip">${item.key}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderMultiFilterItem(item: interfaces.ISecondaryMenuItemMultiFilter): TemplateResult {
|
|
const isCollapsed = this.collapsedMultiFilters.has(item.key);
|
|
const checkedCount = item.options.filter(opt => opt.checked).length;
|
|
|
|
return html`
|
|
<div class="multiFilter">
|
|
<div
|
|
class="multiFilter-header ${isCollapsed ? 'collapsed' : ''}"
|
|
@click="${() => this.toggleMultiFilter(item.key)}"
|
|
>
|
|
<span class="multiFilter-title">
|
|
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
|
${item.key}
|
|
</span>
|
|
${checkedCount > 0 ? html`<span class="multiFilter-count">${checkedCount}</span>` : ''}
|
|
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
|
|
</div>
|
|
<div class="multiFilter-options ${isCollapsed ? 'collapsed' : ''}">
|
|
${item.options.map(option => html`
|
|
<div
|
|
class="multiFilter-option"
|
|
@click="${() => this.handleMultiFilterOptionToggle(item, option.key)}"
|
|
>
|
|
<div class="option-checkbox ${option.checked ? 'checked' : ''}">
|
|
${option.checked ? html`<dees-icon .icon="${'lucide:check'}"></dees-icon>` : ''}
|
|
</div>
|
|
${option.iconName ? html`<dees-icon class="option-icon" .icon="${this.normalizeIcon(option.iconName)}"></dees-icon>` : ''}
|
|
<span>${option.label}</span>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderDivider(): TemplateResult {
|
|
return html`<div class="menuDivider"></div>`;
|
|
}
|
|
|
|
private renderHeader(item: interfaces.ISecondaryMenuItemHeader): TemplateResult {
|
|
return html`<div class="menuHeader">${item.label}</div>`;
|
|
}
|
|
|
|
private renderLinkItem(item: interfaces.ISecondaryMenuItemLink): TemplateResult {
|
|
const isExternal = item.external ?? item.href.startsWith('http');
|
|
const isDisabled = item.disabled === true;
|
|
|
|
return html`
|
|
<div
|
|
class="menuItem link ${isDisabled ? 'disabled' : ''}"
|
|
@click="${() => !isDisabled && this.handleLinkClick(item)}"
|
|
>
|
|
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
|
<span class="itemLabel">${item.key}</span>
|
|
${isExternal ? html`<dees-icon class="external-icon" .icon="${'lucide:externalLink'}"></dees-icon>` : ''}
|
|
<span class="item-tooltip">${item.key}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderLegacyOptions(): TemplateResult {
|
|
return html`
|
|
<div class="legacyOptions">
|
|
${this.selectionOptions.map((option) => {
|
|
if ('divider' in option && option.divider) {
|
|
return html`<div class="divider"></div>`;
|
|
}
|
|
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);
|
|
})}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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<void> {
|
|
// 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<string | number | symbol, unknown>) {
|
|
await super.firstUpdated(_changedProperties);
|
|
|
|
// Initialize collapsed state from group defaults
|
|
if (this.groups.length > 0) {
|
|
const initialCollapsed = new Set<string>();
|
|
const initialMultiFilterCollapsed = new Set<string>();
|
|
|
|
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;
|
|
}
|
|
}
|