2025-06-17 08:41:36 +00:00
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 {
2025-06-27 22:55:20 +00:00
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 `
< div style = "padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};" >
$ { text }
2025-06-27 22:47:24 +00:00
< / div >
2025-06-27 22:55:20 +00:00
` ;
return html `
< style >
. demo - container {
display : flex ;
flex - direction : column ;
gap : 32px ;
padding : 48px ;
background : $ { cssManager . bdTheme ( '#f8f9fa' , '#0a0a0a' ) } ;
min - height : 100vh ;
}
. section {
background : $ { cssManager . bdTheme ( '#ffffff' , '#18181b' ) } ;
border : 1px solid $ { cssManager . bdTheme ( '#e5e7eb' , '#27272a' ) } ;
border - radius : 8px ;
padding : 24px ;
box - shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. section - title {
font - size : 18px ;
font - weight : 600 ;
margin - bottom : 16px ;
color : $ { cssManager . bdTheme ( '#09090b' , '#fafafa' ) } ;
}
. two - column {
display : grid ;
grid - template - columns : 200px 1 fr ;
gap : 24px ;
align - items : start ;
}
< / style >
< 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.' ) }
< / d e e s - a p p u i - t a b s >
2025-06-27 22:47:24 +00:00
< / div >
2025-06-27 22:55:20 +00:00
< div class = "section" >
< div class = "section-title" > Vertical Tabs Layout < / div >
< div class = "two-column" >
< dees - appui - tabs .tabStyle = $ { 'vertical' } .tabs = $ { verticalTabs } > < / d e e s - a p p u i - t a b s >
$ { demoContent ( 'Vertical tabs work great for settings pages and navigation menus. The animated indicator smoothly transitions between selections.' ) }
2025-06-27 22:47:24 +00:00
< / div >
2025-06-27 22:55:20 +00:00
< / div >
< 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.' ) }
< / d e e s - a p p u i - t a b s >
< / div >
2025-06-27 22:47:24 +00:00
< / div >
2025-06-27 22:55:20 +00:00
` ;
} ;
2025-06-17 08:41:36 +00:00
// 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 ;
2025-06-27 22:47:24 +00:00
}
. tabs - wrapper . horizontal - wrapper {
border - bottom : 1px solid $ { cssManager . bdTheme ( '#e5e7eb' , '#27272a' ) } ;
2025-06-17 08:41:36 +00:00
}
. tabsContainer {
position : relative ;
user - select : none ;
}
. tabsContainer . horizontal {
2025-06-27 22:47:24 +00:00
display : flex ;
align - items : center ;
2025-06-17 08:41:36 +00:00
font - size : 14px ;
2025-06-27 22:47:24 +00:00
overflow - x : auto ;
scrollbar - width : none ;
height : 48px ;
padding : 0 16 px ;
gap : 4px ;
}
. tabsContainer . horizontal : : - webkit - scrollbar {
display : none ;
2025-06-17 08:41:36 +00:00
}
. tabsContainer . vertical {
display : flex ;
flex - direction : column ;
2025-06-27 22:47:24 +00:00
padding : 8px ;
2025-06-17 08:41:36 +00:00
font - size : 14px ;
2025-06-27 22:47:24 +00:00
gap : 2px ;
position : relative ;
background : $ { cssManager . bdTheme ( '#f9fafb' , '#18181b' ) } ;
border - radius : 8px ;
2025-06-17 08:41:36 +00:00
}
. tab {
2025-06-27 22:47:24 +00:00
color : $ { cssManager . bdTheme ( '#71717a' , '#71717a' ) } ;
2025-06-17 08:41:36 +00:00
white - space : nowrap ;
2025-06-27 22:47:24 +00:00
cursor : pointer ;
transition : color 0.15 s ease ;
font - weight : 500 ;
position : relative ;
z - index : 2 ;
2025-06-17 08:41:36 +00:00
}
. horizontal . tab {
2025-06-27 22:47:24 +00:00
padding : 0 16 px ;
height : 100 % ;
display : inline - flex ;
align - items : center ;
gap : 8px ;
position : relative ;
border - radius : 6px 6 px 0 0 ;
transition : background - color 0.15 s ease ;
}
. horizontal . tab :not ( : last - child ) : : after {
content : '' ;
position : absolute ;
right : - 2 px ;
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 ;
2025-06-17 08:41:36 +00:00
}
. vertical . tab {
2025-06-27 22:47:24 +00:00
padding : 10px 16 px ;
border - radius : 6px ;
2025-06-17 08:41:36 +00:00
width : 100 % ;
display : flex ;
align - items : center ;
gap : 8px ;
2025-06-27 22:47:24 +00:00
transition : all 0.15 s ease ;
2025-06-17 08:41:36 +00:00
}
. tab :hover {
2025-06-27 22:47:24 +00:00
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 ;
2025-06-17 08:41:36 +00:00
}
. vertical . tab :hover {
2025-06-27 22:47:24 +00:00
background : $ { cssManager . bdTheme ( 'rgba(244, 244, 245, 0.5)' , 'rgba(39, 39, 42, 0.5)' ) } ;
2025-06-17 08:41:36 +00:00
}
2025-06-27 22:47:24 +00:00
. horizontal . tab . selectedTab {
color : $ { cssManager . bdTheme ( '#09090b' , '#fafafa' ) } ;
}
. horizontal . tab . selectedTab : : after ,
. horizontal . tab . selectedTab + . tab : : after {
opacity : 0 ;
2025-06-17 08:41:36 +00:00
}
. vertical . tab . selectedTab {
2025-06-27 22:47:24 +00:00
color : $ { cssManager . bdTheme ( '#09090b' , '#fafafa' ) } ;
2025-06-17 08:41:36 +00:00
}
. tab dees - icon {
font - size : 16px ;
}
2025-06-27 22:47:24 +00:00
. tabIndicator {
2025-06-17 08:41:36 +00:00
position : absolute ;
2025-06-27 22:47:24 +00:00
transition : all 0.3 s 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 3 px 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 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
2025-06-17 08:41:36 +00:00
}
. content {
2025-06-27 22:47:24 +00:00
padding : 32px 24 px ;
2025-06-17 08:41:36 +00:00
}
` ,
] ;
public render ( ) : TemplateResult {
return html `
2025-06-27 22:47:24 +00:00
$ { this . renderTabsWrapper ( ) }
2025-06-17 08:41:36 +00:00
< div class = "content" >
< slot > < / slot >
< / div >
` ;
}
2025-06-27 22:47:24 +00:00
private renderTabsWrapper ( ) : TemplateResult {
const isHorizontal = this . tabStyle === 'horizontal' ;
const wrapperClass = isHorizontal ? 'tabs-wrapper horizontal-wrapper' : 'vertical-wrapper' ;
const containerClass = ` tabsContainer ${ this . tabStyle } ` ;
return html `
< div class = "${wrapperClass}" >
< div class = "${containerClass}" >
$ { this . tabs . map ( tab = > this . renderTab ( tab , isHorizontal ) ) }
< / div >
$ { this . showTabIndicator ? html ` <div class="tabIndicator"></div> ` : '' }
< / div >
` ;
}
private renderTab ( tab : interfaces.ITab , isHorizontal : boolean ) : TemplateResult {
const isSelected = tab === this . selectedTab ;
const classes = ` tab ${ isSelected ? 'selectedTab' : '' } ` ;
const content = isHorizontal ? html `
< span class = "tab-content" >
$ { this . renderTabIcon ( tab ) }
$ { tab . key }
< / span >
` : html `
$ { this . renderTabIcon ( tab ) }
$ { tab . key }
` ;
return html `
< div
class = "${classes}"
@click = "${() => this.selectTab(tab)}"
>
$ { content }
< / div >
` ;
}
private renderTabIcon ( tab : interfaces.ITab ) : TemplateResult | '' {
return tab . iconName ? html ` <dees-icon .icon= ${ tab . iconName } ></dees-icon> ` : '' ;
}
2025-06-17 08:41:36 +00:00
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 < 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' ) ) {
2025-06-27 22:47:24 +00:00
await this . updateComplete ;
// Wait for fonts to load on first update
if ( ! this . indicatorInitialized && document . fonts ) {
await document . fonts . ready ;
}
requestAnimationFrame ( ( ) = > {
this . updateTabIndicator ( ) ;
} ) ;
2025-06-17 08:41:36 +00:00
}
}
2025-06-27 22:47:24 +00:00
private indicatorInitialized = false ;
private updateTabIndicator() {
2025-06-27 22:55:20 +00:00
if ( ! this . shouldShowIndicator ( ) ) return ;
2025-06-27 22:47:24 +00:00
2025-06-27 22:55:20 +00:00
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 ) ;
2025-06-27 22:47:24 +00:00
}
2025-06-27 22:55:20 +00:00
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
2025-06-27 22:47:24 +00:00
? ` .tabs-wrapper .tabsContainer .tab:nth-child( ${ selectedIndex + 1 } ) `
: ` .vertical-wrapper .tabsContainer .tab:nth-child( ${ selectedIndex + 1 } ) ` ;
2025-06-27 22:55:20 +00:00
return this . shadowRoot . querySelector ( selector ) ;
}
2025-06-27 22:47:24 +00:00
2025-06-27 22:55:20 +00:00
private getIndicatorElement ( ) : HTMLElement | null {
return this . shadowRoot . querySelector ( '.tabIndicator' ) ;
}
2025-06-27 22:47:24 +00:00
2025-06-27 22:55:20 +00:00
private handleInitialTransition ( indicator : HTMLElement ) : void {
2025-06-27 22:47:24 +00:00
if ( ! this . indicatorInitialized ) {
indicator . classList . add ( 'no-transition' ) ;
this . indicatorInitialized = true ;
setTimeout ( ( ) = > {
indicator . classList . remove ( 'no-transition' ) ;
} , 50 ) ;
}
2025-06-27 22:55:20 +00:00
}
2025-06-27 22:47:24 +00:00
2025-06-27 22:55:20 +00:00
private updateHorizontalIndicator ( indicator : HTMLElement , tabElement : HTMLElement ) : void {
const tabContent = tabElement . querySelector ( '.tab-content' ) as HTMLElement ;
if ( ! tabContent ) return ;
2025-06-27 22:47:24 +00:00
2025-06-27 22:55:20 +00:00
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 ` ;
2025-06-27 22:47:24 +00:00
}
2025-06-17 08:41:36 +00:00
}