This commit is contained in:
Juergen Kunz
2025-06-27 22:47:24 +00:00
parent 46065b2424
commit 7b6c135cd3
4 changed files with 543 additions and 113 deletions

View File

@ -1,5 +1,18 @@
# Changelog
## 2025-06-27 - 1.10.8 - feat(ui-components)
Update multiple components with shadcn-aligned styling and improved animations
- Updated dees-modal with shadcn colors, borders, and subtle shadows
- Updated dees-chips with shadcn styling and fixed selection logic bug
- Updated dees-dataview-codebox with shadcn syntax highlighting colors and responsive label layout
- Updated dees-input-multitoggle with transparent blue indicator and smooth animations
- Updated dees-appui-tabs with animated sliding indicator for both horizontal and vertical layouts
- Fixed indicator positioning to be perfectly centered on tab content
- Indicator width is content width + 8px for minimal visual padding
- Adjusted tab spacing to start more to the left with tighter padding
- Improved overall spacing and visual consistency across components
## 2025-06-27 - 1.10.1 - fix(modal)
Improve modal overscroll behavior by adding 'overscroll-behavior: contain' to content container

72
test-output.log Normal file
View File

@ -0,0 +1,72 @@
> @design.estate/dees-catalog@1.10.8 test /mnt/data/lossless/design.estate/dees-catalog
> tstest test/ --web --verbose --timeout 30 --logfile test/test.tabs-indicator.browser.ts

🔍 Test Discovery
 Mode: file
 Pattern: test/test.tabs-indicator.browser.ts
 Found: 1 test file(s)

▶️ test/test.tabs-indicator.browser.ts (1/1)
 Runtime: chromium
running spawned compilation process
=======> ESBUILD
{
cwd: '/mnt/data/lossless/design.estate/dees-catalog',
from: 'test/test.tabs-indicator.browser.ts',
to: '/mnt/data/lossless/design.estate/dees-catalog/.nogit/tstest_cache/test__test.tabs-indicator.browser.ts.js',
mode: 'test',
argv: { bundler: 'esbuild' }
}
switched to /mnt/data/lossless/design.estate/dees-catalog
building for test:
Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy
"/test" maps to 1 handlers
-> GET
"*" maps to 1 handlers
-> GET
now listening on 3007!
Launching puppeteer browser with arguments:
[]
Using executable: /usr/bin/google-chrome
added connection. now 1 sockets connected.
added connection. now 2 sockets connected.
connection ended
removed connection. 1 sockets remaining.
connection ended
removed connection. 0 sockets remaining.
added connection. now 1 sockets connected.
/favicon.ico
could not resolve /mnt/data/lossless/design.estate/dees-catalog/.nogit/tstest_cache/favicon.ico
/test__test.tabs-indicator.browser.ts.js
 Test starting: tabs indicator positioning debug
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 Using globalThis.tapPromise
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
connection ended
removed connection. 0 sockets remaining.
=>  Stopped test/test.tabs-indicator.browser.ts chromium instance and server.

⚠️ Error
 Only 0 out of 1 completed!

⚠️ Error
 The amount of received tests and expectedTests is unequal! Therefore the testfile failed
 Summary: -1 passed, 1 failed of 0 tests in 2.7s

📊 Test Summary
┌────────────────────────────────┐
│ Total Files: 1 │
│ Total Tests: 0 │
│ Passed: 0 │
│ Failed: 0 │
│ Duration: 4.2s │
└────────────────────────────────┘

⏱️ Performance Metrics:
 Average per test: 0ms

ALL TESTS PASSED! 🎉
Exited NOT OK!
ELIFECYCLE Test failed. See above for more details.

View File

