diff --git a/apps/test/content/posts/code-tabs-test.md b/apps/test/content/posts/code-tabs-test.md index 8f3c1e7a..943ed2ee 100644 --- a/apps/test/content/posts/code-tabs-test.md +++ b/apps/test/content/posts/code-tabs-test.md @@ -40,6 +40,19 @@ def sum(a, b): print(sum(1, 2)) ``` +## Active Tab + +```js {group=tab-active name="Inactive Tab"} +function greet(name) { + return `Hello, ${name}!` +} +``` + +```python {group=tab-active name="Active Tab" .active} +def greet(name): + return f"Hello, {name}!" +``` + ## Mode ```ts {group=tab-mode name="Classic"} diff --git a/assets/css/_page/_single/_code.scss b/assets/css/_page/_single/_code.scss index 0cf9b10f..853a18f4 100644 --- a/assets/css/_page/_single/_code.scss +++ b/assets/css/_page/_single/_code.scss @@ -590,27 +590,40 @@ pre { .tabs-items { display: flex; overflow-x: auto; + position: relative; @include border-radius(top); - + .tab-item { padding: 0.4rem 0.8rem; cursor: pointer; font-weight: bold; color: fixit-var(secondary); white-space: nowrap; - transition: all 0.2s; user-select: none; - - &:hover { - color: fixit-var(global-font-color); - background-color: rgba(128, 128, 128, 0.05); - } - - &.active { - color: fixit-var(global-font-color); - box-shadow: inset 0 -2px 0 fixit-var(code-tab-active-border-color, fixit-var(primary)); - background-color: fixit-var(code-block-background-color); - } + @include border-radius; + + // mobile style + // padding-inline: 0.6rem; + } + + &:not(:has(.tab-item:hover)) .tab-item.active, + .tab-item:hover { + anchor-name: #{$rootPrefix}tab-hover; + color: fixit-var(global-font-color); + } + + &::before { + content: ''; + position-anchor: #{$rootPrefix}tab-hover; + position: absolute; + inset: anchor(inside); + translate: 2px 2px; + width: calc(anchor-size(width) - 0.25rem); + height: calc(anchor-size(height) - 0.25rem); + box-shadow: inset 0 0 1rem 0.25rem color-mix(in srgb, fixit-var(secondary) 30%, transparent); + pointer-events: none; + transition: 0.3s ease-in-out; + @include border-radius; } } diff --git a/assets/css/_page/_single/_shortcodes/_tabs.scss b/assets/css/_page/_single/_shortcodes/_tabs.scss index 1a916e8f..00c1a928 100644 --- a/assets/css/_page/_single/_shortcodes/_tabs.scss +++ b/assets/css/_page/_single/_shortcodes/_tabs.scss @@ -107,17 +107,15 @@ tab-container { // ======================================== // Style 1: Underline Style (Default) - // Clean, minimal with bottom border + // Clean, minimal with sliding indicator // ======================================== &[type='underline'] { > .tab-button { - @include set-fixit-var(tab-button-border-color, transparent); @include tab-button-size(0); &[aria-selected='true'], &[aria-selected='false']:hover { color: fixit-var(primary); - @include set-fixit-var(tab-button-border-color, fixit-var(primary)); } &[aria-selected='false']:hover { @@ -125,6 +123,21 @@ tab-container { } } + // Anchor tracking: follows hover, rests on active tab + &:not(:has(> .tab-button:hover)) > .tab-button[aria-selected='true'], + > .tab-button:hover { + anchor-name: #{$rootPrefix}tab-underline-active; + } + + // Sliding box-shadow indicator via anchor positioning + &::part(tablist-tab-wrapper)::before { + content: ''; + position: absolute; + position-anchor: #{$rootPrefix}tab-underline-active; + inset: anchor(inside); + pointer-events: none; + } + &[placement='top'], &[placement='bottom'] { gap: 0.5rem; @@ -137,6 +150,10 @@ tab-container { overflow-x: scroll; overflow-y: hidden; } + + &::part(tablist-tab-wrapper)::before { + transition: all 0.3s ease-in-out, top, bottom 0s; + } } &[placement='left'], @@ -144,17 +161,21 @@ tab-container { > .tab-button { @include tab-button-size(1rem, 0); } + + &::part(tablist-tab-wrapper)::before { + transition: all 0.3s ease-in-out, left, right 0s; + } } // Top placement &[placement='top'] { &::part(tablist-tab-wrapper) { border-bottom: 2px solid fixit-var(global-border-color); - } - > .tab-button { - translate: 0 2px; - border-bottom: 2px solid fixit-var(tab-button-border-color); + &::before { + box-shadow: inset 0 -2px 0 fixit-var(primary); + translate: 0 2px; + } } } @@ -162,11 +183,11 @@ tab-container { &[placement='bottom'] { &::part(tablist-tab-wrapper) { border-top: 2px solid fixit-var(global-border-color); - } - > .tab-button { - translate: 0 -2px; - border-top: 2px solid fixit-var(tab-button-border-color); + &::before { + box-shadow: inset 0 2px 0 fixit-var(primary); + translate: 0 -2px; + } } } @@ -174,12 +195,15 @@ tab-container { &[placement='left'] { &::part(tablist-tab-wrapper) { border-right: 2px solid fixit-var(global-border-color); + + &::before { + box-shadow: inset -2px 0 0 fixit-var(primary); + translate: 2px 0; + } } > .tab-button { text-align: right; - translate: 2px 0; - border-right: 2px solid fixit-var(tab-button-border-color); } } @@ -187,12 +211,15 @@ tab-container { &[placement='right'] { &::part(tablist-tab-wrapper) { border-left: 2px solid fixit-var(global-border-color); + + &::before { + box-shadow: inset 2px 0 0 fixit-var(primary); + translate: -2px 0; + } } > .tab-button { text-align: left; - translate: -2px 0; - border-left: 2px solid fixit-var(tab-button-border-color); } } } @@ -451,8 +478,10 @@ tab-container { } > .tab-button { + position: relative; font-weight: 400; flex-grow: 1; + @include z-index(base); @include tab-button-size; @include border-radius; @@ -463,12 +492,26 @@ tab-container { } &[aria-selected='true'] { - background-color: fixit-var(global-background-color); - box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1); + anchor-name: #{$rootPrefix}tab-segment-active; + background-color: transparent; + box-shadow: none; + } + } - [data-theme=dark] & { - box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1); - } + // Sliding active indicator via anchor positioning + &::part(tablist-tab-wrapper)::before { + content: ''; + position: absolute; + pointer-events: none; + position-anchor: #{$rootPrefix}tab-segment-active; + inset: anchor(inside); + background-color: fixit-var(global-background-color); + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1); + transition: all 0.15s ease, top, bottom 0s; + @include border-radius; + + [data-theme=dark] & { + box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1); } } } @@ -531,3 +574,54 @@ tab-container { tab-container .tab-panel:not([hidden]) { animation: tabSlideIn 0.3s ease-out; } + +// Firefox fallback: `::part(...)::before` is not supported. +// We use a CSS variable to control the border color of the active tab button, +// and apply the sliding effect directly on the button itself. +@supports (-moz-appearance: none) { + tab-container { + &[type='underline'] { + > .tab-button { + @include set-fixit-var(tab-button-border-color, transparent); + + &[aria-selected='true'], + &[aria-selected='false']:hover { + @include set-fixit-var(tab-button-border-color, fixit-var(primary)); + } + } + + &[placement='top'] > .tab-button { + translate: 0 2px; + border-bottom: 2px solid fixit-var(tab-button-border-color); + } + + &[placement='bottom'] > .tab-button { + translate: 0 -2px; + border-top: 2px solid fixit-var(tab-button-border-color); + } + + &[placement='left'] > .tab-button { + translate: 2px 0; + border-right: 2px solid fixit-var(tab-button-border-color); + } + + &[placement='right'] > .tab-button { + translate: -2px 0; + border-left: 2px solid fixit-var(tab-button-border-color); + } + } + + &[type='segment'] { + > .tab-button { + &[aria-selected='true'] { + background-color: fixit-var(global-background-color); + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1); + + [data-theme=dark] & { + box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1); + } + } + } + } + } +} diff --git a/assets/js/theme.js b/assets/js/theme.js index 46c68ceb..6b676e33 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -640,13 +640,15 @@ class FixIt { const $firstBlock = $tabs[0]; $firstBlock.parentNode.insertBefore($container, $firstBlock); + const activeTabIndex = $tabs.findIndex(tab => tab.classList.contains('active')); $tabs.forEach(($tab, index) => { const title = $tab.dataset.tabTitle || 'Code'; + const defaultActiveTab = activeTabIndex === -1 && index === 0; // tab button const $btn = document.createElement('span'); $btn.className = 'tab-item'; - if (index === 0) $btn.classList.add('active'); + if (defaultActiveTab) $btn.classList.add('active'); $btn.textContent = title; $btn.dataset.index = index; $btn.title = title; @@ -678,8 +680,7 @@ class FixIt { $items.appendChild($btn); // move block to content - $tab.classList.toggle('active', index === 0); - // remove is-collapsed class + $tab.classList.toggle('active', activeTabIndex === index || defaultActiveTab); $tab.classList.remove('is-collapsed'); $content.appendChild($tab); }); @@ -687,8 +688,13 @@ class FixIt { $container.appendChild($header); $container.appendChild($content); - // initialize actions for first tab - $items.firstElementChild.click(); + // initialize actions for the active tab + if (activeTabIndex !== -1) { + const $activeBtn = $items.querySelector(`.tab-item[data-index="${activeTabIndex}"]`); + if ($activeBtn) $activeBtn.click(); + } else { + $items.firstElementChild.click(); + } }); }