Compare commits

...

18 Commits

Author SHA1 Message Date
540f1c2431 v3.11.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-30 10:27:34 +00:00
af1df1b3d6 feat(dees-appui-tabs): improve horizontal tabs UX with scroll fades, hover scrollbar, and smooth scroll-to-selected 2025-12-30 10:27:34 +00:00
34ed47e535 v3.10.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 23:33:38 +00:00
5f67bcfb71 feat(appui-tabs): add closeable tabs and auto-hide behavior for content tabs, plus API and events to control them 2025-12-29 23:33:38 +00:00
7b39c871f3 v3.9.0
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 22:45:39 +00:00
6f9bebf0f8 feat(dees-appui-mainmenu): add status badges to main menu items with theme-aware styling 2025-12-29 22:45:39 +00:00
e51c906adb v3.8.0
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 12:03:11 +00:00
0626889bef feat(dees-appui-base): add interactive demo controls to manipulate appui via view activation context 2025-12-29 12:03:11 +00:00
3c1456c0c1 v3.7.1
Some checks failed
Default (tags) / security (push) Failing after 13s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 11:44:16 +00:00
cc71f232d2 fix(dees-appui-maincontent): migrate main content layout to CSS Grid and enable topbar collapse transitions 2025-12-29 11:44:16 +00:00
8a4d69694c v3.7.0
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 11:30:49 +00:00
e45810dd06 feat(dees-contextmenu,dees-appui-tabs,test): Prevent double-destruction of context menus, await window layer teardown, update destroyAll behavior, remove tabs content slot, and adjust tests 2025-12-29 11:30:49 +00:00
45a2743312 v3.6.1
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 03:04:39 +00:00
c5b50f3eb0 fix(readme): document new App UI APIs to control main/secondary menu and content tabs visibility 2025-12-29 03:04:39 +00:00
aedd4a041c v3.6.0
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 02:55:03 +00:00
1f37474d3f feat(dees-appui): add visibility toggles for main/secondary menus and ability to show/hide content tabs; expose corresponding setters and appconfig entries 2025-12-29 02:55:03 +00:00
76748a81c9 v3.5.1
Some checks failed
Default (tags) / security (push) Failing after 13s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 02:09:09 +00:00
1e432ca92e fix(dees-appui-view): remove DeesAppuiView component, its demo, documentation snippet, and related exports 2025-12-29 02:09:09 +00:00
18 changed files with 854 additions and 306 deletions

View File

@@ -1,5 +1,80 @@
# Changelog
## 2025-12-30 - 3.11.0 - feat(dees-appui-tabs)
improve horizontal tabs UX with scroll fades, hover scrollbar, and smooth scroll-to-selected
- Add reactive scroll state (canScrollLeft / canScrollRight) and updateScrollState to track horizontal overflow.
- Introduce scroll-fade gradient elements and CSS to indicate overflow on left/right edges.
- Show a thin, styled scrollbar on hover (webkit + Firefox styling) instead of hiding it completely.
- Auto-scroll selected tab into view using scrollTabIntoView and smooth scroll when selecting a tab.
- Set up a ResizeObserver to recompute scroll state on container size changes and clean it up on disconnect.
- Ensure lifecycle hooks call updateScrollState (firstUpdated/updated) so indicators stay in sync after render/fonts ready.
## 2025-12-29 - 3.10.0 - feat(appui-tabs)
add closeable tabs and auto-hide behavior for content tabs, plus API and events to control them
- Add closeable tab support: IMenuItem.closeable & IMenuItem.onClose; dees-appui-tabs renders a close button, invokes onClose, and emits a 'tab-close' event.
- Add auto-hide feature: dees-appui-tabs (autoHide, autoHideThreshold) and corresponding properties in dees-appui-maincontent/dees-appui-base to hide tabs when count ≤ threshold.
- Expose new API: dees-appui-base.setContentTabsAutoHide(enabled, threshold) and update appconfig interface to include setContentTabsAutoHide.
- Re-emit 'tab-close' events from dees-appui-maincontent and dees-appui-base so parent components can react to tab closures.
- Add interactive demos (demo-closeable-tabs, demo-autohide-tabs) demonstrating the new closeable and auto-hide behaviors and controls.
## 2025-12-29 - 3.9.0 - feat(dees-appui-mainmenu)
add status badges to main menu items with theme-aware styling
- Introduce .badge element and layout (min-width, height, padding, font-size, weight, border-radius) to display counts/status on menu items.
- Add four badge variants: default, success, warning, error, using cssManager.bdTheme for light/dark color pairs.
- Render the badge element conditionally in the menu item template when tabArg.badge is provided; hide badges when host has [collapsed] attribute.
## 2025-12-29 - 3.8.0 - feat(dees-appui-base)
add interactive demo controls to manipulate appui via view activation context
- Store IViewActivationContext on the demo element (this.ctx) during onActivate
- Add a new "Context Actions" UI section with buttons that call ctx.appui methods (toggle main/secondary menus, content tabs, collapse/expand main menu, set breadcrumbs, navigate to views, add activity entry, set/clear menu badges)
- Include styles for ctx-actions and button variants (success, danger, hover states)
- Change is limited to the demo file (dees-appui-base.demo.ts) and is non-breaking
## 2025-12-29 - 3.7.1 - fix(dees-appui-maincontent)
migrate main content layout to CSS Grid and enable topbar collapse transitions
- Replace absolute positioning with CSS Grid on :host and .maincontainer to enable natural document flow
- Make .topbar a grid and animate collapse via grid-template-rows; switch :host([notabs]) to grid-template-rows: 0fr instead of display:none to allow transitions
- Set .maincontainer to display:contents and add min-height:0 on content areas and topbar children to fix overflow/scrolling and flex/grid height issues
- Remove positional styles (position:absolute/top/left/right/bottom) so content scrolls correctly and layout is more robust
## 2025-12-29 - 3.7.0 - feat(dees-contextmenu,dees-appui-tabs,test)
Prevent double-destruction of context menus, await window layer teardown, update destroyAll behavior, remove tabs content slot, and adjust tests
- Add isDestroying guard in DeesContextmenu.destroy to avoid double-destruction.
- Await windowLayer.destroy() to ensure the window layer is fully torn down before continuing.
- Ensure submenu timeouts are cleared and submenu.destroy() is awaited during teardown.
- Change destroyAll to locate the root/top-level menu and destroy from the root to cascade teardown reliably.
- Remove the .content wrapper and the <slot> output from dees-appui-tabs (demo updated to render content outside the component) — this is a breaking change to the tabs API (slotted content no longer rendered).
- Increase test timeout in test.contextmenu-nested-close.browser.ts to 600ms to account for ~300ms windowLayer destruction + ~100ms context menu delay.
## 2025-12-29 - 3.6.1 - fix(readme)
document new App UI APIs to control main/secondary menu and content tabs visibility
- Added docs for setMainMenuCollapsed(), setMainMenuVisible(), setSecondaryMenuCollapsed(), setSecondaryMenuVisible(), and setContentTabsVisible() programmatic APIs.
- Included a TypeScript example showing how to hide secondary menu, hide content tabs, and collapse the main menu in a view's onActivate hook.
## 2025-12-29 - 3.6.0 - feat(dees-appui)
add visibility toggles for main/secondary menus and ability to show/hide content tabs; expose corresponding setters and appconfig entries
- ts_web/elements/00group-appui/dees-appui-base: added boolean properties mainmenuVisible, secondarymenuVisible, maincontentTabsVisible; render main and secondary menus conditionally; pass showTabs to dees-appui-maincontent; added setter methods: setMainMenuVisible, setSecondaryMenuCollapsed, setSecondaryMenuVisible, setContentTabsVisible.
- ts_web/elements/00group-appui/dees-appui-maincontent: added showTabs property, support for a notabs attribute via styles, updated() and firstUpdated() to apply notabs state so tabs can be hidden/shown dynamically.
- ts_web/elements/interfaces/appconfig.ts: expanded appconfig interface to include setMainMenuVisible, setSecondaryMenuCollapsed, setSecondaryMenuVisible, setContentTabsVisible so host app can control visibility programmatically.
- No breaking changes: defaults preserve existing behavior (menus and tabs remain visible by default).
## 2025-12-29 - 3.5.1 - fix(dees-appui-view)
remove DeesAppuiView component, its demo, documentation snippet, and related exports
- Deleted component implementation: ts_web/elements/00group-appui/dees-appui-view/dees-appui-view.ts
- Deleted demo file: ts_web/elements/00group-appui/dees-appui-view/dees-appui-view.demo.ts
- Removed index re-export: ts_web/elements/00group-appui/dees-appui-view/index.ts (deleted) and removed export from ts_web/elements/00group-appui/index.ts
- Removed documentation section for DeesAppuiView from readme.md
- Breaking change: any consumers using the <dees-appui-view> component or its public API (selectTab, getMenuItems, getTabs) must be migrated to alternate components/approach
## 2025-12-29 - 3.5.0 - feat(theme,interfaces)
Introduce a global theming system and unify menu/tab interfaces; migrate components to use themeDefaultStyles and update APIs accordingly

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "3.5.0",
"version": "3.11.0",
"private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js",

