diff --git a/source/astatic/cyclus.css_t b/source/astatic/cyclus.css_t index 834dd6ba..81b50a2b 100644 --- a/source/astatic/cyclus.css_t +++ b/source/astatic/cyclus.css_t @@ -128,3 +128,92 @@ div.sphinxsidebar ul li a:hover { background: none !important; border-color: transparent !important; } + +/* Sidebar toggle button */ +.sidebar-toggle-button { + position: absolute; + top: 10px; + right: 10px; + z-index: 1000; + width: 25px; + height: 25px; + padding: 0; + margin: 0; + border: 2px solid #4b1a07; + border-radius: 4px; + background-color: #fcf1df; + color: #4b1a07; + font-size: 12px; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all 0.2s ease; +} + +.sidebar-toggle-button:hover { + background-color: #4b1a07; + color: #fcf1df; + box-shadow: 0 2px 6px rgba(75, 26, 7, 0.4); +} + +.sidebar-toggle-button:active { + transform: scale(0.95); +} + +.sidebar-toggle-button:focus { + outline: 2px solid #bb3f3f; + outline-offset: 2px; +} + +/* Button when collapsed */ +.sidebar-toggle-button.sidebar-toggle-collapsed { + position: absolute !important; + display: flex !important; + visibility: visible !important; + opacity: 1 !important; + z-index: 1000 !important; + transition: none !important; /* No animation when moving */ +} + +/* Custom tooltip */ +.sidebar-toggle-button::after { + content: attr(title); + position: absolute; + bottom: 100%; + margin-bottom: 5px; + padding: 3px 6px; + background-color: #34312e; + color: #fcf1df; + font-size: 11px; + white-space: nowrap; + border-radius: 3px; + opacity: 0; + pointer-events: none; + transition: opacity 0.1s ease; + z-index: 1001; +} + +/* Tooltip when sidebar is expanded (center-aligned) */ +.sidebar-toggle-button::after { + left: 50%; + transform: translateX(-50%); /* Center align */ +} + +/* Tooltip when sidebar is collapsed (left-aligned) */ +.sidebar-toggle-button.sidebar-toggle-collapsed::after { + left: 0; + transform: none; /* Remove centering, align to left */ +} + +.sidebar-toggle-button:hover::after { + opacity: 1; +} + +/* Bodywrapper when sidebar is collapsed */ +body.sidebar-collapsed-body .bodywrapper { + background-color: white !important; + position: relative; +} diff --git a/source/astatic/sidebar-toggle.js b/source/astatic/sidebar-toggle.js new file mode 100644 index 00000000..5d8c2c3c --- /dev/null +++ b/source/astatic/sidebar-toggle.js @@ -0,0 +1,250 @@ +// Sidebar collapse functionality for Cyclus documentation +(function() { + 'use strict'; + + const COLLAPSE_THRESHOLD = 0.55; // Auto-collapse when window width <= 55% of screen width + const STORAGE_KEY = 'cyclus-sidebar-collapsed'; + + let sidebar = null; + let toggleButton = null; + + function init() { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupSidebarToggle); + } else { + setupSidebarToggle(); + } + } + + function setupSidebarToggle() { + // Find the sidebar element + sidebar = document.querySelector('.sphinxsidebar'); + if (!sidebar) { + console.warn('Sidebar element not found'); + return; + } + + // Create toggle button + toggleButton = document.createElement('button'); + toggleButton.id = 'sidebar-toggle-btn'; + toggleButton.className = 'sidebar-toggle-button'; + toggleButton.setAttribute('aria-label', 'Toggle sidebar'); + toggleButton.setAttribute('title', 'Collapse sidebar'); + toggleButton.innerHTML = '◀'; + + // Initially place button in sidebar (top-right) + sidebar.style.position = 'relative'; + sidebar.appendChild(toggleButton); + + // Check initial state + const wasCollapsed = localStorage.getItem(STORAGE_KEY) === 'true'; + const shouldAutoCollapse = checkAutoCollapse(); + + if (wasCollapsed || shouldAutoCollapse) { + collapseSidebar(true); + } + + // Button click handler + toggleButton.addEventListener('click', function() { + const isCollapsed = sidebar.style.display === 'none'; + if (isCollapsed) { + expandSidebar(); + } else { + collapseSidebar(false); + } + }); + + // Window resize handler for auto-collapse and button repositioning + let resizeTimeout; + window.addEventListener('resize', function() { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(function() { + const shouldCollapse = checkAutoCollapse(); + const isCollapsed = sidebar.style.display === 'none'; + + // If sidebar is collapsed, update button position and bodywrapper width + if (isCollapsed && toggleButton.classList.contains('sidebar-toggle-collapsed')) { + // Recalculate bodywrapper width to match navbar + const bodyWrapper = document.querySelector('.bodywrapper'); + if (bodyWrapper) { + const relatedNav = document.querySelector('.related'); + if (relatedNav) { + const navRect = relatedNav.getBoundingClientRect(); + const docWrapper = document.querySelector('.documentwrapper'); + const docLeft = docWrapper ? docWrapper.getBoundingClientRect().left : 0; + + const relativeNavLeft = navRect.left - docLeft; + const navWidth = navRect.right - navRect.left; + + bodyWrapper.style.marginLeft = relativeNavLeft + 'px'; + bodyWrapper.style.width = navWidth + 'px'; + bodyWrapper.offsetHeight; // Force reflow + } + } + updateCollapsedButtonPosition(); + } + + if (shouldCollapse && !isCollapsed) { + collapseSidebar(true); + } else if (!shouldCollapse && isCollapsed && localStorage.getItem(STORAGE_KEY) !== 'true') { + expandSidebar(); + } + }, 150); + }); + } + + function checkAutoCollapse() { + const windowWidth = window.innerWidth || document.documentElement.clientWidth; + const screenWidth = screen.width; + return windowWidth <= (screenWidth * COLLAPSE_THRESHOLD); + } + + function updateCollapsedButtonPosition(targetTop) { + // If targetTop not provided, maintain current vertical position + if (targetTop === undefined) { + const rect = toggleButton.getBoundingClientRect(); + targetTop = rect.top; + + // Ensure button is below navbar + const relatedNav = document.querySelector('.related'); + if (relatedNav) { + const navBottom = relatedNav.getBoundingClientRect().bottom; + if (targetTop < navBottom + 10) { + targetTop = navBottom + 10; + } + } + } + + // Get bodywrapper's current position + const bodyWrapper = document.querySelector('.bodywrapper'); + let targetLeft = '10px'; + if (bodyWrapper) { + // Force layout recalculation to get updated position + bodyWrapper.offsetHeight; + const bodyRect = bodyWrapper.getBoundingClientRect(); + targetLeft = (bodyRect.left + 10) + 'px'; // 10px padding from left edge + } + + // Disable transition for instant positioning + toggleButton.style.transition = 'none'; + toggleButton.style.top = targetTop + 'px'; + toggleButton.style.left = targetLeft; + + // Re-enable transition after a brief moment (for hover effects) + setTimeout(function() { + toggleButton.style.transition = ''; + }, 10); + } + + function collapseSidebar(isAutoCollapse) { + if (!sidebar || !toggleButton) return; + + // Hide sidebar + sidebar.style.display = 'none'; + + // Move button to body first + if (sidebar.contains(toggleButton)) { + document.body.appendChild(toggleButton); + } + + toggleButton.classList.add('sidebar-toggle-collapsed'); + toggleButton.style.position = 'fixed'; + + // Adjust bodywrapper to match navbar width FIRST + const bodyWrapper = document.querySelector('.bodywrapper'); + if (bodyWrapper) { + const relatedNav = document.querySelector('.related'); + if (relatedNav) { + const navRect = relatedNav.getBoundingClientRect(); + const navStyles = window.getComputedStyle(relatedNav); + const docWrapper = document.querySelector('.documentwrapper'); + const docLeft = docWrapper ? docWrapper.getBoundingClientRect().left : 0; + + // Calculate relative position + const relativeNavLeft = navRect.left - docLeft; + const navWidth = navRect.right - navRect.left; + + bodyWrapper.style.marginLeft = relativeNavLeft + 'px'; + bodyWrapper.style.width = navWidth + 'px'; + bodyWrapper.style.backgroundColor = 'white'; + // No padding needed - button is fixed and won't scroll with content + bodyWrapper.classList.add('sidebar-collapsed-body'); + + // Force layout recalculation + bodyWrapper.offsetHeight; + } + } + + // NOW position button AFTER bodywrapper has been repositioned + // Get button's current vertical position + const rect = toggleButton.getBoundingClientRect(); + let targetTop = rect.top; + + // Ensure button is below navbar + const relatedNav = document.querySelector('.related'); + if (relatedNav) { + const navBottom = relatedNav.getBoundingClientRect().bottom; + if (targetTop < navBottom + 10) { + targetTop = navBottom + 10; + } + } + + // Position button at bodywrapper's left edge with a bit of breathing room + // (Called after bodywrapper is repositioned) + updateCollapsedButtonPosition(targetTop); + + toggleButton.innerHTML = '▶'; + toggleButton.setAttribute('title', 'Expand sidebar'); + + // Store state + if (!isAutoCollapse) { + localStorage.setItem(STORAGE_KEY, 'true'); + } + + document.body.classList.add('sidebar-collapsed-body'); + } + + function expandSidebar() { + if (!sidebar || !toggleButton) return; + + // Show sidebar + sidebar.style.display = ''; + + // Move button back to sidebar + if (document.body.contains(toggleButton)) { + sidebar.appendChild(toggleButton); + } + + toggleButton.classList.remove('sidebar-toggle-collapsed'); + toggleButton.style.position = 'absolute'; + + // Disable transition for instant positioning + toggleButton.style.transition = 'none'; + toggleButton.style.top = '10px'; + toggleButton.style.right = '10px'; + toggleButton.style.left = 'auto'; + toggleButton.innerHTML = '◀'; + toggleButton.setAttribute('title', 'Collapse sidebar'); + + // Re-enable transition after a brief moment (for hover effects) + setTimeout(function() { + toggleButton.style.transition = ''; + }, 10); + + // Restore bodywrapper + const bodyWrapper = document.querySelector('.bodywrapper'); + if (bodyWrapper) { + bodyWrapper.style.marginLeft = ''; + bodyWrapper.style.width = ''; + bodyWrapper.style.backgroundColor = ''; + bodyWrapper.classList.remove('sidebar-collapsed-body'); + } + + // Clear state + localStorage.removeItem(STORAGE_KEY); + document.body.classList.remove('sidebar-collapsed-body'); + } + + init(); +})(); + diff --git a/source/atemplates/layout.html b/source/atemplates/layout.html new file mode 100644 index 00000000..4e012719 --- /dev/null +++ b/source/atemplates/layout.html @@ -0,0 +1,8 @@ +{# Extend the cloud theme's layout and add sidebar toggle script #} +{% extends "!layout.html" %} + +{% block extrahead %} +{{ super() }} + +{% endblock %} +