Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/en/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,12 @@
**** xref:develop:view_hooks.adoc[View Hooks]
**** xref:develop:view_models_aka_cells.adoc[View Models (Cells)]
** xref:develop:api/index.adoc[API]
*** xref:develop:api/proposals/mutations.adoc[Proposals]
*** xref:develop:api/authentication.adoc[Authentication]
*** xref:develop:api/core-concepts.adoc[Core concepts]
*** API reference by module
**** Spaces
**** Components
***** xref:develop:api/reference/components/proposals.adoc[Proposals]

* Understand
** xref:understand:about.adoc[About]
Expand Down
87 changes: 87 additions & 0 deletions supplemental_ui/css/tabs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* Generic Tabs Component for Antora Documentation */

.tabs {
margin: 1.5rem 0;
}

.tabs-nav {
display: flex;
gap: 8px;
margin-bottom: 0;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 2px;
flex-wrap: wrap;
}

.tabs-nav__button {
padding: 8px 16px;
background: none;
border: none;
color: #718096;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
border-radius: 6px 6px 0 0;
position: relative;
font-family: inherit;
}

.tabs-nav__button:hover {
color: #667eea;
background: #f7fafc;
}

.tabs-nav__button.is-active {
color: #667eea;
background: #edf2f7;
}

.tabs-nav__button.is-active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #667eea;
}

.tabs-content {
position: relative;
}

.tabs-content__panel {
display: none;
}

.tabs-content__panel.is-active {
display: block;
animation: fadeIn 0.3s ease-in;
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

/* Ensure proper spacing for nested content */
.tabs .tabs-content__panel > *:first-child {
margin-top: 0;
}

.tabs .tabs-content__panel > *:last-child {
margin-bottom: 0;
}

/* Specific styling for code blocks in tabs */
.tabs-content .listingblock {
margin-top: 0;
margin-bottom: 0;
}
156 changes: 156 additions & 0 deletions supplemental_ui/js/tabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Generic Tabs Component for Antora Documentation
* Handles tabbed content with preference persistence
*/

(function() {
'use strict';

// Constants
const SELECTORS = {
TABS_BLOCK: '.tabs',
TABS_NAV: '.tabs-nav',
TAB_BUTTON: '.tabs-nav__button',
TABS_CONTENT: '.tabs-content',
TAB_PANEL: '.tabs-content__panel'
};

const CLASSES = {
ACTIVE: 'is-active'
};

const STORAGE_PREFIX = 'tabs-preference-';

/**
* Storage utilities for tab preferences
*/
const Storage = {
get(key) {
try {
return localStorage.getItem(STORAGE_PREFIX + key);
} catch (e) {
return null;
}
},

set(key, value) {
try {
localStorage.setItem(STORAGE_PREFIX + key, value);
} catch (e) {
// Silently fail if localStorage is not available
}
}
};

/**
* Tab controller for a single tab group
*/
class TabGroup {
constructor(element) {
this.element = element;
this.groupId = element.dataset.tabGroup;
this.nav = element.querySelector(SELECTORS.TABS_NAV);
this.content = element.querySelector(SELECTORS.TABS_CONTENT);

if (!this.nav || !this.content) {
console.warn('Tabs component missing required elements', element);
return;
}

this.buttons = Array.from(this.nav.querySelectorAll(SELECTORS.TAB_BUTTON));
this.panels = Array.from(this.content.querySelectorAll(SELECTORS.TAB_PANEL));

this.init();
}

init() {
this.attachEventListeners();
this.restorePreference();
}

attachEventListeners() {
this.buttons.forEach(button => {
button.addEventListener('click', () => this.handleTabClick(button));
});
}

handleTabClick(clickedButton) {
const tabId = clickedButton.dataset.tab;
this.activateTab(tabId);
this.savePreference(tabId);
}

activateTab(tabId) {
this.deactivateAllTabs();
this.activateButton(tabId);
this.activatePanel(tabId);
}

deactivateAllTabs() {
this.buttons.forEach(button => {
button.classList.remove(CLASSES.ACTIVE);
});
this.panels.forEach(panel => {
panel.classList.remove(CLASSES.ACTIVE);
});
}

activateButton(tabId) {
const button = this.findButtonByTabId(tabId);
if (button) {
button.classList.add(CLASSES.ACTIVE);
}
}

activatePanel(tabId) {
const panel = this.findPanelByTabId(tabId);
if (panel) {
panel.classList.add(CLASSES.ACTIVE);
}
}

findButtonByTabId(tabId) {
return this.buttons.find(button => button.dataset.tab === tabId);
}

findPanelByTabId(tabId) {
return this.panels.find(panel => panel.dataset.tab === tabId);
}

savePreference(tabId) {
if (this.groupId) {
Storage.set(this.groupId, tabId);
}
}

restorePreference() {
if (!this.groupId) return;

const savedTab = Storage.get(this.groupId);
if (savedTab && this.findButtonByTabId(savedTab)) {
this.activateTab(savedTab);
}
}
}

/**
* Initialize all tab groups on the page
*/
function initializeTabs() {
const tabElements = document.querySelectorAll(SELECTORS.TABS_BLOCK);
tabElements.forEach(element => new TabGroup(element));
}

/**
* Bootstrap the tabs when DOM is ready
*/
function bootstrap() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeTabs);
} else {
initializeTabs();
}
}

bootstrap();
})();
5 changes: 4 additions & 1 deletion supplemental_ui/partials/head-meta.hbs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<link rel="stylesheet" href="{{{uiRootPath}}}/css/styles.css">
<link rel="stylesheet" href="{{uiRootPath}}/css/icons.css">
<link rel="stylesheet" href="{{{uiRootPath}}}/css/icons.css">
<link rel="stylesheet" href="{{{uiRootPath}}}/css/medium-zoom.css">

<link rel="stylesheet" href="{{{uiRootPath}}}/css/tabs.css">
<script src="{{{uiRootPath}}}/js/tabs.js"></script>