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
+1
View File
@@ -1,3 +1,4 @@
2026-08-26 13:41 UTC — Move sidebar left, add collapse toggle and remembered sections
2026-08-26 13:17 UTC — Add hover tooltips for truncated sidebar titles 2026-08-26 13:17 UTC — Add hover tooltips for truncated sidebar titles
2026-08-26 13:12 UTC — Add course outline sidebar to the lesson page 2026-08-26 13:12 UTC — Add course outline sidebar to the lesson page
2026-08-25 12:25 UTC — Remove unreliable duplicate-lesson-file matching 2026-08-25 12:25 UTC — Remove unreliable duplicate-lesson-file matching
+8 -5
View File
@@ -225,12 +225,15 @@ silently stay blank instead of erroring.
**Playback & progress** **Playback & progress**
- Video/audio player with resize, playback-speed presets, and resume-from- - Video/audio player with resize, playback-speed presets, and resume-from-
last-position last-position
- Course outline sidebar on the lesson page: every section and lesson in - Course outline sidebar on the lesson page (left side): every section and
the course (videos and documents alike - a standalone PDF/doc is its own lesson in the course (videos and documents alike - a standalone PDF/doc
entry, same as a video), one click to jump anywhere without backing out is its own entry, same as a video), one click to jump anywhere without
to the course page first. The current lesson's section opens backing out to the course page first. The current lesson's section
automatically and scrolls into view; sticky and independently opens automatically and scrolls into view; sticky and independently
scrollable on desktop, stacks below the player on narrow viewports. scrollable on desktop, stacks below the player on narrow viewports.
Collapsible to a slim rail (remembered per device) for a full-width
player, and which sections you've manually expanded is remembered too,
so re-opening a course doesn't reset everything back to collapsed.
Shares its rendering with the loaded-course dashboard view (`templates/ Shares its rendering with the loaded-course dashboard view (`templates/
_course_tree.html`), so the two always look and behave the same. _course_tree.html`), so the two always look and behave the same.
- Auto-play next lesson when one ends, with a cancelable few-second - Auto-play next lesson when one ends, with a cancelable few-second
+1 -1
View File
@@ -1 +1 @@
2026-08-26 13:17 UTC — add hover tooltips for truncated sidebar titles 2026-08-26 13:41 UTC — move sidebar left, add collapse toggle and remembered sections
+1 -1
View File
@@ -33,7 +33,7 @@
</div> </div>
{% if node.children or node.lessons %} {% if node.children or node.lessons %}
<div class="tree-content"> <div class="tree-content" data-section-path="{{ node.path }}">
{% for child_name, child_node in node.children.items() %} {% for child_name, child_node in node.children.items() %}
{{ render_tree_node(child_node, course_path, current_lesson, depth + 1) }} {{ render_tree_node(child_node, course_path, current_lesson, depth + 1) }}
{% endfor %} {% endfor %}
+33 -13
View File
@@ -1360,29 +1360,49 @@
} }
function toggleTree(element) { function toggleTree(element) {
console.log('Toggle clicked:', element);
// Find the content div that comes after this header // Find the content div that comes after this header
const content = element.nextElementSibling; const content = element.nextElementSibling;
const toggle = element.querySelector('.tree-toggle'); const toggle = element.querySelector('.tree-toggle');
console.log('Content element:', content);
console.log('Toggle element:', toggle);
if (content && content.classList.contains('tree-content')) { if (content && content.classList.contains('tree-content')) {
const isExpanded = content.classList.contains('expanded'); const isExpanded = content.classList.toggle('expanded');
content.classList.toggle('expanded'); if (toggle) toggle.textContent = isExpanded ? '▼' : '▶';
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
if (toggle) { }
toggle.textContent = content.classList.contains('expanded') ? '▼' : '▶';
} }
console.log('Toggled tree, expanded:', content.classList.contains('expanded')); // Remembers which course sections a user has manually expanded
} else { // (keyed by the section's absolute path, so it's stable across
console.log('No content element found or not tree-content'); // page loads and shared between this tree and the lesson page's
// sidebar - 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 restoreExpandedSections() {
const saved = getExpandedSectionPaths();
if (!saved.size) return;
document.querySelectorAll('.tree-content[data-section-path]').forEach(function(content) {
if (!saved.has(content.dataset.sectionPath)) return;
content.classList.add('expanded');
const header = content.previousElementSibling;
const toggle = header ? header.querySelector('.tree-toggle') : null;
if (toggle) toggle.textContent = '▼';
});
}
restoreExpandedSections();
// Single-level drill-down: the Library browser shows one folder's // Single-level drill-down: the Library browser shows one folder's
// contents at a time (replacing the list, not nesting under it) and // contents at a time (replacing the list, not nesting under it) and
// tracks how we got here in libraryTrail so a breadcrumb can jump // tracks how we got here in libraryTrail so a breadcrumb can jump
+108 -10
View File
@@ -166,9 +166,43 @@
top: 20px; top: 20px;
max-height: calc(100vh - 40px); max-height: calc(100vh - 40px);
overflow-y: auto; overflow-y: auto;
order: -1;
} }
.lesson-sidebar h3 { .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) { @media (max-width: 900px) {
.lesson-layout { .lesson-layout {
@@ -179,6 +213,10 @@
width: 100%; width: 100%;
position: static; position: static;
max-height: 420px; max-height: 420px;
order: 0;
}
.lesson-sidebar.collapsed {
max-height: none;
} }
} }
/* Course-outline sidebar tree - scoped under .lesson-sidebar so /* Course-outline sidebar tree - scoped under .lesson-sidebar so
@@ -796,8 +834,11 @@
</div> </div>
</div> </div>
<div class="container lesson-sidebar"> <div class="container lesson-sidebar" id="lesson-sidebar">
<div class="lesson-sidebar-header">
<h3>Course Outline</h3> <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"> <div class="tree-container">
{{ tree.render_tree_node(course.root_node, course.path, lesson) }} {{ tree.render_tree_node(course.root_node, course.path, lesson) }}
</div> </div>
@@ -1199,27 +1240,84 @@
if (content && content.classList.contains('tree-content')) { if (content && content.classList.contains('tree-content')) {
const isExpanded = content.classList.toggle('expanded'); const isExpanded = content.classList.toggle('expanded');
if (toggleBtn) toggleBtn.textContent = isExpanded ? '▼' : '▶'; if (toggleBtn) toggleBtn.textContent = isExpanded ? '▼' : '▶';
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
} }
} }
// Sections default collapsed (same as the dashboard) - expand // Remembers which course sections a user has manually expanded
// just the ancestor chain down to the current lesson so it's // (keyed by the section's absolute path, shared with the
// visible without the user having to hunt for it, and scroll // dashboard's own tree - see _course_tree.html) so re-opening
// it into view within the sidebar's own scroll area. // 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() { (function revealCurrentLessonInSidebar() {
const current = document.querySelector('.lesson-sidebar .lesson-item.current'); const current = document.querySelector('.lesson-sidebar .lesson-item.current');
if (!current) return; if (!current) return;
let content = current.closest('.tree-content'); let content = current.closest('.tree-content');
while (content) { while (content) {
content.classList.add('expanded'); expandSection(content);
const header = content.previousElementSibling; if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, true);
const toggleBtn = header ? header.querySelector('.tree-toggle') : null;
if (toggleBtn) toggleBtn.textContent = '▼';
content = content.parentElement ? content.parentElement.closest('.tree-content') : null; content = content.parentElement ? content.parentElement.closest('.tree-content') : null;
} }
current.scrollIntoView({ block: 'center' }); 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(); renderNotes();
// Seek once metadata is available (activeMedia.duration is NaN // Seek once metadata is available (activeMedia.duration is NaN