487 lines
14 KiB
TypeScript
487 lines
14 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';
|
|
|
|
/**
|
|
* 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 = () => html`
|
|
<style>
|
|
.demo-container {
|
|
height: 500px;
|
|
display: flex;
|
|
background: #1a1a1a;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|
|
<div class="demo-container">
|
|
<dees-appui-secondarymenu
|
|
.heading=${'Projects'}
|
|
.groups=${[
|
|
{
|
|
name: 'Active',
|
|
iconName: 'lucide:folder',
|
|
items: [
|
|
{ key: 'Frontend App', iconName: 'code', action: () => console.log('Frontend'), badge: 3, badgeVariant: 'warning' },
|
|
{ key: 'API Server', iconName: 'server', action: () => console.log('API'), badge: 'new', badgeVariant: 'success' },
|
|
{ key: 'Database', iconName: 'database', action: () => console.log('Database') },
|
|
]
|
|
},
|
|
{
|
|
name: 'Archived',
|
|
iconName: 'lucide:archive',
|
|
collapsed: true,
|
|
items: [
|
|
{ key: 'Legacy System', iconName: 'box', action: () => console.log('Legacy') },
|
|
{ key: 'Old API', iconName: 'server', action: () => console.log('Old API') },
|
|
]
|
|
},
|
|
{
|
|
name: 'Settings',
|
|
iconName: 'lucide:settings',
|
|
items: [
|
|
{ key: 'Configuration', iconName: 'sliders', action: () => console.log('Config') },
|
|
{ key: 'Integrations', iconName: 'plug', action: () => console.log('Integrations'), badge: 5, badgeVariant: 'error' },
|
|
]
|
|
}
|
|
] as interfaces.ISecondaryMenuGroup[]}
|
|
@item-select=${(e: CustomEvent) => console.log('Selected:', e.detail)}
|
|
></dees-appui-secondarymenu>
|
|
</div>
|
|
`;
|
|
|
|
// 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<string> = new Set();
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
--sidebar-width: 240px;
|
|
--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')};
|
|
|
|
/* 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);
|
|
background: var(--sidebar-bg);
|
|
border-right: 1px solid var(--sidebar-border);
|
|
font-family: 'Geist Sans', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
user-select: none;
|
|
}
|
|
|
|
.maincontainer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Header Section */
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 16px;
|
|
border-bottom: 1px solid var(--sidebar-border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.header .heading {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--sidebar-fg-active);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
.groupHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 8px;
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
transition: background 0.15s ease;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Divider */
|
|
.divider {
|
|
height: 1px;
|
|
background: var(--sidebar-border);
|
|
margin: 8px 12px;
|
|
}
|
|
|
|
/* Legacy options container */
|
|
.legacyOptions {
|
|
padding: 0 8px;
|
|
}
|
|
`,
|
|
];
|
|
|
|
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>
|
|
`;
|
|
}
|
|
|
|
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="${group.iconName.startsWith('lucide:') ? group.iconName : `lucide:${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.renderMenuItem(item, group))}
|
|
</div>
|
|
</div>
|
|
`)}
|
|
`;
|
|
}
|
|
|
|
private renderMenuItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): TemplateResult {
|
|
const isSelected = this.selectedItem?.key === item.key;
|
|
return html`
|
|
<div
|
|
class="menuItem ${isSelected ? 'selected' : ''}"
|
|
@click="${() => this.selectItem(item, group)}"
|
|
@contextmenu="${(e: MouseEvent) => this.handleContextMenu(e, item)}"
|
|
>
|
|
${item.iconName ? html`<dees-icon .icon="${item.iconName.startsWith('lucide:') ? item.iconName : `lucide:${item.iconName}`}"></dees-icon>` : ''}
|
|
<span class="itemLabel">${item.key}</span>
|
|
${item.badge !== undefined ? html`
|
|
<span class="badge ${item.badgeVariant || 'default'}">${item.badge}</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.ISelectionOption;
|
|
return this.renderMenuItem({
|
|
key: item.key,
|
|
iconName: item.iconName,
|
|
action: item.action,
|
|
});
|
|
})}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 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<string | number | symbol, unknown>) {
|
|
await super.firstUpdated(_changedProperties);
|
|
|
|
// Initialize collapsed state from group defaults
|
|
if (this.groups.length > 0) {
|
|
const initialCollapsed = new Set<string>();
|
|
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;
|
|
}
|
|
}
|