@ -0,0 +1,146 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
tap.test('tabs indicator positioning - detailed measurements', async () => {
// Create tabs element with different length labels
const tabsElement = new deesCatalog.DeesAppuiTabs();
tabsElement.tabs = [
{ key: 'Home', iconName: 'lucide:home', action: () => {} },
{ key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => {} },
{ key: 'User Settings', iconName: 'lucide:settings', action: () => {} },
];
document.body.appendChild(tabsElement);
await tabsElement.updateComplete;
// Wait for fonts and indicator initialization
await new Promise(resolve => setTimeout(resolve, 200));
// Get all elements
const shadowRoot = tabsElement.shadowRoot;
const wrapper = shadowRoot.querySelector('.tabs-wrapper') as HTMLElement;
const container = shadowRoot.querySelector('.tabsContainer') as HTMLElement;
const tabs = shadowRoot.querySelectorAll('.tab');
const firstTab = tabs[0] as HTMLElement;
const firstContent = firstTab.querySelector('.tab-content') as HTMLElement;
const indicator = shadowRoot.querySelector('.tabIndicator') as HTMLElement;
// Verify all elements exist
expect(wrapper).toBeInstanceOf(HTMLElement);
expect(container).toBeInstanceOf(HTMLElement);
expect(firstTab).toBeInstanceOf(HTMLElement);
expect(firstContent).toBeInstanceOf(HTMLElement);
expect(indicator).toBeInstanceOf(HTMLElement);
// Get all measurements
const wrapperRect = wrapper.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const tabRect = firstTab.getBoundingClientRect();
const contentRect = firstContent.getBoundingClientRect();
const indicatorRect = indicator.getBoundingClientRect();
console.log('\n=== DETAILED MEASUREMENTS ===');
console.log('Document body left:', document.body.getBoundingClientRect().left);
console.log('Wrapper left:', wrapperRect.left);
console.log('Container left:', containerRect.left);
console.log('Tab left:', tabRect.left);
console.log('Content left:', contentRect.left);
console.log('Indicator left (actual):', indicatorRect.left);
console.log('\n=== RELATIVE POSITIONS ===');
console.log('Container padding (container - wrapper):', containerRect.left - wrapperRect.left);
console.log('Tab position in container:', tabRect.left - containerRect.left);
console.log('Content position in tab:', contentRect.left - tabRect.left);
console.log('Content relative to wrapper:', contentRect.left - wrapperRect.left);
console.log('Indicator relative to wrapper (actual):', indicatorRect.left - wrapperRect.left);
console.log('\n=== WIDTHS ===');
console.log('Tab width:', tabRect.width);
console.log('Content width:', contentRect.width);
console.log('Indicator width:', indicatorRect.width);
console.log('\n=== STYLES (what we set) ===');
console.log('Indicator style.left:', indicator.style.left);
console.log('Indicator style.width:', indicator.style.width);
console.log('\n=== CALCULATIONS ===');
const expectedIndicatorLeft = contentRect.left - wrapperRect.left - 4; // We subtract 4 to center
const expectedIndicatorWidth = contentRect.width + 8; // We add 8 in the code
console.log('Expected indicator left:', expectedIndicatorLeft);
console.log('Expected indicator width:', expectedIndicatorWidth);
console.log('Actual indicator left (from style):', parseFloat(indicator.style.left));
console.log('Actual indicator width (from style):', parseFloat(indicator.style.width));
console.log('\n=== VISUAL ALIGNMENT CHECK ===');
const tabCenter = tabRect.left + (tabRect.width / 2);
const contentCenter = contentRect.left + (contentRect.width / 2);
const indicatorCenter = indicatorRect.left + (indicatorRect.width / 2);
console.log('Tab center:', tabCenter);
console.log('Content center:', contentCenter);
console.log('Indicator center:', indicatorCenter);
console.log('Content offset from tab center:', contentCenter - tabCenter);
console.log('Indicator offset from content center:', indicatorCenter - contentCenter);
console.log('Indicator offset from tab center:', indicatorCenter - tabCenter);
console.log('---');
console.log('Indicator extends left of content by:', contentRect.left - indicatorRect.left);
console.log('Indicator extends right of content by:', (indicatorRect.left + indicatorRect.width) - (contentRect.left + contentRect.width));
// Check if icons are rendering
const icon = firstContent.querySelector('dees-icon');
console.log('\n=== ICON CHECK ===');
console.log('Icon element found:', icon ? 'YES' : 'NO');
if (icon) {
const iconRect = icon.getBoundingClientRect();
console.log('Icon width:', iconRect.width);
console.log('Icon height:', iconRect.height);
console.log('Icon visible:', iconRect.width > 0 && iconRect.height > 0 ? 'YES' : 'NO');
}
// Verify indicator is visible
expect(indicator.style.opacity).toEqual('1');
// Verify positioning calculations
expect(parseFloat(indicator.style.left)).toBeCloseTo(expectedIndicatorLeft, 1);
expect(parseFloat(indicator.style.width)).toBeCloseTo(expectedIndicatorWidth, 1);
// Verify visual centering on content (should be perfectly centered)
expect(Math.abs(indicatorCenter - contentCenter)).toBeLessThan(1);
document.body.removeChild(tabsElement);
});
tap.test('tabs indicator should move when tab is clicked', async () => {
// Create tabs element
const tabsElement = new deesCatalog.DeesAppuiTabs();
tabsElement.tabs = [
{ key: 'Home', iconName: 'lucide:home', action: () => {} },
{ key: 'Analytics', iconName: 'lucide:barChart', action: () => {} },
{ key: 'Settings', iconName: 'lucide:settings', action: () => {} },
];
document.body.appendChild(tabsElement);
await tabsElement.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
const shadowRoot = tabsElement.shadowRoot;
const tabs = shadowRoot.querySelectorAll('.tab');
const indicator = shadowRoot.querySelector('.tabIndicator') as HTMLElement;
// Get initial position
const initialLeft = parseFloat(indicator.style.left);
// Click second tab
(tabs[1] as HTMLElement).click();
await tabsElement.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Position should have changed
const newLeft = parseFloat(indicator.style.left);
expect(newLeft).not.toEqual(initialLeft);
expect(newLeft).toBeGreaterThan(initialLeft);
document.body.removeChild(tabsElement);
});
export default tap.start();

