import * as interfaces from '../../interfaces/index.js'; import { DeesElement, type TemplateResult, property, state, customElement, html, css, cssManager, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { demoFunc } from './dees-appui-tabs.demo.js'; import { themeDefaultStyles } from '../../00theme.js'; @customElement('dees-appui-tabs') export class DeesAppuiTabs extends DeesElement { public static demo = demoFunc; public static demoGroup = 'App UI'; // INSTANCE @property({ type: Array, }) accessor tabs: interfaces.IMenuItem[] = []; @property({ type: Object }) accessor selectedTab: interfaces.IMenuItem | null = null; @property({ type: Boolean }) accessor showTabIndicator: boolean = true; @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, css` /* TODO: Migrate hardcoded values to --dees-* CSS variables */ :host { 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 { display: flex; align-items: center; font-size: 14px; overflow-x: auto; overflow-y: hidden; overscroll-behavior: contain; 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 { 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 { 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); } /* 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()} `; } private renderTabsWrapper(): TemplateResult { const isHorizontal = this.tabStyle === 'horizontal'; const wrapperClass = isHorizontal ? 'tabs-wrapper horizontal-wrapper' : 'vertical-wrapper'; const containerClass = `tabsContainer ${this.tabStyle}`; if (isHorizontal) { return html`