diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d5e6e0..329b097 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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:12 UTC — Add course outline sidebar to the lesson page
2026-08-25 12:25 UTC — Remove unreliable duplicate-lesson-file matching
diff --git a/README.md b/README.md
index 59d3b55..0584de0 100644
--- a/README.md
+++ b/README.md
@@ -225,12 +225,15 @@ silently stay blank instead of erroring.
**Playback & progress**
- Video/audio player with resize, playback-speed presets, and resume-from-
last-position
-- Course outline sidebar on the lesson page: every section and lesson in
- the course (videos and documents alike - a standalone PDF/doc is its own
- entry, same as a video), one click to jump anywhere without backing out
- to the course page first. The current lesson's section opens
- automatically and scrolls into view; sticky and independently
+- Course outline sidebar on the lesson page (left side): every section and
+ lesson in the course (videos and documents alike - a standalone PDF/doc
+ is its own entry, same as a video), one click to jump anywhere without
+ backing out to the course page first. The current lesson's section
+ opens automatically and scrolls into view; sticky and independently
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/
_course_tree.html`), so the two always look and behave the same.
- Auto-play next lesson when one ends, with a cancelable few-second
diff --git a/VERSION b/VERSION
index 43dccd4..1f3250b 100644
--- a/VERSION
+++ b/VERSION
@@ -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
diff --git a/templates/_course_tree.html b/templates/_course_tree.html
index d87372c..f5abeee 100644
--- a/templates/_course_tree.html
+++ b/templates/_course_tree.html
@@ -33,7 +33,7 @@
{% if node.children or node.lessons %}
-
+
{% for child_name, child_node in node.children.items() %}
{{ render_tree_node(child_node, course_path, current_lesson, depth + 1) }}
{% endfor %}
diff --git a/templates/course_dashboard.html b/templates/course_dashboard.html
index b44909c..e7ec574 100644
--- a/templates/course_dashboard.html
+++ b/templates/course_dashboard.html
@@ -1360,29 +1360,49 @@
}
function toggleTree(element) {
- console.log('Toggle clicked:', element);
-
// Find the content div that comes after this header
const content = element.nextElementSibling;
const toggle = element.querySelector('.tree-toggle');
-
- console.log('Content element:', content);
- console.log('Toggle element:', toggle);
-
+
if (content && content.classList.contains('tree-content')) {
- const isExpanded = content.classList.contains('expanded');
- content.classList.toggle('expanded');
-
- if (toggle) {
- toggle.textContent = content.classList.contains('expanded') ? '▼' : '▶';
- }
-
- console.log('Toggled tree, expanded:', content.classList.contains('expanded'));
- } else {
- console.log('No content element found or not tree-content');
+ const isExpanded = content.classList.toggle('expanded');
+ if (toggle) toggle.textContent = isExpanded ? '▼' : '▶';
+ if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
}
}
+ // Remembers which course sections a user has manually expanded
+ // (keyed by the section's absolute path, so it's stable across
+ // 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
// contents at a time (replacing the list, not nesting under it) and
// tracks how we got here in libraryTrail so a breadcrumb can jump
diff --git a/templates/lesson_view.html b/templates/lesson_view.html
index 03fd7c2..a98ff3d 100644
--- a/templates/lesson_view.html
+++ b/templates/lesson_view.html
@@ -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 @@
-