diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af0aeb..0f66f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +2026-08-26 14:45 UTC — Redesign expandable lists to a leaner, flat style 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 diff --git a/README.md b/README.md index 8f32150..c7fb7b8 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,8 @@ silently stay blank instead of erroring. rather than a level to browse into); every row also has a checkbox, so several items - found via search or expanded across different tree levels - can be hidden, shown, or moved to the same destination together - in one batch instead of one at a time. + in one batch instead of one at a time. Rows use the same flat, leading- + chevron styling as the course tree (see Playback & progress below). - *Undo*: a "Last action: ... [Undo]" bar appears after any move or rename (Sort Unsorted apply, Bulk Rename apply, a Manage Library move/rename, a bulk move) and reverses the whole batch in one click. @@ -242,7 +243,11 @@ silently stay blank instead of erroring. "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. + _course_tree.html`), so the two always look and behave the same. Flat + list styling - no per-row box or border, a small leading chevron that + doubles as the expand affordance instead of a separate right-edge + button, indentation for hierarchy - for a denser, cleaner look than + boxed rows would give at this list length. - Auto-play next lesson when one ends, with a cancelable few-second countdown - on by default, toggle it off in Settings → Video Player - Keyboard shortcuts on the lesson page: Space (play/pause), ←/→ (seek diff --git a/VERSION b/VERSION index b84a828..51b7f31 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2026-08-26 14:27 UTC — add expand all/collapse all to the course tree +2026-08-26 14:45 UTC — redesign expandable lists to a leaner, flat style diff --git a/templates/_course_tree.html b/templates/_course_tree.html index f5abeee..6bfb8b9 100644 --- a/templates/_course_tree.html +++ b/templates/_course_tree.html @@ -8,6 +8,13 @@ defined by whichever page imports this - see course_dashboard.html for the canonical versions. + Flat list styling (no per-row box/border), a leading chevron that + doubles as the expand affordance instead of a separate right-edge + button, and indentation for hierarchy - the chevron's rotation is + driven purely by CSS (:has(+ .tree-content.expanded)), so toggling + is just adding/removing one class on .tree-content; nothing in JS + ever touches the icon directly. + `current_lesson` (a Lesson object, not a path string) is optional - when given, the matching row gets a `current` class instead of being a plain click target, and stays unclickable since you're already @@ -20,16 +27,11 @@ {% set stats = section_stats(node) %}
-
- {{ icons.icon('folder', 16) }} - {{ node.name }} - - {% if stats.total_lessons %}{{ stats.completed_lessons }}/{{ stats.total_lessons }} watched{% else %}Empty{% endif %} - -
- {% if node.children or node.lessons %} - - {% endif %} + + {{ node.name }} + + {% if stats.total_lessons %}{{ stats.completed_lessons }}/{{ stats.total_lessons }}{% else %}Empty{% endif %} +
{% if node.children or node.lessons %} @@ -44,31 +46,21 @@ {% set is_current = current_lesson and lesson.path == current_lesson.path %}
-
- - {% if lesson.lesson_type == 'video' %}{{ icons.icon('video', 16) }} - {% elif lesson.lesson_type == 'audio' %}{{ icons.icon('music', 16) }} - {% elif lesson.lesson_type == 'quiz' %}{{ icons.icon('clipboard', 16) }} - {% elif lesson.lesson_type == 'mixed' %}{{ icons.icon('package', 16) }} - {% else %}{{ icons.icon('file-text', 16) }}{% endif %} - - {{ lesson.title }} -
-
- {{ lesson.lesson_type|title }} - {% if lesson.duration_seconds and lesson.duration_seconds >= 60 %} - {{ lesson.duration_seconds|format_duration }} - {% endif %} - {% if lesson.completed %} - {{ icons.icon('check', 14) }} - {% elif percent_watched %} - {{ percent_watched }}% watched - {% else %} - {{ icons.icon('circle', 14) }} - {% endif %} -
- {% if percent_watched %} -
+ + {% if lesson.lesson_type == 'video' %}{{ icons.icon('video', 15) }} + {% elif lesson.lesson_type == 'audio' %}{{ icons.icon('music', 15) }} + {% elif lesson.lesson_type == 'quiz' %}{{ icons.icon('clipboard', 15) }} + {% elif lesson.lesson_type == 'mixed' %}{{ icons.icon('package', 15) }} + {% else %}{{ icons.icon('file-text', 15) }}{% endif %} + + {{ lesson.title }} + {% if lesson.duration_seconds and lesson.duration_seconds >= 60 %} + {{ lesson.duration_seconds|format_duration }} + {% endif %} + {% if lesson.completed %} + {{ icons.icon('check', 13) }} + {% elif percent_watched %} + {{ percent_watched }}% {% endif %}
{% endfor %} diff --git a/templates/course_dashboard.html b/templates/course_dashboard.html index 47f9ce4..be6e6ac 100644 --- a/templates/course_dashboard.html +++ b/templates/course_dashboard.html @@ -682,6 +682,89 @@ display: block; } + /* Course-outline tree: a leaner, flatter look than the boxed rows + above - those stay as-is since directoryRowHtml (the Library + folder browser) reuses the same base .tree-header/.tree-icon/ + .lesson-item/etc. classes and isn't part of this redesign. + Scoped under .tree-container (exclusive to the course tree) so + nothing outside it is affected. No per-row background/border; a + small leading chevron doubles as the expand affordance instead + of a separate right-edge button, rotating via :has() so JS + never has to touch the icon directly - toggling is just + add/remove of one class on .tree-content. */ + .tree-container .tree-header { + background: none; + border-left: none; + padding: 6px 8px; + gap: 8px; + } + .tree-container .tree-header:hover { + background: var(--bg-tertiary); + } + .tree-container .tree-toggle-icon { + display: inline-block; + width: 14px; + flex-shrink: 0; + color: var(--accent); + font-size: 0.85em; + transition: transform 0.15s ease; + } + .tree-container .tree-header:has(+ .tree-content.expanded) .tree-toggle-icon { + transform: rotate(90deg); + } + .tree-container .tree-name { + font-weight: 600; + } + .tree-container .tree-content { + margin-left: 20px; + margin-top: 2px; + } + .tree-container .lesson-item { + background: none; + border-left: none; + padding: 5px 8px; + margin-bottom: 0; + gap: 8px; + justify-content: flex-start; + transform: none; + } + .tree-container .lesson-item .lesson-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 400; + font-size: 0.95em; + } + .tree-container .lesson-item:hover { + background: var(--bg-tertiary); + transform: none; + } + .tree-container .lesson-item.completed { + background: none; + } + .tree-container .lesson-item.completed:hover { + background: var(--bg-tertiary); + } + .tree-container .lesson-item.current { + background: var(--bg-tertiary); + cursor: default; + } + .tree-container .lesson-item.current .lesson-name { + font-weight: 600; + color: var(--accent); + } + .tree-container .lesson-item .lesson-icon { + font-size: 0.9em; + color: var(--text-muted); + } + .tree-container .lesson-item .lesson-duration { + font-size: 0.8em; + color: var(--text-muted); + flex-shrink: 0; + } + .library-toolbar { display: flex; gap: 10px; @@ -1376,13 +1459,13 @@ } function toggleTree(element) { - // Find the content div that comes after this header + // Find the content div that comes after this header - the + // chevron's rotation is pure CSS (:has(+ .tree-content.expanded) + // in _course_tree.html's consumers), so toggling this one class + // is the entire job here. const content = element.nextElementSibling; - const toggle = element.querySelector('.tree-toggle'); - if (content && content.classList.contains('tree-content')) { const isExpanded = content.classList.toggle('expanded'); - if (toggle) toggle.textContent = isExpanded ? '▼' : '▶'; if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded); } } @@ -1410,11 +1493,7 @@ 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 = '▼'; + if (saved.has(content.dataset.sectionPath)) content.classList.add('expanded'); }); } restoreExpandedSections(); @@ -1430,9 +1509,6 @@ 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'; diff --git a/templates/lesson_view.html b/templates/lesson_view.html index cb3c508..c34cb79 100644 --- a/templates/lesson_view.html +++ b/templates/lesson_view.html @@ -223,8 +223,10 @@ /* Course-outline sidebar tree - scoped under .lesson-sidebar so these don't collide with this page's own .lesson-title (the H1) 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. */ + styling (shared macro, see _course_tree.html): a flat list, no + per-row background/border, a small leading chevron that doubles + as the expand affordance and rotates via :has() rather than any + JS touching it directly. */ .lesson-sidebar .tree-container-header { display: flex; justify-content: flex-end; @@ -246,40 +248,33 @@ margin-top: 6px; } .lesson-sidebar .tree-item { - margin-bottom: 5px; + margin-bottom: 0; } .lesson-sidebar .tree-header { - background: var(--bg-tertiary); - padding: 10px 12px; + padding: 5px 6px; border-radius: var(--radius); cursor: pointer; display: flex; align-items: center; - justify-content: space-between; - transition: background 0.3s; - border-left: 3px solid #666; + gap: 6px; + transition: background 0.15s; } .lesson-sidebar .tree-header:hover { - background: var(--bg-tertiary-hover); + background: var(--bg-tertiary); } - .lesson-sidebar .tree-header.directory { - border-left-color: var(--accent); - } - .lesson-sidebar .tree-title { - display: flex; - align-items: center; - gap: 8px; - flex: 1; - min-width: 0; - } - .lesson-sidebar .tree-icon { - font-size: 1.1em; - width: 18px; - text-align: center; + .lesson-sidebar .tree-toggle-icon { + display: inline-block; + width: 12px; flex-shrink: 0; + color: var(--accent); + font-size: 0.8em; + transition: transform 0.15s ease; + } + .lesson-sidebar .tree-header:has(+ .tree-content.expanded) .tree-toggle-icon { + transform: rotate(90deg); } .lesson-sidebar .tree-name { - font-weight: 500; + font-weight: 600; flex: 1; min-width: 0; overflow: hidden; @@ -288,132 +283,72 @@ font-size: 0.9em; } .lesson-sidebar .tree-stats { - font-size: 0.75em; + font-size: 0.7em; color: var(--text-muted); - margin-left: 8px; flex-shrink: 0; white-space: nowrap; } - .lesson-sidebar .tree-toggle { - background: none; - border: none; - color: var(--text-primary); - font-size: 1.1em; - cursor: pointer; - padding: 4px; - border-radius: 3px; - transition: background 0.3s; - } - .lesson-sidebar .tree-toggle:hover { - background: #555; - } .lesson-sidebar .tree-content { - margin-left: 16px; - margin-top: 5px; + margin-left: 14px; + margin-top: 1px; display: none; } .lesson-sidebar .tree-content.expanded { display: block; } .lesson-sidebar .lesson-item { - background: var(--bg-tertiary); - padding: 8px 12px; + padding: 4px 6px; border-radius: var(--radius); - margin-bottom: 5px; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-items: center; - transition: all 0.2s; - cursor: pointer; - border-left: 3px solid #666; - position: relative; - overflow: hidden; - gap: 4px; - } - .lesson-sidebar .lesson-item:hover { - border-left-color: var(--accent); - background: var(--bg-tertiary-hover); - } - .lesson-sidebar .lesson-item.completed { - border-left-color: var(--success); - } - .lesson-sidebar .lesson-item.in-progress { - border-left-color: var(--accent); - } - .lesson-sidebar .lesson-item.current { - border-left-color: var(--accent); - background: var(--bg-tertiary-hover); - cursor: default; - font-weight: 600; - } - .lesson-sidebar .lesson-progress-track { - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 3px; - background: rgba(255, 255, 255, 0.08); - } - .lesson-sidebar .lesson-progress-fill { - height: 100%; - background: var(--accent); - } - .lesson-sidebar .lesson-item.completed .lesson-progress-fill { - background: var(--success); - } - .lesson-sidebar .watched-badge { - font-size: 0.7em; - background: var(--bg-primary); - color: var(--accent); - padding: 1px 6px; - border-radius: 3px; - border: 1px solid var(--accent); - white-space: nowrap; - } - .lesson-sidebar .lesson-item .lesson-title { + margin-bottom: 0; display: flex; align-items: center; gap: 6px; - flex: 1; - min-width: 0; + transition: background 0.15s; + cursor: pointer; + } + .lesson-sidebar .lesson-item:hover { + background: var(--bg-tertiary); + } + .lesson-sidebar .lesson-item.current { + background: var(--bg-tertiary); + cursor: default; + } + .lesson-sidebar .watched-badge { + font-size: 0.65em; + color: var(--accent); + white-space: nowrap; + flex-shrink: 0; } .lesson-sidebar .lesson-icon { - font-size: 1em; + font-size: 0.85em; + color: var(--text-muted); flex-shrink: 0; } .lesson-sidebar .lesson-name { - font-size: 0.85em; + font-size: 0.8em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + flex: 1; + min-width: 0; } - .lesson-sidebar .lesson-meta { - font-size: 0.75em; - color: var(--text-muted); - display: flex; - align-items: center; - gap: 4px; - flex-shrink: 0; - } - .lesson-sidebar .lesson-type { - display: none; + .lesson-sidebar .lesson-item.current .lesson-name { + font-weight: 600; + color: var(--accent); } .lesson-sidebar .lesson-duration { - font-size: 0.75em; + font-size: 0.7em; color: var(--text-muted); white-space: nowrap; + flex-shrink: 0; } .lesson-sidebar .status-icon { - font-size: 1.1em; - font-weight: bold; + font-size: 0.9em; + flex-shrink: 0; } .lesson-sidebar .status-icon.completed { color: var(--success); } - .lesson-sidebar .status-icon.pending { - color: var(--text-muted); - } video, audio { width: 100%; max-width: 100%; @@ -1262,12 +1197,13 @@ // ---- Course outline sidebar (shared tree markup/macro with the // dashboard's loaded-course view - see _course_tree.html) ---- + // The chevron's rotation is pure CSS (:has(+ .tree-content. + // expanded) in this file's tree styles), so toggling this one + // class is the entire job - nothing here ever touches the icon. function toggleTree(element) { const content = element.nextElementSibling; - const toggleBtn = element.querySelector('.tree-toggle'); 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); } } @@ -1292,9 +1228,6 @@ 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() { @@ -1334,9 +1267,6 @@ 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'; diff --git a/templates/unsorted.html b/templates/unsorted.html index f0d1807..b6bd607 100644 --- a/templates/unsorted.html +++ b/templates/unsorted.html @@ -164,14 +164,14 @@ align-items: center; justify-content: space-between; gap: 10px; - padding: 8px 10px; + padding: 5px 8px; border-radius: var(--radius); - background: var(--bg-tertiary); - margin-bottom: 5px; + background: none; + margin-bottom: 0; cursor: pointer; } .curate-row:hover { - background: var(--bg-tertiary-hover); + background: var(--bg-tertiary); } .curate-row.is-hidden { opacity: 0.55; @@ -188,15 +188,24 @@ text-overflow: ellipsis; white-space: nowrap; } + /* Leading chevron doubles as the expand affordance (matches the + course tree's leaner style - see _course_tree.html) and rotates + via :has() rather than any JS touching it directly; toggling is + just add/remove of one class on .curate-children. */ .curate-toggle { - font-size: 1.1em; - width: 18px; - text-align: center; + display: inline-block; + width: 12px; flex-shrink: 0; + color: var(--accent); + font-size: 0.85em; + transition: transform 0.15s ease; + } + .curate-row:has(+ .curate-children.expanded) .curate-toggle { + transform: rotate(90deg); } .curate-children { - margin-left: 22px; - margin-top: 4px; + margin-left: 16px; + margin-top: 1px; display: none; } .curate-children.expanded { @@ -1456,7 +1465,7 @@
${checkbox}
- + ${icon} ${escapeHtml(item.name)}
@@ -1477,16 +1486,13 @@ function toggleCurateDir(rowEl, path) { const content = rowEl.nextElementSibling; - const toggleIcon = rowEl.querySelector('.curate-toggle'); if (!content || !content.classList.contains('curate-children')) return; if (content.classList.contains('expanded')) { content.classList.remove('expanded'); - if (toggleIcon) toggleIcon.textContent = '▶'; return; } content.classList.add('expanded'); - if (toggleIcon) toggleIcon.textContent = '▼'; if (content.dataset.loaded === 'true') return; content.innerHTML = '
Loading...
';