From c13369e252470ce4fe37fc6df74468cc39ce888f Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Fri, 21 Nov 2025 10:34:47 -0600 Subject: [PATCH 1/5] basic implementation of a collapsable sidebar with button and auto half-screen detection --- source/astatic/cyclus.css_t | 79 ++++++++++++ source/astatic/sidebar-toggle.js | 203 +++++++++++++++++++++++++++++++ source/atemplates/layout.html | 8 ++ 3 files changed, 290 insertions(+) create mode 100644 source/astatic/sidebar-toggle.js create mode 100644 source/atemplates/layout.html diff --git a/source/astatic/cyclus.css_t b/source/astatic/cyclus.css_t index 834dd6ba..af023af2 100644 --- a/source/astatic/cyclus.css_t +++ b/source/astatic/cyclus.css_t @@ -128,3 +128,82 @@ 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: 36px; + height: 36px; + padding: 0; + margin: 0; + border: 2px solid #4b1a07; + border-radius: 4px; + background-color: #fcf1df; + color: #4b1a07; + font-size: 18px; + 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: fixed !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%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 5px; + padding: 4px 8px; + background-color: #34312e; + color: #fcf1df; + font-size: 12px; + white-space: nowrap; + border-radius: 3px; + opacity: 0; + pointer-events: none; + transition: opacity 0.1s ease; + z-index: 1001; +} + +.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..6fe10db3 --- /dev/null +++ b/source/astatic/sidebar-toggle.js @@ -0,0 +1,203 @@ +// 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 + let resizeTimeout; + window.addEventListener('resize', function() { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(function() { + const shouldCollapse = checkAutoCollapse(); + const isCollapsed = sidebar.style.display === 'none'; + + 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 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'; + 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 (all the way to the left) + let targetLeft = '0px'; + if (bodyWrapper) { + // Get bodywrapper's NEW position after it's been repositioned + const bodyRect = bodyWrapper.getBoundingClientRect(); + targetLeft = bodyRect.left + 'px'; + } + + // Disable transition for instant positioning + toggleButton.style.transition = 'none'; + toggleButton.style.top = targetTop + 'px'; + toggleButton.style.left = targetLeft; + toggleButton.innerHTML = '▶'; + toggleButton.setAttribute('title', 'Expand sidebar'); + + // Re-enable transition after a brief moment (for hover effects) + setTimeout(function() { + toggleButton.style.transition = ''; + }, 10); + + // 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 %} + From fb21075e75343021a28db47c5906c9ba24224e95 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Fri, 21 Nov 2025 10:43:29 -0600 Subject: [PATCH 2/5] gave the text some space when the sidebar is collapsed so the button doesn't overlap --- source/astatic/sidebar-toggle.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/source/astatic/sidebar-toggle.js b/source/astatic/sidebar-toggle.js index 6fe10db3..d725e0b3 100644 --- a/source/astatic/sidebar-toggle.js +++ b/source/astatic/sidebar-toggle.js @@ -108,6 +108,8 @@ bodyWrapper.style.marginLeft = relativeNavLeft + 'px'; bodyWrapper.style.width = navWidth + 'px'; bodyWrapper.style.backgroundColor = 'white'; + // Add left padding to create space for the button column (button is 36px + 10px padding on each side = 56px) + bodyWrapper.style.paddingLeft = '56px'; bodyWrapper.classList.add('sidebar-collapsed-body'); // Force layout recalculation @@ -129,12 +131,12 @@ } } - // Position button at bodywrapper's left edge (all the way to the left) - let targetLeft = '0px'; + // Position button at bodywrapper's left edge with a bit of breathing room + let targetLeft = '10px'; if (bodyWrapper) { // Get bodywrapper's NEW position after it's been repositioned const bodyRect = bodyWrapper.getBoundingClientRect(); - targetLeft = bodyRect.left + 'px'; + targetLeft = (bodyRect.left + 10) + 'px'; // 10px padding from left edge } // Disable transition for instant positioning @@ -190,6 +192,7 @@ bodyWrapper.style.marginLeft = ''; bodyWrapper.style.width = ''; bodyWrapper.style.backgroundColor = ''; + bodyWrapper.style.paddingLeft = ''; // Remove the padding we added bodyWrapper.classList.remove('sidebar-collapsed-body'); } From 064146475aa19c710f0b59dd912c4102c2fed411 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Fri, 21 Nov 2025 10:52:11 -0600 Subject: [PATCH 3/5] made button smaller and fixed a positioning bug when swithcing between half-screen and full screen --- source/astatic/cyclus.css_t | 8 ++-- source/astatic/sidebar-toggle.js | 65 +++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/source/astatic/cyclus.css_t b/source/astatic/cyclus.css_t index af023af2..ef71bd55 100644 --- a/source/astatic/cyclus.css_t +++ b/source/astatic/cyclus.css_t @@ -135,15 +135,15 @@ div.sphinxsidebar ul li a:hover { top: 10px; right: 10px; z-index: 1000; - width: 36px; - height: 36px; + width: 18px; + height: 18px; padding: 0; margin: 0; border: 2px solid #4b1a07; - border-radius: 4px; + border-radius: 3px; background-color: #fcf1df; color: #4b1a07; - font-size: 18px; + font-size: 10px; font-weight: bold; cursor: pointer; display: flex; diff --git a/source/astatic/sidebar-toggle.js b/source/astatic/sidebar-toggle.js index d725e0b3..d260792e 100644 --- a/source/astatic/sidebar-toggle.js +++ b/source/astatic/sidebar-toggle.js @@ -54,7 +54,7 @@ } }); - // Window resize handler for auto-collapse + // Window resize handler for auto-collapse and button repositioning let resizeTimeout; window.addEventListener('resize', function() { clearTimeout(resizeTimeout); @@ -62,6 +62,11 @@ const shouldCollapse = checkAutoCollapse(); const isCollapsed = sidebar.style.display === 'none'; + // If sidebar is collapsed, update button position + if (isCollapsed && toggleButton.classList.contains('sidebar-toggle-collapsed')) { + updateCollapsedButtonPosition(); + } + if (shouldCollapse && !isCollapsed) { collapseSidebar(true); } else if (!shouldCollapse && isCollapsed && localStorage.getItem(STORAGE_KEY) !== 'true') { @@ -77,6 +82,43 @@ 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; @@ -108,8 +150,8 @@ bodyWrapper.style.marginLeft = relativeNavLeft + 'px'; bodyWrapper.style.width = navWidth + 'px'; bodyWrapper.style.backgroundColor = 'white'; - // Add left padding to create space for the button column (button is 36px + 10px padding on each side = 56px) - bodyWrapper.style.paddingLeft = '56px'; + // Add left padding to create space for the button column (button is 18px + 10px padding on each side = 38px) + bodyWrapper.style.paddingLeft = '38px'; bodyWrapper.classList.add('sidebar-collapsed-body'); // Force layout recalculation @@ -132,25 +174,12 @@ } // Position button at bodywrapper's left edge with a bit of breathing room - let targetLeft = '10px'; - if (bodyWrapper) { - // Get bodywrapper's NEW position after it's been repositioned - const bodyRect = bodyWrapper.getBoundingClientRect(); - targetLeft = (bodyRect.left + 10) + 'px'; // 10px padding from left edge - } + // (Called after bodywrapper is repositioned) + updateCollapsedButtonPosition(targetTop); - // Disable transition for instant positioning - toggleButton.style.transition = 'none'; - toggleButton.style.top = targetTop + 'px'; - toggleButton.style.left = targetLeft; toggleButton.innerHTML = '▶'; toggleButton.setAttribute('title', 'Expand sidebar'); - // Re-enable transition after a brief moment (for hover effects) - setTimeout(function() { - toggleButton.style.transition = ''; - }, 10); - // Store state if (!isAutoCollapse) { localStorage.setItem(STORAGE_KEY, 'true'); From 6bc7148c0ad56499c42d24379b873be31255922d Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Fri, 21 Nov 2025 12:09:40 -0600 Subject: [PATCH 4/5] made button a little bigger, fixed a bug with half-screen to full screen mode, and removed the little scroll column --- source/astatic/cyclus.css_t | 26 ++++++++++++++++++-------- source/astatic/sidebar-toggle.js | 23 +++++++++++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/source/astatic/cyclus.css_t b/source/astatic/cyclus.css_t index ef71bd55..7f7a200b 100644 --- a/source/astatic/cyclus.css_t +++ b/source/astatic/cyclus.css_t @@ -135,15 +135,15 @@ div.sphinxsidebar ul li a:hover { top: 10px; right: 10px; z-index: 1000; - width: 18px; - height: 18px; + width: 25px; + height: 25px; padding: 0; margin: 0; border: 2px solid #4b1a07; - border-radius: 3px; + border-radius: 4px; background-color: #fcf1df; color: #4b1a07; - font-size: 10px; + font-size: 12px; font-weight: bold; cursor: pointer; display: flex; @@ -183,13 +183,11 @@ div.sphinxsidebar ul li a:hover { content: attr(title); position: absolute; bottom: 100%; - left: 50%; - transform: translateX(-50%); margin-bottom: 5px; - padding: 4px 8px; + padding: 3px 6px; background-color: #34312e; color: #fcf1df; - font-size: 12px; + font-size: 11px; white-space: nowrap; border-radius: 3px; opacity: 0; @@ -198,6 +196,18 @@ div.sphinxsidebar ul li a:hover { 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; } diff --git a/source/astatic/sidebar-toggle.js b/source/astatic/sidebar-toggle.js index d260792e..5d8c2c3c 100644 --- a/source/astatic/sidebar-toggle.js +++ b/source/astatic/sidebar-toggle.js @@ -62,8 +62,25 @@ const shouldCollapse = checkAutoCollapse(); const isCollapsed = sidebar.style.display === 'none'; - // If sidebar is collapsed, update button position + // 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(); } @@ -150,8 +167,7 @@ bodyWrapper.style.marginLeft = relativeNavLeft + 'px'; bodyWrapper.style.width = navWidth + 'px'; bodyWrapper.style.backgroundColor = 'white'; - // Add left padding to create space for the button column (button is 18px + 10px padding on each side = 38px) - bodyWrapper.style.paddingLeft = '38px'; + // No padding needed - button is fixed and won't scroll with content bodyWrapper.classList.add('sidebar-collapsed-body'); // Force layout recalculation @@ -221,7 +237,6 @@ bodyWrapper.style.marginLeft = ''; bodyWrapper.style.width = ''; bodyWrapper.style.backgroundColor = ''; - bodyWrapper.style.paddingLeft = ''; // Remove the padding we added bodyWrapper.classList.remove('sidebar-collapsed-body'); } From cee61cb242e378823d1e6d9446491e5dc8fa8599 Mon Sep 17 00:00:00 2001 From: Dean Krueger Date: Fri, 21 Nov 2025 12:15:48 -0600 Subject: [PATCH 5/5] made the button stick to the top of the page instead of scrolling down with the text --- source/astatic/cyclus.css_t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/astatic/cyclus.css_t b/source/astatic/cyclus.css_t index 7f7a200b..81b50a2b 100644 --- a/source/astatic/cyclus.css_t +++ b/source/astatic/cyclus.css_t @@ -170,7 +170,7 @@ div.sphinxsidebar ul li a:hover { /* Button when collapsed */ .sidebar-toggle-button.sidebar-toggle-collapsed { - position: fixed !important; + position: absolute !important; display: flex !important; visibility: visible !important; opacity: 1 !important;