View File

@@ -54,7 +54,7 @@ For developers working on this library, please refer to the [UI Components Playb
|----------|------------|
| **Core UI** | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols) |
| **Forms** | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesFormSubmit`](#deesformsubmit) |
| **Layout** | [`DeesAppuiBase`](#deesappuibase), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesAppuiView`](#deesappuiview), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) |
| **Layout** | [`DeesAppuiBase`](#deesappuibase), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) |
| **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) |
| **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) |
| **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) |
@@ -666,9 +666,26 @@ class MyApp extends DeesElement {
- `navigateToView(viewId, params?)` - Navigate between views
- `setAppBarMenus()`, `setBreadcrumbs()`, `setUser()` - Control the app bar
- `setMainMenu()`, `setMainMenuSelection()`, `setMainMenuBadge()` - Control main navigation
- `setSecondaryMenu()`, `setContentTabs()` - Control view-specific UI
- `setMainMenuCollapsed()`, `setMainMenuVisible()` - Control main menu visibility
- `setSecondaryMenu()`, `setSecondaryMenuCollapsed()`, `setSecondaryMenuVisible()` - Control secondary menu
- `setContentTabs()`, `setContentTabsVisible()` - Control view-specific UI
- `activityLog.add()`, `activityLog.addMany()`, `activityLog.clear()` - Manage activity entries
**View Visibility Control:**
```typescript
// In your view's onActivate hook
onActivate(context: IViewActivationContext) {
// Hide secondary menu for a fullscreen view
context.appui.setSecondaryMenuVisible(false);
// Hide content tabs
context.appui.setContentTabsVisible(false);
// Collapse main menu to give more space
context.appui.setMainMenuCollapsed(true);
}
```
#### `DeesAppuiMainmenu`
Main navigation menu component for application-wide navigation.
@@ -775,23 +792,6 @@ Professional application bar component with hierarchical menus, breadcrumb navig
- **User Account Section** - Avatar with status indicator
- **Accessibility** - Full ARIA support with menubar roles
#### `DeesAppuiView`
View wrapper component for single-view layouts with internal tabs.
```typescript
<dees-appui-view
.viewConfig=${{
id: 'settings',
name: 'Settings',
tabs: [
{ key: 'General', iconName: 'lucide:settings', action: () => {}, content: html`<div>General settings</div>` },
{ key: 'Security', iconName: 'lucide:shield', action: () => {}, content: html`<div>Security settings</div>` }
]
}}
.paddingPx=${16} // Default padding for tab content
></dees-appui-view>
```
#### `DeesAppuiTabs`
Reusable tab component with horizontal/vertical layout support.
@@ -1234,14 +1234,6 @@ interface IMenuItem {
badgeVariant?: 'default' | 'success' | 'warning' | 'error';
}
// Extended tab interface for views with content
interface IViewTab extends IMenuItem {
content?: TemplateResult | (() => TemplateResult);
useSlotName?: boolean;
slotName?: string;
paddingPx?: number;
}
// Menu group interface for organized menus
interface IMenuGroup {
name: string;

View File

@@ -76,8 +76,8 @@ tap.test('should close all parent menus when clicking action in nested submenu',
expect(childItem).toBeTruthy();
childItem!.click();
// Wait for menus to close
await new Promise(resolve => setTimeout(resolve, 200));
// Wait for menus to close (windowLayer destruction takes 300ms + context menu 100ms)
await new Promise(resolve => setTimeout(resolve, 600));
// Verify action was called
expect(actionCalled).toEqual(true);

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '3.5.0',
version: '3.11.0',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}

View File

@@ -9,7 +9,10 @@ class DemoDashboardView extends DeesElement {
@state()
accessor activated: boolean = false;
private ctx: IViewActivationContext;
onActivate(context: IViewActivationContext) {
this.ctx = context;
this.activated = true;
console.log('Dashboard activated with context:', context);
@@ -75,6 +78,52 @@ class DemoDashboardView extends DeesElement {
.metric { font-size: 32px; font-weight: 700; color: #fafafa; }
.status { display: inline-block; padding: 2px 8px; border-radius: 9px; font-size: 12px; }
.status.active { background: #14532d; color: #4ade80; }
.ctx-actions {
margin-top: 32px;
padding: 24px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
}
.ctx-actions h2 { color: #fafafa; font-size: 16px; font-weight: 600; margin-bottom: 16px; }
.button-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ctx-btn {
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
color: #60a5fa;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.15s ease;
}
.ctx-btn:hover {
background: rgba(59, 130, 246, 0.2);
border-color: rgba(59, 130, 246, 0.5);
}
.ctx-btn.danger {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
.ctx-btn.danger:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.5);
}
.ctx-btn.success {
background: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.3);
color: #4ade80;
}
.ctx-btn.success:hover {
background: rgba(34, 197, 94, 0.2);
border-color: rgba(34, 197, 94, 0.5);
}
</style>
<h1>Dashboard</h1>
<p>Welcome back! Here's an overview of your system.</p>
@@ -95,8 +144,48 @@ class DemoDashboardView extends DeesElement {
<p style="color: #737373; font-size: 12px; margin: 0;">All systems operational</p>
</div>
</div>
<div class="ctx-actions">
<h2>Context Actions (ctx.appui)</h2>
<div class="button-grid">
<button class="ctx-btn" @click=${() => this.ctx?.appui.setMainMenuVisible(false)}>Hide Main Menu</button>
<button class="ctx-btn success" @click=${() => this.ctx?.appui.setMainMenuVisible(true)}>Show Main Menu</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.setSecondaryMenuVisible(false)}>Hide Secondary Menu</button>
<button class="ctx-btn success" @click=${() => this.ctx?.appui.setSecondaryMenuVisible(true)}>Show Secondary Menu</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.setContentTabsVisible(false)}>Hide Content Tabs</button>
<button class="ctx-btn success" @click=${() => this.ctx?.appui.setContentTabsVisible(true)}>Show Content Tabs</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.setMainMenuCollapsed(true)}>Collapse Main Menu</button>
<button class="ctx-btn success" @click=${() => this.ctx?.appui.setMainMenuCollapsed(false)}>Expand Main Menu</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.setBreadcrumbs(['Dashboard', 'Overview', 'Stats'])}>Set Breadcrumbs</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.navigateToView('projects')}>Go to Projects</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.navigateToView('settings', { section: 'security' })}>Go to Settings/Security</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.activityLog.add({ type: 'custom', user: 'Demo User', message: 'Button clicked from ctx!', iconName: 'lucide:mouse-pointer-click' })}>Add Activity Entry</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.setMainMenuBadge('tasks', 99)}>Set Tasks Badge to 99</button>
<button class="ctx-btn danger" @click=${() => this.ctx?.appui.clearMainMenuBadge('tasks')}>Clear Tasks Badge</button>
<button class="ctx-btn" @click=${() => this.ctx?.appui.setContentTabsAutoHide(true, 1)}>Auto-hide Tabs (≤1)</button>
<button class="ctx-btn danger" @click=${() => this.ctx?.appui.setContentTabsAutoHide(false)}>Disable Auto-hide</button>
<button class="ctx-btn success" @click=${() => this.addCloseableTab()}>Add Closeable Tab</button>
</div>
</div>
`;
}
private tabCounter = 0;
private addCloseableTab() {
if (!this.ctx) return;
this.tabCounter++;
const tabKey = `Tab ${this.tabCounter}`;
this.ctx.appui.addContentTab({
key: tabKey,
iconName: 'lucide:file',
action: () => console.log(`Selected ${tabKey}`),
closeable: true,
onClose: () => {
this.ctx?.appui.removeContentTab(tabKey);
}
});
}
}
// Settings view with route params and canDeactivate guard

View File

@@ -110,6 +110,22 @@ export class DeesAppuiBase extends DeesElement {
@property({ type: Boolean })
accessor secondarymenuCollapsed: boolean = false;
// Visibility states
@property({ type: Boolean })
accessor mainmenuVisible: boolean = true;
@property({ type: Boolean })
accessor secondarymenuVisible: boolean = true;
@property({ type: Boolean })
accessor maincontentTabsVisible: boolean = true;
@property({ type: Boolean })
accessor contentTabsAutoHide: boolean = false;
@property({ type: Number })
accessor contentTabsAutoHideThreshold: number = 0;
// Properties for maincontent
@property({ type: Array })
accessor maincontentTabs: interfaces.IMenuItem[] = [];
@@ -213,29 +229,37 @@ export class DeesAppuiBase extends DeesElement {
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
></dees-appui-appbar>
<div class="maingrid">
<dees-appui-mainmenu
.logoIcon=${this.mainmenuLogoIcon}
.logoText=${this.mainmenuLogoText}
.menuGroups=${this.mainmenuGroups}
.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)}
></dees-appui-mainmenu>
<dees-appui-secondarymenu
.heading=${this.secondarymenuHeading}
.groups=${this.secondarymenuGroups}
.selectedItem=${this.secondarymenuSelectedItem}
.collapsed=${this.secondarymenuCollapsed}
@item-select=${(e: CustomEvent) => this.handleSecondarymenuItemSelect(e)}
@collapse-change=${(e: CustomEvent) => this.handleSecondarymenuCollapseChange(e)}
></dees-appui-secondarymenu>
${this.mainmenuVisible ? html`
<dees-appui-mainmenu
.logoIcon=${this.mainmenuLogoIcon}
.logoText=${this.mainmenuLogoText}
.menuGroups=${this.mainmenuGroups}
.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)}
></dees-appui-mainmenu>
` : ''}
${this.secondarymenuVisible ? html`
<dees-appui-secondarymenu
.heading=${this.secondarymenuHeading}
.groups=${this.secondarymenuGroups}
.selectedItem=${this.secondarymenuSelectedItem}
.collapsed=${this.secondarymenuCollapsed}
@item-select=${(e: CustomEvent) => this.handleSecondarymenuItemSelect(e)}
@collapse-change=${(e: CustomEvent) => this.handleSecondarymenuCollapseChange(e)}
></dees-appui-secondarymenu>
` : ''}
<dees-appui-maincontent
.tabs=${this.maincontentTabs}
.selectedTab=${this.maincontentSelectedTab}
.showTabs=${this.maincontentTabsVisible}
.tabsAutoHide=${this.contentTabsAutoHide}
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
>
<div class="view-container"></div>
<slot name="maincontent"></slot>
@@ -425,6 +449,44 @@ export class DeesAppuiBase extends DeesElement {
this.mainmenuCollapsed = collapsed;
}
/**
* Set main menu visibility
*/
public setMainMenuVisible(visible: boolean): void {
this.mainmenuVisible = visible;
}
/**
* Set secondary menu collapsed state
*/
public setSecondaryMenuCollapsed(collapsed: boolean): void {
this.secondarymenuCollapsed = collapsed;
}
/**
* Set secondary menu visibility
*/
public setSecondaryMenuVisible(visible: boolean): void {
this.secondarymenuVisible = visible;
}
/**
* Set content tabs visibility
*/
public setContentTabsVisible(visible: boolean): void {
this.maincontentTabsVisible = visible;
}
/**
* Set content tabs auto-hide behavior
* @param enabled - Enable auto-hide feature
* @param threshold - Hide when tabs.length <= threshold (default 0 = hide when no tabs)
*/
public setContentTabsAutoHide(enabled: boolean, threshold: number = 0): void {
this.contentTabsAutoHide = enabled;
this.contentTabsAutoHideThreshold = threshold;
}
/**
* Set a badge on a main menu item
*/
@@ -977,4 +1039,12 @@ export class DeesAppuiBase extends DeesElement {
composed: true
}));
}
private handleContentTabClose(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('content-tab-close', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
}

View File

@@ -43,6 +43,15 @@ export class DeesAppuiMaincontent extends DeesElement {
@property({ type: Object })
accessor selectedTab: interfaces.IMenuItem | null = null;
@property({ type: Boolean })
accessor showTabs: boolean = true;
@property({ type: Boolean })
accessor tabsAutoHide: boolean = false;
@property({ type: Number })
accessor tabsAutoHideThreshold: number = 0;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
@@ -50,33 +59,36 @@ export class DeesAppuiMaincontent extends DeesElement {
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
color: ${cssManager.bdTheme('#333', '#fff')};
display: block;
display: grid;
grid-template-rows: auto 1fr;
width: 100%;
height: 100%;
position: relative;
background: ${cssManager.bdTheme('#ffffff', '#161616')};
}
.maincontainer {
position: absolute;
height: 100%;
right: 0px;
top: 0px;
width: 100%;
display: contents;
}
.topbar {
position: absolute;
width: 100%;
display: grid;
grid-template-rows: 1fr;
overflow: hidden;
user-select: none;
transition: grid-template-rows 0.3s ease;
}
.topbar > * {
min-height: 0;
}
.content-area {
position: absolute;
top: 60px;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
min-height: 0;
}
:host([notabs]) .topbar {
grid-template-rows: 0fr;
}
`,
];
@@ -90,7 +102,10 @@ export class DeesAppuiMaincontent extends DeesElement {
.selectedTab=${this.selectedTab}
.showTabIndicator=${true}
.tabStyle=${'horizontal'}
.autoHide=${this.tabsAutoHide}
.autoHideThreshold=${this.tabsAutoHideThreshold}
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
@tab-close=${(e: CustomEvent) => this.handleTabClose(e)}
></dees-appui-tabs>
</div>
<div class="content-area">
@@ -103,7 +118,7 @@ export class DeesAppuiMaincontent extends DeesElement {
private handleTabSelect(e: CustomEvent) {
this.selectedTab = e.detail.tab;
// Re-emit the event
this.dispatchEvent(new CustomEvent('tab-select', {
detail: e.detail,
@@ -112,8 +127,32 @@ export class DeesAppuiMaincontent extends DeesElement {
}));
}
private handleTabClose(e: CustomEvent) {
// Re-emit the event
this.dispatchEvent(new CustomEvent('tab-close', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
updated(changedProperties: Map<string | number | symbol, unknown>) {
super.updated(changedProperties);
if (changedProperties.has('showTabs')) {
if (this.showTabs) {
this.removeAttribute('notabs');
} else {
this.setAttribute('notabs', '');
}
}
}
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
await super.firstUpdated(_changedProperties);
// Apply initial notabs state
if (!this.showTabs) {
this.setAttribute('notabs', '');
}
// Tab selection is now handled by the dees-appui-tabs component
// But we need to ensure the tabs component is ready
const tabsComponent = this.shadowRoot.querySelector('dees-appui-tabs') as DeesAppuiTabs;

View File

@@ -336,6 +336,44 @@ export class DeesAppuiMainmenu extends DeesElement {
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: 11px;
font-weight: 600;
border-radius: 9px;
margin-left: auto;
}
.badge.default {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#3f3f46', '#a1a1aa')};
}
.badge.success {
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
color: ${cssManager.bdTheme('#166534', '#4ade80')};
}
.badge.warning {
background: ${cssManager.bdTheme('#fef3c7', '#451a03')};
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
}
.badge.error {
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
}
:host([collapsed]) .badge {
display: none;
}
/* Bottom Section */
.bottomSection {
flex-shrink: 0;
@@ -420,6 +458,9 @@ export class DeesAppuiMainmenu extends DeesElement {
>
<dees-icon .icon="${tabArg.iconName || ''}"></dees-icon>
<span class="tabLabel">${tabArg.key}</span>
${tabArg.badge !== undefined ? html`
<span class="badge ${tabArg.badgeVariant || 'default'}">${tabArg.badge}</span>
` : ''}
<span class="tab-tooltip">${tabArg.key}</span>
</div>
`;

View File

@@ -1,5 +1,212 @@
import { html, cssManager } from '@design.estate/dees-element';
import { html, cssManager, css, DeesElement, customElement, state } from '@design.estate/dees-element';
import * as interfaces from '../../interfaces/index.js';
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
// Interactive demo component for closeable tabs
@customElement('demo-closeable-tabs')
class DemoCloseableTabs extends DeesElement {
@state()
accessor tabs: interfaces.IMenuItem[] = [
{ key: 'Main', iconName: 'lucide:home', action: () => console.log('Main clicked') },
];
@state()
accessor tabCounter: number = 0;
static styles = [
css`
:host {
display: block;
}
.controls {
display: flex;
gap: 8px;
margin-top: 16px;
}
button {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
border: 1px solid ${cssManager.bdTheme('rgba(59, 130, 246, 0.3)', 'rgba(59, 130, 246, 0.3)')};
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.15s ease;
}
button:hover {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
}
.info {
margin-top: 16px;
padding: 12px 16px;
background: ${cssManager.bdTheme('rgba(0,0,0,0.02)', 'rgba(255,255,255,0.02)')};
border-radius: 6px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`
];
private addTab() {
this.tabCounter++;
const tabKey = `Document ${this.tabCounter}`;
this.tabs = [
...this.tabs,
{
key: tabKey,
iconName: 'lucide:file',
action: () => console.log(`${tabKey} clicked`),
closeable: true,
onClose: () => this.removeTab(tabKey)
}
];
}
private removeTab(tabKey: string) {
this.tabs = this.tabs.filter(t => t.key !== tabKey);
}
render() {
return html`
<dees-appui-tabs
.tabs=${this.tabs}
@tab-close=${(e: CustomEvent) => this.removeTab(e.detail.tab.key)}
></dees-appui-tabs>
<div class="controls">
<button @click=${() => this.addTab()}>+ Add New Tab</button>
</div>
<div class="info">
Click the X button on tabs to close them. The "Main" tab is not closeable.
<br>Current tabs: ${this.tabs.length}
</div>
`;
}
}
// Interactive demo for auto-hide feature
@customElement('demo-autohide-tabs')
class DemoAutoHideTabs extends DeesElement {
@state()
accessor tabs: interfaces.IMenuItem[] = [
{ key: 'Tab 1', iconName: 'lucide:file', action: () => console.log('Tab 1') },
{ key: 'Tab 2', iconName: 'lucide:file', action: () => console.log('Tab 2') },
];
@state()
accessor autoHide: boolean = true;
@state()
accessor threshold: number = 1;
static styles = [
css`
:host {
display: block;
}
.tabs-container {
min-height: 60px;
border: 1px dashed ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.tabs-container dees-appui-tabs {
width: 100%;
}
.placeholder {
color: ${cssManager.bdTheme('#a1a1aa', '#71717a')};
font-size: 13px;
font-style: italic;
}
.controls {
display: flex;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}
button {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
border: 1px solid ${cssManager.bdTheme('rgba(59, 130, 246, 0.3)', 'rgba(59, 130, 246, 0.3)')};
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.15s ease;
}
button:hover {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
}
button.danger {
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')};
border-color: ${cssManager.bdTheme('rgba(239, 68, 68, 0.3)', 'rgba(239, 68, 68, 0.3)')};
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
}
button.danger:hover {
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.2)', 'rgba(239, 68, 68, 0.2)')};
}
.info {
margin-top: 16px;
padding: 12px 16px;
background: ${cssManager.bdTheme('rgba(0,0,0,0.02)', 'rgba(255,255,255,0.02)')};
border-radius: 6px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`
];
private tabCounter = 2;
private addTab() {
this.tabCounter++;
this.tabs = [...this.tabs, {
key: `Tab ${this.tabCounter}`,
iconName: 'lucide:file',
action: () => console.log(`Tab ${this.tabCounter}`)
}];
}
private removeLastTab() {
if (this.tabs.length > 0) {
this.tabs = this.tabs.slice(0, -1);
}
}
private clearTabs() {
this.tabs = [];
}
render() {
const shouldHide = this.autoHide && this.tabs.length <= this.threshold;
return html`
<div class="tabs-container">
${shouldHide
? html`<span class="placeholder">Tabs hidden (${this.tabs.length} tabs ≤ threshold ${this.threshold})</span>`
: html`<dees-appui-tabs
.tabs=${this.tabs}
.autoHide=${this.autoHide}
.autoHideThreshold=${this.threshold}
></dees-appui-tabs>`
}
</div>
<div class="controls">
<button @click=${() => this.addTab()}>+ Add Tab</button>
<button class="danger" @click=${() => this.removeLastTab()}>- Remove Tab</button>
<button class="danger" @click=${() => this.clearTabs()}>Clear All</button>
<button @click=${() => { this.threshold = 0; }}>Threshold: 0</button>
<button @click=${() => { this.threshold = 1; }}>Threshold: 1</button>
<button @click=${() => { this.threshold = 2; }}>Threshold: 2</button>
</div>
<div class="info">
Auto-hide: ${this.autoHide ? 'ON' : 'OFF'} | Threshold: ${this.threshold} | Tabs: ${this.tabs.length}
<br>Tabs will hide when count ≤ threshold.
</div>
`;
}
}
export const demoFunc = () => {
const horizontalTabs: interfaces.IMenuItem[] = [
@@ -67,9 +274,18 @@ export const demoFunc = () => {
<div class="demo-container">
<div class="section">
<div class="section-title">Horizontal Tabs with Animated Indicator</div>
<dees-appui-tabs .tabs=${horizontalTabs}>
${demoContent('Select a tab to see the smooth sliding animation of the indicator. The indicator automatically adjusts its width to match the tab content with minimal padding.')}
</dees-appui-tabs>
<dees-appui-tabs .tabs=${horizontalTabs}></dees-appui-tabs>
${demoContent('Select a tab to see the smooth sliding animation of the indicator. The indicator automatically adjusts its width to match the tab content with minimal padding.')}
</div>
<div class="section">
<div class="section-title">Closeable Tabs (Browser-style)</div>
<demo-closeable-tabs></demo-closeable-tabs>
</div>
<div class="section">
<div class="section-title">Auto-hide Tabs</div>
<demo-autohide-tabs></demo-autohide-tabs>
</div>
<div class="section">
@@ -82,9 +298,8 @@ export const demoFunc = () => {
<div class="section">
<div class="section-title">Without Indicator</div>
<dees-appui-tabs .showTabIndicator=${false} .tabs=${noIndicatorTabs}>
${demoContent('Tabs can also be used without the animated indicator by setting showTabIndicator to false.')}
</dees-appui-tabs>
<dees-appui-tabs .showTabIndicator=${false} .tabs=${noIndicatorTabs}></dees-appui-tabs>
${demoContent('Tabs can also be used without the animated indicator by setting showTabIndicator to false.')}
</div>
</div>
`;

View File

@@ -4,6 +4,7 @@ import {
DeesElement,
type TemplateResult,
property,
state,
customElement,
html,
css,
@@ -33,6 +34,21 @@ export class DeesAppuiTabs extends DeesElement {
@property({ type: String })
accessor tabStyle: 'horizontal' | 'vertical' = 'horizontal';
@property({ type: Boolean })
accessor autoHide: boolean = false;
@property({ type: Number })
accessor autoHideThreshold: number = 0;
// Scroll state for fade indicators
@state()
private accessor canScrollLeft: boolean = false;
@state()
private accessor canScrollRight: boolean = false;
private resizeObserver: ResizeObserver | null = null;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
@@ -42,21 +58,56 @@ export class DeesAppuiTabs extends DeesElement {
display: block;
position: relative;
width: 100%;
min-width: 0;
overflow: hidden;
}
.tabs-wrapper {
position: relative;
min-width: 0;
}
.tabs-wrapper.horizontal-wrapper {
height: 48px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
box-sizing: border-box;
overflow: hidden;
}
/* Scroll fade indicators */
.scroll-fade {
position: absolute;
top: 0;
bottom: 1px;
width: 48px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 10;
}
.scroll-fade-left {
left: 0;
background: linear-gradient(to right,
${cssManager.bdTheme('#ffffff', '#161616')} 0%,
${cssManager.bdTheme('rgba(255,255,255,0)', 'rgba(22,22,22,0)')} 100%);
}
.scroll-fade-right {
right: 0;
background: linear-gradient(to left,
${cssManager.bdTheme('#ffffff', '#161616')} 0%,
${cssManager.bdTheme('rgba(255,255,255,0)', 'rgba(22,22,22,0)')} 100%);
}
.scroll-fade.visible {
opacity: 1;
}
.tabsContainer {
position: relative;
user-select: none;
min-width: 0;
}
.tabsContainer.horizontal {
@@ -64,14 +115,39 @@ export class DeesAppuiTabs extends DeesElement {
align-items: center;
font-size: 14px;
overflow-x: auto;
scrollbar-width: none;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: transparent transparent;
height: 100%;
padding: 0 16px;
gap: 4px;
}
/* Show scrollbar on hover */
.tabs-wrapper:hover .tabsContainer.horizontal {
scrollbar-color: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')} transparent;
}
.tabsContainer.horizontal::-webkit-scrollbar {
display: none;
height: 4px;
}
.tabsContainer.horizontal::-webkit-scrollbar-track {
background: transparent;
}
.tabsContainer.horizontal::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 2px;
transition: background 0.2s ease;
}
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')};
}
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('rgba(0,0,0,0.35)', 'rgba(255,255,255,0.35)')};
}
.tabsContainer.vertical {
@@ -199,18 +275,51 @@ export class DeesAppuiTabs extends DeesElement {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.content {
padding: 32px 24px;
/* Close button */
.tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 4px;
margin-left: 8px;
opacity: 0.4;
transition: opacity 0.15s, background 0.15s;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
}
.tab:hover .tab-close {
opacity: 0.7;
}
.tab-close:hover {
opacity: 1;
background: ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
}
.tab.selectedTab .tab-close {
opacity: 0.5;
}
.tab.selectedTab:hover .tab-close {
opacity: 0.8;
}
.tab.selectedTab .tab-close:hover {
opacity: 1;
}
`,
];
public render(): TemplateResult {
// Auto-hide when enabled and tab count is at or below threshold
if (this.autoHide && this.tabs.length <= this.autoHideThreshold) {
return html``;
}
return html`
${this.renderTabsWrapper()}
<div class="content">
<slot></slot>
</div>
`;
}
@@ -219,6 +328,19 @@ export class DeesAppuiTabs extends DeesElement {
const wrapperClass = isHorizontal ? 'tabs-wrapper horizontal-wrapper' : 'vertical-wrapper';
const containerClass = `tabsContainer ${this.tabStyle}`;
if (isHorizontal) {
return html`
<div class="${wrapperClass}">
<div class="scroll-fade scroll-fade-left ${this.canScrollLeft ? 'visible' : ''}"></div>
<div class="${containerClass}" @scroll=${this.handleScroll}>
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
</div>
<div class="scroll-fade scroll-fade-right ${this.canScrollRight ? 'visible' : ''}"></div>
${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''}
</div>
`;
}
return html`
<div class="${wrapperClass}">
<div class="${containerClass}">
@@ -232,15 +354,23 @@ export class DeesAppuiTabs extends DeesElement {
private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult {
const isSelected = tab === this.selectedTab;
const classes = `tab ${isSelected ? 'selectedTab' : ''}`;
const closeButton = tab.closeable ? html`
<span class="tab-close" @click="${(e: Event) => this.closeTab(e, tab)}">
<dees-icon .icon=${'lucide:x'} style="font-size: 12px;"></dees-icon>
</span>
` : '';
const content = isHorizontal ? html`
<span class="tab-content">
${this.renderTabIcon(tab)}
${tab.key}
</span>
${closeButton}
` : html`
${this.renderTabIcon(tab)}
${tab.key}
${closeButton}
`;
return html`
@@ -260,7 +390,12 @@ export class DeesAppuiTabs extends DeesElement {
private selectTab(tabArg: interfaces.IMenuItem) {
this.selectedTab = tabArg;
tabArg.action();
// Scroll selected tab into view
requestAnimationFrame(() => {
this.scrollTabIntoView(tabArg);
});
// Emit tab-select event
this.dispatchEvent(new CustomEvent('tab-select', {
detail: { tab: tabArg },
@@ -269,19 +404,107 @@ export class DeesAppuiTabs extends DeesElement {
}));
}
private closeTab(e: Event, tab: interfaces.IMenuItem) {
e.stopPropagation(); // Don't select tab when closing
// Call the tab's onClose callback if defined
if (tab.onClose) {
tab.onClose();
}
// Also emit event for parent components
this.dispatchEvent(new CustomEvent('tab-close', {
detail: { tab },
bubbles: true,
composed: true
}));
}
firstUpdated() {
if (this.tabs && this.tabs.length > 0) {
this.selectTab(this.tabs[0]);
}
// Set up ResizeObserver for scroll state updates
this.setupResizeObserver();
// Initial scroll state check
requestAnimationFrame(() => {
this.updateScrollState();
});
}
async disconnectedCallback() {
await super.disconnectedCallback();
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
private setupResizeObserver() {
if (this.tabStyle !== 'horizontal') return;
this.resizeObserver = new ResizeObserver(() => {
this.updateScrollState();
});
const container = this.shadowRoot?.querySelector('.tabsContainer.horizontal');
if (container) {
this.resizeObserver.observe(container);
}
}
private handleScroll = () => {
this.updateScrollState();
};
private updateScrollState() {
const container = this.shadowRoot?.querySelector('.tabsContainer.horizontal') as HTMLElement;
if (!container) return;
const scrollLeft = container.scrollLeft;
const scrollWidth = container.scrollWidth;
const clientWidth = container.clientWidth;
// Small threshold to account for rounding
const threshold = 2;
this.canScrollLeft = scrollLeft > threshold;
this.canScrollRight = scrollLeft < scrollWidth - clientWidth - threshold;
}
private scrollTabIntoView(tab: interfaces.IMenuItem) {
if (this.tabStyle !== 'horizontal') return;
const tabIndex = this.tabs.indexOf(tab);
if (tabIndex === -1) return;
const container = this.shadowRoot?.querySelector('.tabsContainer.horizontal') as HTMLElement;
const tabElement = container?.querySelector(`.tab:nth-child(${tabIndex + 1})`) as HTMLElement;
if (tabElement && container) {
const containerRect = container.getBoundingClientRect();
const tabRect = tabElement.getBoundingClientRect();
// Check if tab is fully visible
const isFullyVisible =
tabRect.left >= containerRect.left &&
tabRect.right <= containerRect.right;
if (!isFullyVisible) {
tabElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
}
}
}
async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('tabs') && this.tabs && this.tabs.length > 0 && !this.selectedTab) {
this.selectTab(this.tabs[0]);
}
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
await this.updateComplete;
// Wait for fonts to load on first update
@@ -290,6 +513,7 @@ export class DeesAppuiTabs extends DeesElement {
}
requestAnimationFrame(() => {
this.updateTabIndicator();
this.updateScrollState();
});
}
}

View File

@@ -1,30 +0,0 @@
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html`
<dees-appui-view
.viewConfig=${{
id: 'demo-view',
name: 'Demo View',
description: 'A demonstration view',
iconName: 'lucide:home',
tabs: [
{
key: 'overview',
iconName: 'lucide:lineChart',
action: () => console.log('Overview tab'),
content: html`<div style="padding: 20px;">Overview Content</div>`
},
{
key: 'details',
iconName: 'lucide:fileText',
action: () => console.log('Details tab'),
content: html`<div style="padding: 20px;">Details Content</div>`
}
],
menuItems: [
{ key: 'General', action: () => console.log('General') },
{ key: 'Advanced', action: () => console.log('Advanced') },
]
}}
></dees-appui-view>
`;

View File

@@ -1,180 +0,0 @@
import * as interfaces from '../../interfaces/index.js';
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import '../dees-appui-tabs/dees-appui-tabs.js';
import type { DeesAppuiTabs } from '../dees-appui-tabs/dees-appui-tabs.js';
import { demoFunc } from './dees-appui-view.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
export interface IViewTab extends interfaces.IMenuItem {
content?: TemplateResult | (() => TemplateResult);
useSlotName?: boolean;
slotName?: string;
paddingPx?: number;
}
export interface IAppView {
id: string;
name: string;
description?: string;
iconName?: string;
tabs: IViewTab[];
menuItems?: interfaces.IMenuItem[];
}
@customElement('dees-appui-view')
export class DeesAppuiView extends DeesElement {
public static demo = demoFunc;
// INSTANCE
@property({ type: Object })
accessor viewConfig: IAppView;
@state()
accessor selectedTab: IViewTab | null = null;
@state()
accessor tabs: DeesAppuiTabs;
@property({ type: Number })
accessor paddingPx: number = 16;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
position: relative;
width: 100%;
height: 100%;
background: #161616;
}
.view-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.view-header {
background: #000000;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.view-content {
flex: 1;
position: relative;
overflow: hidden;
}
.tab-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
opacity: 0;
transition: opacity 0.2s;
}
.tab-content.active {
opacity: 1;
}
dees-appui-tabs {
height: 60px;
}
`,
];
public render(): TemplateResult {
if (!this.viewConfig) {
return html`<div>No view configuration provided</div>`;
}
return html`
<div class="view-container">
<div class="view-header">
<dees-appui-tabs
.tabs=${this.viewConfig.tabs}
.selectedTab=${this.selectedTab}
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
></dees-appui-tabs>
</div>
<div class="view-content">
${this.viewConfig.tabs.map((tab) => {
const isActive = tab === this.selectedTab;
const content = typeof tab.content === 'function' ? tab.content() : tab.content;
const padding = tab.paddingPx ?? this.paddingPx;
return html`
<div class="tab-content ${isActive ? 'active' : ''}" style="padding: ${padding}px;">
${content
? content
: tab.useSlotName
? html`<slot name="${tab.slotName || tab.key}"></slot>`
: ''}
</div>
`;
})}
</div>
</div>
`;
}
async firstUpdated() {
this.tabs = this.shadowRoot.querySelector('dees-appui-tabs');
if (this.viewConfig?.tabs?.length > 0) {
this.selectedTab = this.viewConfig.tabs[0];
}
}
private handleTabSelect(e: CustomEvent) {
this.selectedTab = e.detail.tab;
// Re-emit the event with view context
this.dispatchEvent(new CustomEvent('view-tab-select', {
detail: {
view: this.viewConfig,
tab: e.detail.tab
},
bubbles: true,
composed: true
}));
}
// Public methods for external control
public selectTab(tabKey: string) {
const tab = this.viewConfig.tabs.find(t => t.key === tabKey);
if (tab) {
this.selectedTab = tab;
if (this.tabs) {
this.tabs.selectedTab = tab;
}
}
}
public getMenuItems(): interfaces.IMenuItem[] {
return this.viewConfig?.menuItems || [];
}
public getTabs(): IViewTab[] {
return this.viewConfig?.tabs || [];
}
}

View File

@@ -1 +0,0 @@
export * from './dees-appui-view.js';

View File

@@ -7,4 +7,3 @@ export * from './dees-appui-mainmenu/index.js';
export * from './dees-appui-secondarymenu/index.js';
export * from './dees-appui-profiledropdown/index.js';
export * from './dees-appui-tabs/index.js';
export * from './dees-appui-view/index.js';

View File

@@ -134,6 +134,7 @@ export class DeesContextmenu extends DeesElement {
private submenu: DeesContextmenu | null = null;
private submenuTimeout: any = null;
private parentMenu: DeesContextmenu | null = null;
private isDestroying: boolean = false;
constructor() {
super();
@@ -416,27 +417,33 @@ export class DeesContextmenu extends DeesElement {
}
public async destroy() {
// Guard against double-destruction
if (this.isDestroying) {
return;
}
this.isDestroying = true;
// Clear timeout
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
this.submenuTimeout = null;
}
// Destroy submenu first
if (this.submenu) {
await this.submenu.destroy();
this.submenu = null;
}
// Only destroy window layer if this is not a submenu
if (this.windowLayer && !this.parentMenu) {
this.windowLayer.destroy();
await this.windowLayer.destroy();
}
this.style.opacity = '0';
this.style.transform = 'scale(0.95) translateY(-10px)';
await domtools.plugins.smartdelay.delayFor(100);
if (this.parentElement) {
this.parentElement.removeChild(this);
}
@@ -446,13 +453,14 @@ export class DeesContextmenu extends DeesElement {
* Destroys this menu and all parent menus in the chain
*/
public async destroyAll() {
// First destroy parent menus if they exist
if (this.parentMenu) {
await this.parentMenu.destroyAll();
} else {
// If we're at the top level, just destroy this menu
await this.destroy();
// Find the root menu (top-level parent)
let rootMenu: DeesContextmenu = this;
while (rootMenu.parentMenu) {
rootMenu = rootMenu.parentMenu;
}
// Destroy from the root - this will cascade through all submenus
await rootMenu.destroy();
}
}

View File

@@ -18,6 +18,11 @@ export type TDeesAppuiBase = HTMLElement & {
removeMainMenuItem: (groupName: string, tabKey: string) => void;
setMainMenuSelection: (tabKey: string) => void;
setMainMenuCollapsed: (collapsed: boolean) => void;
setMainMenuVisible: (visible: boolean) => void;
setSecondaryMenuCollapsed: (collapsed: boolean) => void;
setSecondaryMenuVisible: (visible: boolean) => void;
setContentTabsVisible: (visible: boolean) => void;
setContentTabsAutoHide: (enabled: boolean, threshold?: number) => void;
setMainMenuBadge: (tabKey: string, badge: string | number) => void;
clearMainMenuBadge: (tabKey: string) => void;
setSecondaryMenu: (config: { heading?: string; groups: IMenuGroup[] }) => void;

View File

@@ -4,4 +4,6 @@ export interface IMenuItem {
action: () => void;
badge?: string | number;
badgeVariant?: 'default' | 'success' | 'warning' | 'error';
closeable?: boolean;
onClose?: () => void;
}