Move sidebar left, add collapse toggle and remembered sections

- Course outline sidebar now sits on the left of the player instead
  of the right (mobile stacking order unchanged - video still first).
- Collapsible to a slim rail via a toggle button, remembered per
  device, for a full-width player when wanted.
- Which sections a user has manually expanded is now remembered
  (keyed by the section's path) and shared between the lesson-page
  sidebar and the dashboard's course view, so re-opening a course
  doesn't reset everything back to collapsed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 09:41:52 -04:00
co-authored by Claude Sonnet 5
parent 0ed531503e
commit becbb0b419
6 changed files with 156 additions and 34 deletions
+109 -11
View File
@@ -166,9 +166,43 @@
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
order: -1;
}
.lesson-sidebar h3 {
margin-top: 0;
margin: 0;
}
.lesson-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.sidebar-toggle-btn {
background: var(--bg-tertiary);
border: none;
color: var(--text-primary);
border-radius: var(--radius);
width: 28px;
height: 28px;
flex-shrink: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9em;
transition: background 0.2s;
}
.sidebar-toggle-btn:hover {
background: var(--bg-tertiary-hover);
}
.lesson-sidebar.collapsed {
flex: 0 0 auto;
padding: 20px 8px;
overflow: hidden;
}
.lesson-sidebar.collapsed .lesson-sidebar-header h3,
.lesson-sidebar.collapsed .tree-container {
display: none;
}
@media (max-width: 900px) {
.lesson-layout {
@@ -179,6 +213,10 @@
width: 100%;
position: static;
max-height: 420px;
order: 0;
}
.lesson-sidebar.collapsed {
max-height: none;
}
}
/* Course-outline sidebar tree - scoped under .lesson-sidebar so
@@ -796,8 +834,11 @@
</div>
</div>
<div class="container lesson-sidebar">
<h3>Course Outline</h3>
<div class="container lesson-sidebar" id="lesson-sidebar">
<div class="lesson-sidebar-header">
<h3>Course Outline</h3>
<button type="button" class="sidebar-toggle-btn" id="sidebar-toggle-btn" onclick="toggleSidebar()" title="Collapse sidebar"></button>
</div>
<div class="tree-container">
{{ tree.render_tree_node(course.root_node, course.path, lesson) }}
</div>
@@ -1199,27 +1240,84 @@
if (content && content.classList.contains('tree-content')) {
const isExpanded = content.classList.toggle('expanded');
if (toggleBtn) toggleBtn.textContent = isExpanded ? '▼' : '▶';
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
}
}
// Sections default collapsed (same as the dashboard) - expand
// just the ancestor chain down to the current lesson so it's
// visible without the user having to hunt for it, and scroll
// it into view within the sidebar's own scroll area.
// Remembers which course sections a user has manually expanded
// (keyed by the section's absolute path, shared with the
// dashboard's own tree - see _course_tree.html) so re-opening
// a course doesn't reset every section back to collapsed.
function getExpandedSectionPaths() {
try {
return new Set(JSON.parse(localStorage.getItem('offlineu-expanded-sections') || '[]'));
} catch (e) {
return new Set();
}
}
function saveSectionExpanded(path, expanded) {
const paths = getExpandedSectionPaths();
if (expanded) paths.add(path); else paths.delete(path);
localStorage.setItem('offlineu-expanded-sections', JSON.stringify([...paths]));
}
function expandSection(content) {
content.classList.add('expanded');
const header = content.previousElementSibling;
const toggleBtn = header ? header.querySelector('.tree-toggle') : null;
if (toggleBtn) toggleBtn.textContent = '▼';
}
(function restoreExpandedSections() {
const saved = getExpandedSectionPaths();
if (!saved.size) return;
document.querySelectorAll('.lesson-sidebar .tree-content[data-section-path]').forEach(function(content) {
if (saved.has(content.dataset.sectionPath)) expandSection(content);
});
})();
// Sections default collapsed - additionally expand the ancestor
// chain down to the current lesson (even if not previously
// remembered) so it's visible without hunting for it, and
// scroll it into view within the sidebar's own scroll area.
// Persisted the same as a manual expand, so it stays open on
// later visits too.
(function revealCurrentLessonInSidebar() {
const current = document.querySelector('.lesson-sidebar .lesson-item.current');
if (!current) return;
let content = current.closest('.tree-content');
while (content) {
content.classList.add('expanded');
const header = content.previousElementSibling;
const toggleBtn = header ? header.querySelector('.tree-toggle') : null;
if (toggleBtn) toggleBtn.textContent = '▼';
expandSection(content);
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, true);
content = content.parentElement ? content.parentElement.closest('.tree-content') : null;
}
current.scrollIntoView({ block: 'center' });
})();
// ---- Sidebar collapse (client-only, remembered per device) ----
function toggleSidebar() {
const sidebar = document.getElementById('lesson-sidebar');
const btn = document.getElementById('sidebar-toggle-btn');
const collapsed = sidebar.classList.toggle('collapsed');
if (btn) {
btn.textContent = collapsed ? '▶' : '◀';
btn.title = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
localStorage.setItem('offlineu-lesson-sidebar-collapsed', collapsed ? '1' : '0');
}
(function restoreSidebarCollapsed() {
if (localStorage.getItem('offlineu-lesson-sidebar-collapsed') !== '1') return;
const sidebar = document.getElementById('lesson-sidebar');
const btn = document.getElementById('sidebar-toggle-btn');
if (sidebar) sidebar.classList.add('collapsed');
if (btn) {
btn.textContent = '▶';
btn.title = 'Expand sidebar';
}
})();
renderNotes();
// Seek once metadata is available (activeMedia.duration is NaN