import * as interfaces from './interfaces/index.js'; import { DeesElement, type TemplateResult, property, customElement, html, css, cssManager, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; @customElement('dees-appui-tabs') export class DeesAppuiTabs extends DeesElement { public static demo = () => { const horizontalTabs: interfaces.ITab[] = [ { key: 'Home', iconName: 'lucide:home', action: () => console.log('Home clicked') }, { key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => console.log('Analytics clicked') }, { key: 'Reports', iconName: 'lucide:fileText', action: () => console.log('Reports clicked') }, { key: 'User Settings', iconName: 'lucide:settings', action: () => console.log('Settings clicked') }, { key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help clicked') }, ]; const verticalTabs: interfaces.ITab[] = [ { key: 'Profile', iconName: 'lucide:user', action: () => console.log('Profile clicked') }, { key: 'Security', iconName: 'lucide:shield', action: () => console.log('Security clicked') }, { key: 'Notifications', iconName: 'lucide:bell', action: () => console.log('Notifications clicked') }, { key: 'Integrations', iconName: 'lucide:link', action: () => console.log('Integrations clicked') }, { key: 'Advanced', iconName: 'lucide:code', action: () => console.log('Advanced clicked') }, ]; const noIndicatorTabs: interfaces.ITab[] = [ { key: 'All', action: () => console.log('All clicked') }, { key: 'Active', action: () => console.log('Active clicked') }, { key: 'Completed', action: () => console.log('Completed clicked') }, { key: 'Archived', action: () => console.log('Archived clicked') }, ]; const demoContent = (text: string) => html`
${text}
`; return html`
Horizontal Tabs with Animated Indicator
${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.')}
Vertical Tabs Layout
${demoContent('Vertical tabs work great for settings pages and navigation menus. The animated indicator smoothly transitions between selections.')}
Without Indicator
${demoContent('Tabs can also be used without the animated indicator by setting showTabIndicator to false.')}
`; }; // INSTANCE @property({ type: Array, }) public tabs: interfaces.ITab[] = []; @property({ type: Object }) public selectedTab: interfaces.ITab | null = null; @property({ type: Boolean }) public showTabIndicator: boolean = true; @property({ type: String }) public tabStyle: 'horizontal' | 'vertical' = 'horizontal'; public static styles = [ cssManager.defaultStyles, css` :host { display: block; position: relative; width: 100%; } .tabs-wrapper { position: relative; } .tabs-wrapper.horizontal-wrapper { border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; } .tabsContainer { position: relative; user-select: none; } .tabsContainer.horizontal { display: flex; align-items: center; font-size: 14px; overflow-x: auto; scrollbar-width: none; height: 48px; padding: 0 16px; gap: 4px; } .tabsContainer.horizontal::-webkit-scrollbar { display: none; } .tabsContainer.vertical { display: flex; flex-direction: column; padding: 8px; font-size: 14px; gap: 2px; position: relative; background: ${cssManager.bdTheme('#f9fafb', '#18181b')}; border-radius: 8px; } .tab { color: ${cssManager.bdTheme('#71717a', '#71717a')}; white-space: nowrap; cursor: pointer; transition: color 0.15s ease; font-weight: 500; position: relative; z-index: 2; } .horizontal .tab { padding: 0 16px; height: 100%; display: inline-flex; align-items: center; gap: 8px; position: relative; border-radius: 6px 6px 0 0; transition: background-color 0.15s ease; } .horizontal .tab:not(:last-child)::after { content: ''; position: absolute; right: -2px; top: 50%; transform: translateY(-50%); height: 20px; width: 1px; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; opacity: 0.5; } .horizontal .tab .tab-content { display: inline-flex; align-items: center; gap: 8px; } .vertical .tab { padding: 10px 16px; border-radius: 6px; width: 100%; display: flex; align-items: center; gap: 8px; transition: all 0.15s ease; } .tab:hover { color: ${cssManager.bdTheme('#09090b', '#fafafa')}; } .horizontal .tab:hover { background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.03)')}; } .horizontal .tab:hover::after, .horizontal .tab:hover + .tab::after { opacity: 0; } .vertical .tab:hover { background: ${cssManager.bdTheme('rgba(244, 244, 245, 0.5)', 'rgba(39, 39, 42, 0.5)')}; } .horizontal .tab.selectedTab { color: ${cssManager.bdTheme('#09090b', '#fafafa')}; } .horizontal .tab.selectedTab::after, .horizontal .tab.selectedTab + .tab::after { opacity: 0; } .vertical .tab.selectedTab { color: ${cssManager.bdTheme('#09090b', '#fafafa')}; } .tab dees-icon { font-size: 16px; } .tabIndicator { position: absolute; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); opacity: 0; } .tabIndicator.no-transition { transition: none; } .tabs-wrapper .tabIndicator { height: 3px; bottom: 0; background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; border-radius: 3px 3px 0 0; z-index: 3; } .vertical-wrapper { position: relative; } .vertical-wrapper .tabIndicator { left: 8px; right: 8px; border-radius: 6px; background: ${cssManager.bdTheme('#ffffff', '#27272a')}; z-index: 1; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } .content { padding: 32px 24px; } `, ]; public render(): TemplateResult { return html` ${this.renderTabsWrapper()}
`; } private renderTabsWrapper(): TemplateResult { const isHorizontal = this.tabStyle === 'horizontal'; const wrapperClass = isHorizontal ? 'tabs-wrapper horizontal-wrapper' : 'vertical-wrapper'; const containerClass = `tabsContainer ${this.tabStyle}`; return html`
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
${this.showTabIndicator ? html`
` : ''}
`; } private renderTab(tab: interfaces.ITab, isHorizontal: boolean): TemplateResult { const isSelected = tab === this.selectedTab; const classes = `tab ${isSelected ? 'selectedTab' : ''}`; const content = isHorizontal ? html` ${this.renderTabIcon(tab)} ${tab.key} ` : html` ${this.renderTabIcon(tab)} ${tab.key} `; return html`
${content}
`; } private renderTabIcon(tab: interfaces.ITab): TemplateResult | '' { return tab.iconName ? html`` : ''; } private selectTab(tabArg: interfaces.ITab) { this.selectedTab = tabArg; tabArg.action(); // Emit tab-select event this.dispatchEvent(new CustomEvent('tab-select', { detail: { tab: tabArg }, bubbles: true, composed: true })); } firstUpdated() { if (this.tabs && this.tabs.length > 0) { this.selectTab(this.tabs[0]); } } async updated(changedProperties: Map) { 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 if (!this.indicatorInitialized && document.fonts) { await document.fonts.ready; } requestAnimationFrame(() => { this.updateTabIndicator(); }); } } private indicatorInitialized = false; private updateTabIndicator() { if (!this.shouldShowIndicator()) return; const selectedTabElement = this.getSelectedTabElement(); if (!selectedTabElement) return; const indicator = this.getIndicatorElement(); if (!indicator) return; this.handleInitialTransition(indicator); if (this.tabStyle === 'horizontal') { this.updateHorizontalIndicator(indicator, selectedTabElement); } else { this.updateVerticalIndicator(indicator, selectedTabElement); } indicator.style.opacity = '1'; } private shouldShowIndicator(): boolean { return this.selectedTab && this.showTabIndicator && this.tabs.includes(this.selectedTab); } private getSelectedTabElement(): HTMLElement | null { const selectedIndex = this.tabs.indexOf(this.selectedTab); const isHorizontal = this.tabStyle === 'horizontal'; const selector = isHorizontal ? `.tabs-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})` : `.vertical-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`; return this.shadowRoot.querySelector(selector); } private getIndicatorElement(): HTMLElement | null { return this.shadowRoot.querySelector('.tabIndicator'); } private handleInitialTransition(indicator: HTMLElement): void { if (!this.indicatorInitialized) { indicator.classList.add('no-transition'); this.indicatorInitialized = true; setTimeout(() => { indicator.classList.remove('no-transition'); }, 50); } } private updateHorizontalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void { const tabContent = tabElement.querySelector('.tab-content') as HTMLElement; if (!tabContent) return; const wrapperRect = indicator.parentElement.getBoundingClientRect(); const contentRect = tabContent.getBoundingClientRect(); const contentLeft = contentRect.left - wrapperRect.left; const indicatorWidth = contentRect.width + 8; const indicatorLeft = contentLeft - 4; indicator.style.width = `${indicatorWidth}px`; indicator.style.left = `${indicatorLeft}px`; } private updateVerticalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void { const tabsContainer = this.shadowRoot.querySelector('.vertical-wrapper .tabsContainer') as HTMLElement; if (!tabsContainer) return; indicator.style.top = `${tabElement.offsetTop + tabsContainer.offsetTop}px`; indicator.style.height = `${tabElement.clientHeight}px`; } }