mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-25 07:48:54 +00:00
feat(tabs): enhance tab styles and improve active code tab handling (#712)
* feat(tabs): enhance tab styles with sliding indicators and hover effects * feat(tabs): improve active code tab handling and initialization logic
This commit is contained in:
@@ -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"}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user