View File

@ -15,13 +15,92 @@ import * as domtools from '@design.estate/dees-domtools';
@customElement('dees-appui-tabs')
export class DeesAppuiTabs extends DeesElement {
public static demo = () => html`
<dees-appui-tabs
.tabs=${[
{ key: 'Tab 1', action: () => console.log('Tab 1 clicked') },
{ key: 'Tab 2', action: () => console.log('Tab 2 clicked') },
{ key: 'Tab 3', action: () => console.log('Tab 3 clicked') },
]}
></dees-appui-tabs>
<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 1px 3px 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 1fr;
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=${[
{ 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') },
]}
>
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
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>
</dees-appui-tabs>
</div>
<div class="section">
<div class="section-title">Vertical Tabs Layout</div>
<div class="two-column">
<dees-appui-tabs
.tabStyle=${'vertical'}
.tabs=${[
{ 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') },
]}
></dees-appui-tabs>
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
Vertical tabs work great for settings pages and navigation menus. The animated indicator smoothly transitions between selections.
</div>
</div>
</div>
<div class="section">
<div class="section-title">Without Indicator</div>
<dees-appui-tabs
.showTabIndicator=${false}
.tabs=${[
{ 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') },
]}
>
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
Tabs can also be used without the animated indicator by setting showTabIndicator to false.
</div>
</dees-appui-tabs>
</div>
</div>
`;
// INSTANCE
@ -50,148 +129,217 @@ export class DeesAppuiTabs extends DeesElement {
.tabs-wrapper {
position: relative;
background: ${cssManager.bdTheme('#f5f5f5', '#000000')};
height: 52px;
}
.tabs-wrapper.horizontal-wrapper {
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
}
.tabsContainer {
position: relative;
z-index: 1;
user-select: none;
}
.tabsContainer.horizontal {
display: grid;
padding-top: 20px;
padding-bottom: 0px;
margin-left: 24px;
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: 20px;
padding: 8px;
font-size: 14px;
gap: 2px;
position: relative;
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
border-radius: 8px;
}
.tab {
color: ${cssManager.bdTheme('#666', '#a0a0a0')};
color: ${cssManager.bdTheme('#71717a', '#71717a')};
white-space: nowrap;
cursor: default;
transition: color 0.1s;
cursor: pointer;
transition: color 0.15s ease;
font-weight: 500;
position: relative;
z-index: 2;
}
.horizontal .tab {
margin-right: 30px;
padding-top: 4px;
padding-bottom: 12px;
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;
}
.vertical .tab {
padding: 12px 16px;
margin-bottom: 4px;
border-radius: 4px;
width: 100%;
display: flex;
.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('#000', '#ffffff')};
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(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
background: ${cssManager.bdTheme('rgba(244, 244, 245, 0.5)', 'rgba(39, 39, 42, 0.5)')};
}
.tab.selectedTab {
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
.horizontal .tab.selectedTab {
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.horizontal .tab.selectedTab::after,
.horizontal .tab.selectedTab + .tab::after {
opacity: 0;
}
.vertical .tab.selectedTab {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
color: ${cssManager.bdTheme('#000', '#ffffff')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.tab dees-icon {
font-size: 16px;
}
.tabs-wrapper .tabIndicator {
.tabIndicator {
position: absolute;
z-index: 0;
left: 40px;
bottom: 0px;
height: 40px;
width: 40px;
background: ${cssManager.bdTheme('#ffffff', '#161616')};
transition: all 0.1s;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444444')};
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 0;
}
.tabIndicator.no-transition {
transition: none;
}
.vertical .tabIndicator {
display: 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 {
margin-top: 20px;
padding: 32px 24px;
}
`,
];
public render(): TemplateResult {
return html`
${this.tabStyle === 'horizontal' ? html`
<style>
.tabsContainer.horizontal {
grid-template-columns: repeat(${this.tabs.length}, min-content);
}
</style>
<div class="tabs-wrapper">
<div class="tabsContainer horizontal">
${this.tabs.map((tabArg) => {
return html`
<div
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
@click="${() => this.selectTab(tabArg)}"
>
${tabArg.key}
</div>
`;
})}
</div>
${this.showTabIndicator ? html`
<div class="tabIndicator"></div>
` : ''}
</div>
` : html`
<div class="tabsContainer vertical">
${this.tabs.map((tabArg) => {
return html`
<div
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
@click="${() => this.selectTab(tabArg)}"
>
${tabArg.iconName ? html`<dees-icon .iconName=${tabArg.iconName}></dees-icon>` : ''}
${tabArg.key}
</div>
`;
})}
</div>
`}
${this.renderTabsWrapper()}
<div class="content">
<slot></slot>
</div>
`;
}
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>` : '';
}
private selectTab(tabArg: interfaces.ITab) {
this.selectedTab = tabArg;
this.updateTabIndicator();
tabArg.action();
// Emit tab-select event
@ -202,31 +350,6 @@ export class DeesAppuiTabs extends DeesElement {
}));
}
/**
* updates the indicator position
*/
private updateTabIndicator() {
if (!this.showTabIndicator || this.tabStyle !== 'horizontal' || !this.selectedTab) {
return;
}
const tabIndex = this.tabs.indexOf(this.selectedTab);
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
`.tabs-wrapper .tabsContainer .tab:nth-child(${tabIndex + 1})`
);
if (!selectedTabElement) return;
const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabsContainer');
const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left"));
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabIndicator');
if (tabIndicator) {
tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px';
tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px';
}
}
firstUpdated() {
if (this.tabs && this.tabs.length > 0) {
this.selectTab(this.tabs[0]);
@ -241,7 +364,83 @@ export class DeesAppuiTabs extends DeesElement {
}
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
this.updateTabIndicator();
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.selectedTab || !this.showTabIndicator) {
return;
}
const selectedIndex = this.tabs.indexOf(this.selectedTab);
if (selectedIndex === -1) {
return;
}
// Select the correct tab element using nth-child
const selector = this.tabStyle === 'horizontal'
? `.tabs-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`
: `.vertical-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`;
const selectedTabElement = this.shadowRoot.querySelector(selector) as HTMLElement;
if (!selectedTabElement) {
return;
}
const indicator = this.shadowRoot.querySelector('.tabIndicator') as HTMLElement;
if (!indicator) {
return;
}
// Disable transition for initial positioning
if (!this.indicatorInitialized) {
indicator.classList.add('no-transition');
this.indicatorInitialized = true;
setTimeout(() => {
indicator.classList.remove('no-transition');
}, 50);
}
if (this.tabStyle === 'horizontal') {
const tabContent = selectedTabElement.querySelector('.tab-content') as HTMLElement;
if (tabContent) {
// Use getBoundingClientRect for accurate positioning
const wrapperRect = indicator.parentElement.getBoundingClientRect();
const contentRect = tabContent.getBoundingClientRect();
// Calculate the position relative to wrapper
const contentLeft = contentRect.left - wrapperRect.left;
// Set width to match content plus small padding
const indicatorWidth = contentRect.width + 8;
// Center the indicator on content (shift left by half the extra width)
const indicatorLeft = contentLeft - 4;
indicator.style.width = `${indicatorWidth}px`;
indicator.style.left = `${indicatorLeft}px`;
}
} else {
// For vertical tabs
const tabsContainer = this.shadowRoot.querySelector('.vertical-wrapper .tabsContainer') as HTMLElement;
indicator.style.top = `${selectedTabElement.offsetTop + tabsContainer.offsetTop}px`;
indicator.style.height = `${selectedTabElement.clientHeight}px`;
}
indicator.style.opacity = '1';
}
}