Skip to content

Commit ffa81c7

Browse files
authored
Merge pull request #436 from dean-krueger/collapse-sidebar
Make the Sidebar Collapsible
2 parents 740ff1f + cee61cb commit ffa81c7

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed

source/astatic/cyclus.css_t

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,92 @@ div.sphinxsidebar ul li a:hover {
128128
background: none !important;
129129
border-color: transparent !important;
130130
}
131+
132+
/* Sidebar toggle button */
133+
.sidebar-toggle-button {
134+
position: absolute;
135+
top: 10px;
136+
right: 10px;
137+
z-index: 1000;
138+
width: 25px;
139+
height: 25px;
140+
padding: 0;
141+
margin: 0;
142+
border: 2px solid #4b1a07;
143+
border-radius: 4px;
144+
background-color: #fcf1df;
145+
color: #4b1a07;
146+
font-size: 12px;
147+
font-weight: bold;
148+
cursor: pointer;
149+
display: flex;
150+
align-items: center;
151+
justify-content: center;
152+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
153+
transition: all 0.2s ease;
154+
}
155+
156+
.sidebar-toggle-button:hover {
157+
background-color: #4b1a07;
158+
color: #fcf1df;
159+
box-shadow: 0 2px 6px rgba(75, 26, 7, 0.4);
160+
}
161+
162+
.sidebar-toggle-button:active {
163+
transform: scale(0.95);
164+
}
165+
166+
.sidebar-toggle-button:focus {
167+
outline: 2px solid #bb3f3f;
168+
outline-offset: 2px;
169+
}
170+
171+
/* Button when collapsed */
172+
.sidebar-toggle-button.sidebar-toggle-collapsed {
173+
position: absolute !important;
174+
display: flex !important;
175+
visibility: visible !important;
176+
opacity: 1 !important;
177+
z-index: 1000 !important;
178+
transition: none !important; /* No animation when moving */
179+
}
180+
181+
/* Custom tooltip */
182+
.sidebar-toggle-button::after {
183+
content: attr(title);
184+
position: absolute;
185+
bottom: 100%;
186+
margin-bottom: 5px;
187+
padding: 3px 6px;
188+
background-color: #34312e;
189+
color: #fcf1df;
190+
font-size: 11px;
191+
white-space: nowrap;
192+
border-radius: 3px;
193+
opacity: 0;
194+
pointer-events: none;
195+
transition: opacity 0.1s ease;
196+
z-index: 1001;
197+
}
198+
199+
/* Tooltip when sidebar is expanded (center-aligned) */
200+
.sidebar-toggle-button::after {
201+
left: 50%;
202+
transform: translateX(-50%); /* Center align */
203+
}
204+
205+
/* Tooltip when sidebar is collapsed (left-aligned) */
206+
.sidebar-toggle-button.sidebar-toggle-collapsed::after {
207+
left: 0;
208+
transform: none; /* Remove centering, align to left */
209+
}
210+
211+
.sidebar-toggle-button:hover::after {
212+
opacity: 1;
213+
}
214+
215+
/* Bodywrapper when sidebar is collapsed */
216+
body.sidebar-collapsed-body .bodywrapper {
217+
background-color: white !important;
218+
position: relative;
219+
}

