diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5cb3e6..7af0aeb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+2026-08-26 14:27 UTC — Add expand all/collapse all to the course tree
2026-08-26 14:13 UTC — Header/footer polish, fix dead link, add focus rings
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
diff --git a/README.md b/README.md
index e36bfd8..8f32150 100644
--- a/README.md
+++ b/README.md
@@ -238,7 +238,9 @@ silently stay blank instead of erroring.
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.
+ so re-opening a course doesn't reset everything back to collapsed. An
+ "Expand all" link (toggling to "Collapse all") opens or closes every
+ section at once - same control on the dashboard's course tree.
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 7550b9c..b84a828 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2026-08-26 14:13 UTC — header/footer polish, fix dead link, add focus rings
+2026-08-26 14:27 UTC — add expand all/collapse all to the course tree
diff --git a/templates/course_dashboard.html b/templates/course_dashboard.html
index cd6b38c..47f9ce4 100644
--- a/templates/course_dashboard.html
+++ b/templates/course_dashboard.html
@@ -481,9 +481,29 @@
/* Dynamic Directory Tree Styles */
.tree-container {
+ margin-top: 8px;
+ }
+
+ .tree-container-header {
+ display: flex;
+ justify-content: flex-end;
margin-top: 20px;
}
+ .expand-all-btn {
+ background: none;
+ border: none;
+ color: var(--accent);
+ cursor: pointer;
+ font-size: 0.85em;
+ text-decoration: underline;
+ padding: 4px 0;
+ }
+
+ .expand-all-btn:hover {
+ color: var(--accent-hover);
+ }
+
.tree-item {
margin-bottom: 5px;
}
@@ -1082,7 +1102,10 @@
{% endif %}
-
+
+
{{ tree.render_tree_node(course.root_node, course.path) }}
@@ -1396,6 +1419,35 @@
}
restoreExpandedSections();
+ // Expand/collapse every section within a tree at once, toggling
+ // between the two based on whether everything's already open -
+ // avoids a one-way "Expand all" with no quick way back.
+ function toggleAllTreeSections(containerSelector, btnEl) {
+ const container = document.querySelector(containerSelector);
+ if (!container) return;
+ const contents = [...container.querySelectorAll('.tree-content')];
+ if (!contents.length) return;
+ const shouldExpand = !contents.every(c => c.classList.contains('expanded'));
+ contents.forEach(function(content) {
+ content.classList.toggle('expanded', shouldExpand);
+ const header = content.previousElementSibling;
+ const toggle = header ? header.querySelector('.tree-toggle') : null;
+ if (toggle) toggle.textContent = shouldExpand ? '▼' : '▶';
+ if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, shouldExpand);
+ });
+ if (btnEl) btnEl.textContent = shouldExpand ? 'Collapse all' : 'Expand all';
+ }
+
+ (function initExpandAllLabel() {
+ const btn = document.getElementById('dashboard-expand-all-btn');
+ const container = document.getElementById('dashboard-tree-container');
+ if (!btn || !container) return;
+ const contents = [...container.querySelectorAll('.tree-content')];
+ if (contents.length && contents.every(c => c.classList.contains('expanded'))) {
+ btn.textContent = 'Collapse all';
+ }
+ })();
+
// 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 f9ef69e..cb3c508 100644
--- a/templates/lesson_view.html
+++ b/templates/lesson_view.html
@@ -201,6 +201,7 @@
overflow: hidden;
}
.lesson-sidebar.collapsed .lesson-sidebar-header h3,
+ .lesson-sidebar.collapsed .tree-container-header,
.lesson-sidebar.collapsed .tree-container {
display: none;
}
@@ -224,9 +225,26 @@
and similarly-named rules. Mirrors course_dashboard.html's tree
styling (shared macro, see _course_tree.html) at a denser size
for the narrower column. */
- .lesson-sidebar .tree-container {
+ .lesson-sidebar .tree-container-header {
+ display: flex;
+ justify-content: flex-end;
margin-top: 10px;
}
+ .lesson-sidebar .expand-all-btn {
+ background: none;
+ border: none;
+ color: var(--accent);
+ cursor: pointer;
+ font-size: 0.8em;
+ text-decoration: underline;
+ padding: 2px 0;
+ }
+ .lesson-sidebar .expand-all-btn:hover {
+ color: var(--accent-hover);
+ }
+ .lesson-sidebar .tree-container {
+ margin-top: 6px;
+ }
.lesson-sidebar .tree-item {
margin-bottom: 5px;
}
@@ -846,7 +864,10 @@
Course Outline
-
+
+
@@ -1302,6 +1323,35 @@
current.scrollIntoView({ block: 'center' });
})();
+ // Expand/collapse every section within a tree at once, toggling
+ // between the two based on whether everything's already open -
+ // avoids a one-way "Expand all" with no quick way back.
+ function toggleAllTreeSections(containerSelector, btnEl) {
+ const container = document.querySelector(containerSelector);
+ if (!container) return;
+ const contents = [...container.querySelectorAll('.tree-content')];
+ if (!contents.length) return;
+ const shouldExpand = !contents.every(c => c.classList.contains('expanded'));
+ contents.forEach(function(content) {
+ content.classList.toggle('expanded', shouldExpand);
+ const header = content.previousElementSibling;
+ const toggle = header ? header.querySelector('.tree-toggle') : null;
+ if (toggle) toggle.textContent = shouldExpand ? '▼' : '▶';
+ if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, shouldExpand);
+ });
+ if (btnEl) btnEl.textContent = shouldExpand ? 'Collapse all' : 'Expand all';
+ }
+
+ (function initExpandAllLabel() {
+ const btn = document.getElementById('sidebar-expand-all-btn');
+ const container = document.getElementById('sidebar-tree-container');
+ if (!btn || !container) return;
+ const contents = [...container.querySelectorAll('.tree-content')];
+ if (contents.length && contents.every(c => c.classList.contains('expanded'))) {
+ btn.textContent = 'Collapse all';
+ }
+ })();
+
// ---- Sidebar collapse (client-only, remembered per device) ----
function toggleSidebar() {
const sidebar = document.getElementById('lesson-sidebar');