diff --git a/docs/en/modules/ROOT/nav.adoc b/docs/en/modules/ROOT/nav.adoc index 627ebf19d1..91dd3e8103 100644 --- a/docs/en/modules/ROOT/nav.adoc +++ b/docs/en/modules/ROOT/nav.adoc @@ -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] diff --git a/supplemental_ui/css/tabs.css b/supplemental_ui/css/tabs.css new file mode 100644 index 0000000000..fa97096e3c --- /dev/null +++ b/supplemental_ui/css/tabs.css @@ -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; +} diff --git a/supplemental_ui/js/tabs.js b/supplemental_ui/js/tabs.js new file mode 100644 index 0000000000..e2fff50445 --- /dev/null +++ b/supplemental_ui/js/tabs.js @@ -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(); +})(); diff --git a/supplemental_ui/partials/head-meta.hbs b/supplemental_ui/partials/head-meta.hbs index a577b9dd8c..ed7bb88b66 100644 --- a/supplemental_ui/partials/head-meta.hbs +++ b/supplemental_ui/partials/head-meta.hbs @@ -1,3 +1,6 @@ - + + + +