source/astatic/sidebar-toggle.js

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Sidebar collapse functionality for Cyclus documentation
2+
(function() {
3+
'use strict';
4+
5+
const COLLAPSE_THRESHOLD = 0.55; // Auto-collapse when window width <= 55% of screen width
6+
const STORAGE_KEY = 'cyclus-sidebar-collapsed';
7+
8+
let sidebar = null;
9+
let toggleButton = null;
10+
11+
function init() {
12+
if (document.readyState === 'loading') {
13+
document.addEventListener('DOMContentLoaded', setupSidebarToggle);
14+
} else {
15+
setupSidebarToggle();
16+
}
17+
}
18+
19+
function setupSidebarToggle() {
20+
// Find the sidebar element
21+
sidebar = document.querySelector('.sphinxsidebar');
22+
if (!sidebar) {
23+
console.warn('Sidebar element not found');
24+
return;
25+
}
26+
27+
// Create toggle button
28+
toggleButton = document.createElement('button');
29+
toggleButton.id = 'sidebar-toggle-btn';
30+
toggleButton.className = 'sidebar-toggle-button';
31+
toggleButton.setAttribute('aria-label', 'Toggle sidebar');
32+
toggleButton.setAttribute('title', 'Collapse sidebar');
33+
toggleButton.innerHTML = '◀';
34+
35+
// Initially place button in sidebar (top-right)
36+
sidebar.style.position = 'relative';
37+
sidebar.appendChild(toggleButton);
38+
39+
// Check initial state
40+
const wasCollapsed = localStorage.getItem(STORAGE_KEY) === 'true';
41+
const shouldAutoCollapse = checkAutoCollapse();
42+
43+
if (wasCollapsed || shouldAutoCollapse) {
44+
collapseSidebar(true);
45+
}
46+
47+
// Button click handler
48+
toggleButton.addEventListener('click', function() {
49+
const isCollapsed = sidebar.style.display === 'none';
50+
if (isCollapsed) {
51+
expandSidebar();
52+
} else {
53+
collapseSidebar(false);
54+
}
55+
});
56+
57+
// Window resize handler for auto-collapse and button repositioning
58+
let resizeTimeout;
59+
window.addEventListener('resize', function() {
60+
clearTimeout(resizeTimeout);
61+
resizeTimeout = setTimeout(function() {
62+
const shouldCollapse = checkAutoCollapse();
63+
const isCollapsed = sidebar.style.display === 'none';
64+
65+
// If sidebar is collapsed, update button position and bodywrapper width
66+
if (isCollapsed && toggleButton.classList.contains('sidebar-toggle-collapsed')) {
67+
// Recalculate bodywrapper width to match navbar
68+
const bodyWrapper = document.querySelector('.bodywrapper');
69+
if (bodyWrapper) {
70+
const relatedNav = document.querySelector('.related');
71+
if (relatedNav) {
72+
const navRect = relatedNav.getBoundingClientRect();
73+
const docWrapper = document.querySelector('.documentwrapper');
74+
const docLeft = docWrapper ? docWrapper.getBoundingClientRect().left : 0;
75+
76+
const relativeNavLeft = navRect.left - docLeft;
77+
const navWidth = navRect.right - navRect.left;
78+
79+
bodyWrapper.style.marginLeft = relativeNavLeft + 'px';
80+
bodyWrapper.style.width = navWidth + 'px';
81+
bodyWrapper.offsetHeight; // Force reflow
82+
}
83+
}
84+
updateCollapsedButtonPosition();
85+
}
86+
87+
if (shouldCollapse && !isCollapsed) {
88+
collapseSidebar(true);
89+
} else if (!shouldCollapse && isCollapsed && localStorage.getItem(STORAGE_KEY) !== 'true') {
90+
expandSidebar();
91+
}
92+
}, 150);
93+
});
94+
}
95+
96+
function checkAutoCollapse() {
97+
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
98+
const screenWidth = screen.width;
99+
return windowWidth <= (screenWidth * COLLAPSE_THRESHOLD);
100+
}
101+
102+
function updateCollapsedButtonPosition(targetTop) {
103+
// If targetTop not provided, maintain current vertical position
104+
if (targetTop === undefined) {
105+
const rect = toggleButton.getBoundingClientRect();
106+
targetTop = rect.top;
107+
108+
// Ensure button is below navbar
109+
const relatedNav = document.querySelector('.related');
110+
if (relatedNav) {
111+
const navBottom = relatedNav.getBoundingClientRect().bottom;
112+
if (targetTop < navBottom + 10) {
113+
targetTop = navBottom + 10;
114+
}
115+
}
116+
}
117+
118+
// Get bodywrapper's current position
119+
const bodyWrapper = document.querySelector('.bodywrapper');
120+
let targetLeft = '10px';
121+
if (bodyWrapper) {
122+
// Force layout recalculation to get updated position
123+
bodyWrapper.offsetHeight;
124+
const bodyRect = bodyWrapper.getBoundingClientRect();
125+
targetLeft = (bodyRect.left + 10) + 'px'; // 10px padding from left edge
126+
}
127+
128+
// Disable transition for instant positioning
129+
toggleButton.style.transition = 'none';
130+
toggleButton.style.top = targetTop + 'px';
131+
toggleButton.style.left = targetLeft;
132+
133+
// Re-enable transition after a brief moment (for hover effects)
134+
setTimeout(function() {
135+
toggleButton.style.transition = '';
136+
}, 10);
137+
}
138+
139+
function collapseSidebar(isAutoCollapse) {
140+
if (!sidebar || !toggleButton) return;
141+
142+
// Hide sidebar
143+
sidebar.style.display = 'none';
144+
145+
// Move button to body first
146+
if (sidebar.contains(toggleButton)) {
147+
document.body.appendChild(toggleButton);
148+
}
149+
150+
toggleButton.classList.add('sidebar-toggle-collapsed');
151+
toggleButton.style.position = 'fixed';
152+
153+
// Adjust bodywrapper to match navbar width FIRST
154+
const bodyWrapper = document.querySelector('.bodywrapper');
155+
if (bodyWrapper) {
156+
const relatedNav = document.querySelector('.related');
157+
if (relatedNav) {
158+
const navRect = relatedNav.getBoundingClientRect();
159+
const navStyles = window.getComputedStyle(relatedNav);
160+
const docWrapper = document.querySelector('.documentwrapper');
161+
const docLeft = docWrapper ? docWrapper.getBoundingClientRect().left : 0;
162+
163+
// Calculate relative position
164+
const relativeNavLeft = navRect.left - docLeft;
165+
const navWidth = navRect.right - navRect.left;
166+
167+
bodyWrapper.style.marginLeft = relativeNavLeft + 'px';
168+
bodyWrapper.style.width = navWidth + 'px';
169+
bodyWrapper.style.backgroundColor = 'white';
170+
// No padding needed - button is fixed and won't scroll with content
171+
bodyWrapper.classList.add('sidebar-collapsed-body');
172+
173+
// Force layout recalculation
174+
bodyWrapper.offsetHeight;
175+
}
176+
}
177+
178+
// NOW position button AFTER bodywrapper has been repositioned
179+
// Get button's current vertical position
180+
const rect = toggleButton.getBoundingClientRect();
181+
let targetTop = rect.top;
182+
183+
// Ensure button is below navbar
184+
const relatedNav = document.querySelector('.related');
185+
if (relatedNav) {
186+
const navBottom = relatedNav.getBoundingClientRect().bottom;
187+
if (targetTop < navBottom + 10) {
188+
targetTop = navBottom + 10;
189+
}
190+
}
191+
192+
// Position button at bodywrapper's left edge with a bit of breathing room
193+
// (Called after bodywrapper is repositioned)
194+
updateCollapsedButtonPosition(targetTop);
195+
196+
toggleButton.innerHTML = '▶';
197+
toggleButton.setAttribute('title', 'Expand sidebar');
198+
199+
// Store state
200+
if (!isAutoCollapse) {
201+
localStorage.setItem(STORAGE_KEY, 'true');
202+
}
203+
204+
document.body.classList.add('sidebar-collapsed-body');
205+
}
206+
207+
function expandSidebar() {
208+
if (!sidebar || !toggleButton) return;
209+
210+
// Show sidebar
211+
sidebar.style.display = '';
212+
213+
// Move button back to sidebar
214+
if (document.body.contains(toggleButton)) {
215+
sidebar.appendChild(toggleButton);
216+
}
217+
218+
toggleButton.classList.remove('sidebar-toggle-collapsed');
219+
toggleButton.style.position = 'absolute';
220+
221+
// Disable transition for instant positioning
222+
toggleButton.style.transition = 'none';
223+
toggleButton.style.top = '10px';
224+
toggleButton.style.right = '10px';
225+
toggleButton.style.left = 'auto';
226+
toggleButton.innerHTML = '◀';
227+
toggleButton.setAttribute('title', 'Collapse sidebar');
228+
229+
// Re-enable transition after a brief moment (for hover effects)
230+
setTimeout(function() {
231+
toggleButton.style.transition = '';
232+
}, 10);
233+
234+
// Restore bodywrapper
235+
const bodyWrapper = document.querySelector('.bodywrapper');
236+
if (bodyWrapper) {
237+
bodyWrapper.style.marginLeft = '';
238+
bodyWrapper.style.width = '';
239+
bodyWrapper.style.backgroundColor = '';
240+
bodyWrapper.classList.remove('sidebar-collapsed-body');
241+
}
242+
243+
// Clear state
244+
localStorage.removeItem(STORAGE_KEY);
245+
document.body.classList.remove('sidebar-collapsed-body');
246+
}
247+
248+
init();
249+
})();
250+

source/atemplates/layout.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{# Extend the cloud theme's layout and add sidebar toggle script #}
2+
{% extends "!layout.html" %}
3+
4+
{% block extrahead %}
5+
{{ super() }}
6+
<script type="text/javascript" src="{{ pathto('_static/sidebar-toggle.js', 1) }}"></script>
7+
{% endblock %}
8+

0 commit comments

Comments
 (0)