feat(appui-tabs): add closeable tabs and auto-hide behavior for content tabs, plus API and events to control them
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
## 2025-12-29 - 3.9.0 - feat(dees-appui-mainmenu)
|
||||||
add status badges to main menu items with theme-aware styling
|
add status badges to main menu items with theme-aware styling
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.9.0',
|
version: '3.10.0',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,10 +162,30 @@ class DemoDashboardView extends DeesElement {
|
|||||||
<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.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" @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 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>
|
||||||
</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
|
// Settings view with route params and canDeactivate guard
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ export class DeesAppuiBase extends DeesElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
accessor maincontentTabsVisible: boolean = true;
|
accessor maincontentTabsVisible: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
accessor contentTabsAutoHide: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor contentTabsAutoHideThreshold: number = 0;
|
||||||
|
|
||||||
// Properties for maincontent
|
// Properties for maincontent
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
accessor maincontentTabs: interfaces.IMenuItem[] = [];
|
accessor maincontentTabs: interfaces.IMenuItem[] = [];
|
||||||
@@ -250,7 +256,10 @@ export class DeesAppuiBase extends DeesElement {
|
|||||||
.tabs=${this.maincontentTabs}
|
.tabs=${this.maincontentTabs}
|
||||||
.selectedTab=${this.maincontentSelectedTab}
|
.selectedTab=${this.maincontentSelectedTab}
|
||||||
.showTabs=${this.maincontentTabsVisible}
|
.showTabs=${this.maincontentTabsVisible}
|
||||||
|
.tabsAutoHide=${this.contentTabsAutoHide}
|
||||||
|
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
|
||||||
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
|
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
|
||||||
|
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
|
||||||
>
|
>
|
||||||
<div class="view-container"></div>
|
<div class="view-container"></div>
|
||||||
<slot name="maincontent"></slot>
|
<slot name="maincontent"></slot>
|
||||||
@@ -468,6 +477,16 @@ export class DeesAppuiBase extends DeesElement {
|
|||||||
this.maincontentTabsVisible = visible;
|
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
|
* Set a badge on a main menu item
|
||||||
*/
|
*/
|
||||||
@@ -1020,4 +1039,12 @@ export class DeesAppuiBase extends DeesElement {
|
|||||||
composed: true
|
composed: true
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleContentTabClose(e: CustomEvent) {
|
||||||
|
this.dispatchEvent(new CustomEvent('content-tab-close', {
|
||||||
|
detail: e.detail,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export class DeesAppuiMaincontent extends DeesElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
accessor showTabs: boolean = true;
|
accessor showTabs: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
accessor tabsAutoHide: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor tabsAutoHideThreshold: number = 0;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
@@ -96,7 +102,10 @@ export class DeesAppuiMaincontent extends DeesElement {
|
|||||||
.selectedTab=${this.selectedTab}
|
.selectedTab=${this.selectedTab}
|
||||||
.showTabIndicator=${true}
|
.showTabIndicator=${true}
|
||||||
.tabStyle=${'horizontal'}
|
.tabStyle=${'horizontal'}
|
||||||
|
.autoHide=${this.tabsAutoHide}
|
||||||
|
.autoHideThreshold=${this.tabsAutoHideThreshold}
|
||||||
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
|
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
|
||||||
|
@tab-close=${(e: CustomEvent) => this.handleTabClose(e)}
|
||||||
></dees-appui-tabs>
|
></dees-appui-tabs>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
@@ -109,7 +118,7 @@ export class DeesAppuiMaincontent extends DeesElement {
|
|||||||
|
|
||||||
private handleTabSelect(e: CustomEvent) {
|
private handleTabSelect(e: CustomEvent) {
|
||||||
this.selectedTab = e.detail.tab;
|
this.selectedTab = e.detail.tab;
|
||||||
|
|
||||||
// Re-emit the event
|
// Re-emit the event
|
||||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||||
detail: e.detail,
|
detail: e.detail,
|
||||||
@@ -118,6 +127,15 @@ 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>) {
|
updated(changedProperties: Map<string | number | symbol, unknown>) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (changedProperties.has('showTabs')) {
|
if (changedProperties.has('showTabs')) {
|
||||||
|
|||||||
@@ -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 * 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 = () => {
|
export const demoFunc = () => {
|
||||||
const horizontalTabs: interfaces.IMenuItem[] = [
|
const horizontalTabs: interfaces.IMenuItem[] = [
|
||||||
@@ -71,6 +278,16 @@ export const demoFunc = () => {
|
|||||||
${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.')}
|
${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>
|
||||||
|
|
||||||
|
<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">
|
<div class="section">
|
||||||
<div class="section-title">Vertical Tabs Layout</div>
|
<div class="section-title">Vertical Tabs Layout</div>
|
||||||
<div class="two-column">
|
<div class="two-column">
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ export class DeesAppuiTabs extends DeesElement {
|
|||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
accessor tabStyle: 'horizontal' | 'vertical' = 'horizontal';
|
accessor tabStyle: 'horizontal' | 'vertical' = 'horizontal';
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
accessor autoHide: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor autoHideThreshold: number = 0;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
@@ -198,10 +204,50 @@ export class DeesAppuiTabs extends DeesElement {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
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`
|
return html`
|
||||||
${this.renderTabsWrapper()}
|
${this.renderTabsWrapper()}
|
||||||
`;
|
`;
|
||||||
@@ -225,15 +271,23 @@ export class DeesAppuiTabs extends DeesElement {
|
|||||||
private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult {
|
private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult {
|
||||||
const isSelected = tab === this.selectedTab;
|
const isSelected = tab === this.selectedTab;
|
||||||
const classes = `tab ${isSelected ? '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`
|
const content = isHorizontal ? html`
|
||||||
<span class="tab-content">
|
<span class="tab-content">
|
||||||
${this.renderTabIcon(tab)}
|
${this.renderTabIcon(tab)}
|
||||||
${tab.key}
|
${tab.key}
|
||||||
</span>
|
</span>
|
||||||
|
${closeButton}
|
||||||
` : html`
|
` : html`
|
||||||
${this.renderTabIcon(tab)}
|
${this.renderTabIcon(tab)}
|
||||||
${tab.key}
|
${tab.key}
|
||||||
|
${closeButton}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -253,7 +307,7 @@ export class DeesAppuiTabs extends DeesElement {
|
|||||||
private selectTab(tabArg: interfaces.IMenuItem) {
|
private selectTab(tabArg: interfaces.IMenuItem) {
|
||||||
this.selectedTab = tabArg;
|
this.selectedTab = tabArg;
|
||||||
tabArg.action();
|
tabArg.action();
|
||||||
|
|
||||||
// Emit tab-select event
|
// Emit tab-select event
|
||||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||||
detail: { tab: tabArg },
|
detail: { tab: tabArg },
|
||||||
@@ -262,6 +316,22 @@ 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() {
|
firstUpdated() {
|
||||||
if (this.tabs && this.tabs.length > 0) {
|
if (this.tabs && this.tabs.length > 0) {
|
||||||
this.selectTab(this.tabs[0]);
|
this.selectTab(this.tabs[0]);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type TDeesAppuiBase = HTMLElement & {
|
|||||||
setSecondaryMenuCollapsed: (collapsed: boolean) => void;
|
setSecondaryMenuCollapsed: (collapsed: boolean) => void;
|
||||||
setSecondaryMenuVisible: (visible: boolean) => void;
|
setSecondaryMenuVisible: (visible: boolean) => void;
|
||||||
setContentTabsVisible: (visible: boolean) => void;
|
setContentTabsVisible: (visible: boolean) => void;
|
||||||
|
setContentTabsAutoHide: (enabled: boolean, threshold?: number) => void;
|
||||||
setMainMenuBadge: (tabKey: string, badge: string | number) => void;
|
setMainMenuBadge: (tabKey: string, badge: string | number) => void;
|
||||||
clearMainMenuBadge: (tabKey: string) => void;
|
clearMainMenuBadge: (tabKey: string) => void;
|
||||||
setSecondaryMenu: (config: { heading?: string; groups: IMenuGroup[] }) => void;
|
setSecondaryMenu: (config: { heading?: string; groups: IMenuGroup[] }) => void;
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ export interface IMenuItem {
|
|||||||
action: () => void;
|
action: () => void;
|
||||||
badge?: string | number;
|
badge?: string | number;
|
||||||
badgeVariant?: 'default' | 'success' | 'warning' | 'error';
|
badgeVariant?: 'default' | 'success' | 'warning' | 'error';
|
||||||
|
closeable?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user