From 85424d07cd7a31bf295d7e54b71805a0627f8ea7 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 8 Dec 2025 15:40:12 +0000 Subject: [PATCH] feat(dees-appui): Add collapsible/compact mode to AppUI sidebars (mainmenu & secondarymenu) with toggles, tooltips and improved z-index stacking --- changelog.md | 10 ++ ts_web/00_commitinfo_data.ts | 2 +- .../dees-appui-base/dees-appui-base.ts | 53 +++++- .../dees-appui-mainmenu.ts | 155 ++++++++++++++++- .../dees-appui-secondarymenu.ts | 161 +++++++++++++++++- 5 files changed, 372 insertions(+), 9 deletions(-) diff --git a/changelog.md b/changelog.md index 7cbecb4..28b51f3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2025-12-08 - 3.1.0 - feat(dees-appui) +Add collapsible/compact mode to AppUI sidebars (mainmenu & secondarymenu) with toggles, tooltips and improved z-index stacking + +- Add collapsed property to dees-appui-mainmenu and dees-appui-secondarymenu (reflect: true) to enable compact horizontal mode. +- Add floating collapse toggle buttons and public toggleCollapse() methods on mainmenu and secondarymenu; these dispatch 'collapse-change' events (bubbles & composed). +- Expose and track collapse state in dees-appui-base via mainmenuCollapsed and secondarymenuCollapsed properties; bind states to child components and re-emit collapse-change events as mainmenu-collapse-change and secondarymenu-collapse-change. +- Implement collapsed styles and animations: reduced sidebar widths, hide/compact labels and headers, center icons, hide badges, and add smooth width/opacity transitions. +- Add tooltips that appear for tabs/items when sidebars are collapsed to preserve discoverability. +- Adjust layout grid in DeesAppuiBase (use auto columns) and add explicit z-index layering to ensure proper stacking order of mainmenu, secondarymenu, maincontent and activitylog. + ## 2025-12-08 - 3.0.1 - fix(dees-appui) Normalize header heights and box-sizing for App UI components diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 905a8a2..1aeb438 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.0.1', + version: '3.1.0', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-appui/dees-appui-base/dees-appui-base.ts b/ts_web/elements/00group-appui/dees-appui-base/dees-appui-base.ts index ccd0f6e..a2a4d33 100644 --- a/ts_web/elements/00group-appui/dees-appui-base/dees-appui-base.ts +++ b/ts_web/elements/00group-appui/dees-appui-base/dees-appui-base.ts @@ -89,6 +89,13 @@ export class DeesAppuiBase extends DeesElement { @property({ type: Array }) accessor secondarymenuOptions: (interfaces.ISelectionOption | { divider: true })[] = []; + // Collapse states + @property({ type: Boolean }) + accessor mainmenuCollapsed: boolean = false; + + @property({ type: Boolean }) + accessor secondarymenuCollapsed: boolean = false; + // Properties for maincontent @property({ type: Array }) accessor maincontentTabs: interfaces.ITab[] = []; @@ -124,9 +131,30 @@ export class DeesAppuiBase extends DeesElement { height: calc(100% - 40px); width: 100%; display: grid; - grid-template-columns: 200px 240px 1fr 240px; + grid-template-columns: auto auto 1fr 240px; grid-template-rows: 1fr; } + + /* Z-index layering for proper stacking (position: relative required for z-index to work) */ + .maingrid > dees-appui-mainmenu { + position: relative; + z-index: 3; + } + + .maingrid > dees-appui-secondarymenu { + position: relative; + z-index: 2; + } + + .maingrid > dees-appui-maincontent { + position: relative; + z-index: 1; + } + + .maingrid > dees-appui-activitylog { + position: relative; + z-index: 1; + } `, ]; @@ -156,14 +184,18 @@ export class DeesAppuiBase extends DeesElement { .bottomTabs=${this.mainmenuBottomTabs} .tabs=${this.mainmenuTabs} .selectedTab=${this.mainmenuSelectedTab} + .collapsed=${this.mainmenuCollapsed} @tab-select=${(e: CustomEvent) => this.handleMainmenuTabSelect(e)} + @collapse-change=${(e: CustomEvent) => this.handleMainmenuCollapseChange(e)} > this.handleSecondarymenuItemSelect(e)} + @collapse-change=${(e: CustomEvent) => this.handleSecondarymenuCollapseChange(e)} > + +
{ DeesContextmenu.openContextMenuWithOptions(eventArg, [{ name: 'app settings', @@ -270,7 +407,7 @@ export class DeesAppuiMainmenu extends DeesElement { }}> ${this.logoIcon || this.logoText ? html`
- ${this.logoIcon ? html`` : ''} + ${this.logoIcon ? html`` : ''} ${this.logoText ? html`${this.logoText}` : ''}
` : ''} @@ -321,6 +458,7 @@ export class DeesAppuiMainmenu extends DeesElement { > ${tabArg.key} + ${tabArg.key}
`; } @@ -351,4 +489,13 @@ export class DeesAppuiMainmenu extends DeesElement { 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 + })); + } } diff --git a/ts_web/elements/00group-appui/dees-appui-secondarymenu/dees-appui-secondarymenu.ts b/ts_web/elements/00group-appui/dees-appui-secondarymenu/dees-appui-secondarymenu.ts index db3d00d..20f2f28 100644 --- a/ts_web/elements/00group-appui/dees-appui-secondarymenu/dees-appui-secondarymenu.ts +++ b/ts_web/elements/00group-appui/dees-appui-secondarymenu/dees-appui-secondarymenu.ts @@ -89,11 +89,16 @@ export class DeesAppuiSecondarymenu extends DeesElement { @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: 240px; + --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')}; @@ -102,6 +107,8 @@ export class DeesAppuiSecondarymenu extends DeesElement { --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')}; @@ -115,11 +122,16 @@ export class DeesAppuiSecondarymenu extends DeesElement { display: block; height: 100%; - width: var(--sidebar-width); + 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 { @@ -127,6 +139,43 @@ export class DeesAppuiSecondarymenu extends DeesElement { 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 */ @@ -142,12 +191,25 @@ export class DeesAppuiSecondarymenu extends DeesElement { } .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 */ @@ -181,6 +243,10 @@ export class DeesAppuiSecondarymenu extends DeesElement { margin-bottom: 4px; } + :host([collapsed]) .menuGroup { + padding: 0 4px; + } + .groupHeader { display: flex; align-items: center; @@ -188,7 +254,8 @@ export class DeesAppuiSecondarymenu extends DeesElement { padding: 8px 8px; cursor: pointer; border-radius: 6px; - transition: background 0.15s ease; + transition: background 0.15s ease, opacity 0.2s ease, max-height 0.25s ease; + max-height: 40px; } .groupHeader:hover { @@ -204,6 +271,8 @@ export class DeesAppuiSecondarymenu extends DeesElement { color: var(--sidebar-fg-muted); text-transform: uppercase; letter-spacing: 0.5px; + white-space: nowrap; + overflow: hidden; } .groupHeader .groupTitle dees-icon { @@ -221,6 +290,15 @@ export class DeesAppuiSecondarymenu extends DeesElement { 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; @@ -234,6 +312,12 @@ export class DeesAppuiSecondarymenu extends DeesElement { 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; @@ -292,6 +376,60 @@ export class DeesAppuiSecondarymenu extends DeesElement { 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 */ @@ -328,6 +466,10 @@ export class DeesAppuiSecondarymenu extends DeesElement { color: var(--badge-error-fg); } + :host([collapsed]) .badge { + display: none; + } + /* Divider */ .divider { height: 1px; @@ -344,6 +486,9 @@ export class DeesAppuiSecondarymenu extends DeesElement { public render(): TemplateResult { return html` +
${this.heading} @@ -392,6 +537,7 @@ export class DeesAppuiSecondarymenu extends DeesElement { ${item.badge !== undefined ? html` ${item.badge} ` : ''} + ${item.key}
`; } @@ -424,6 +570,15 @@ export class DeesAppuiSecondarymenu extends DeesElement { 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();