Redesign expandable lists to a leaner, flat style
Course tree (lesson-page sidebar + dashboard's loaded-course view) and File Management's Manage Library browser both move from boxed, bordered rows to a flat list: no per-row background/border, a small leading chevron that doubles as the expand affordance instead of a separate right-edge button, indentation for hierarchy. The chevron's rotation is pure CSS (:has(+ .tree-content.expanded) / :has(+ .curate-children.expanded)) rather than JS swapping glyph text, which also let a fair amount of now-redundant JS come out. The course tree's new styles are scoped under .tree-container (course dashboard) and .lesson-sidebar (lesson page) so they don't leak into the Library folder browser and other rows that reuse the same base .tree-header/.lesson-item/etc. class names elsewhere in course_dashboard.html - confirmed those are visually untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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: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 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:41 UTC — Move sidebar left, add collapse toggle and remembered sections
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ silently stay blank instead of erroring.
|
|||||||
rather than a level to browse into); every row also has a checkbox, so
|
rather than a level to browse into); every row also has a checkbox, so
|
||||||
several items - found via search or expanded across different tree
|
several items - found via search or expanded across different tree
|
||||||
levels - can be hidden, shown, or moved to the same destination together
|
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
|
- *Undo*: a "Last action: ... [Undo]" bar appears after any move or
|
||||||
rename (Sort Unsorted apply, Bulk Rename apply, a Manage Library
|
rename (Sort Unsorted apply, Bulk Rename apply, a Manage Library
|
||||||
move/rename, a bulk move) and reverses the whole batch in one click.
|
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
|
"Expand all" link (toggling to "Collapse all") opens or closes every
|
||||||
section at once - same control on the dashboard's course tree.
|
section at once - same control on the dashboard's course tree.
|
||||||
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. 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
|
- Auto-play next lesson when one ends, with a cancelable few-second
|
||||||
countdown - on by default, toggle it off in Settings → Video Player
|
countdown - on by default, toggle it off in Settings → Video Player
|
||||||
- Keyboard shortcuts on the lesson page: Space (play/pause), ←/→ (seek
|
- Keyboard shortcuts on the lesson page: Space (play/pause), ←/→ (seek
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+27
-35
@@ -8,6 +8,13 @@
|
|||||||
defined by whichever page imports this - see course_dashboard.html
|
defined by whichever page imports this - see course_dashboard.html
|
||||||
for the canonical versions.
|
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 -
|
`current_lesson` (a Lesson object, not a path string) is optional -
|
||||||
when given, the matching row gets a `current` class instead of being
|
when given, the matching row gets a `current` class instead of being
|
||||||
a plain click target, and stays unclickable since you're already
|
a plain click target, and stays unclickable since you're already
|
||||||
@@ -20,16 +27,11 @@
|
|||||||
{% set stats = section_stats(node) %}
|
{% set stats = section_stats(node) %}
|
||||||
<div class="tree-item">
|
<div class="tree-item">
|
||||||
<div class="tree-header directory" onclick="toggleTree(this)">
|
<div class="tree-header directory" onclick="toggleTree(this)">
|
||||||
<div class="tree-title">
|
<span class="tree-toggle-icon">▸</span>
|
||||||
<span class="tree-icon">{{ icons.icon('folder', 16) }}</span>
|
<span class="tree-name" title="{{ node.name }}">{{ node.name }}</span>
|
||||||
<span class="tree-name" title="{{ node.name }}">{{ node.name }}</span>
|
<span class="tree-stats">
|
||||||
<span class="tree-stats">
|
{% if stats.total_lessons %}{{ stats.completed_lessons }}/{{ stats.total_lessons }}{% else %}Empty{% endif %}
|
||||||
{% if stats.total_lessons %}{{ stats.completed_lessons }}/{{ stats.total_lessons }} watched{% else %}Empty{% endif %}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% if node.children or node.lessons %}
|
|
||||||
<button class="tree-toggle">▶</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if node.children or node.lessons %}
|
{% if node.children or node.lessons %}
|
||||||
@@ -44,31 +46,21 @@
|
|||||||
{% set is_current = current_lesson and lesson.path == current_lesson.path %}
|
{% set is_current = current_lesson and lesson.path == current_lesson.path %}
|
||||||
<div class="lesson-item {% if is_current %}current{% elif lesson.completed %}completed{% elif percent_watched %}in-progress{% endif %}"
|
<div class="lesson-item {% if is_current %}current{% elif lesson.completed %}completed{% elif percent_watched %}in-progress{% endif %}"
|
||||||
{% if not is_current %}onclick="window.location.href='/lesson/{{ lesson_relative_path }}/{{ lesson.title|replace(' ', '_') }}'"{% endif %}>
|
{% if not is_current %}onclick="window.location.href='/lesson/{{ lesson_relative_path }}/{{ lesson.title|replace(' ', '_') }}'"{% endif %}>
|
||||||
<div class="lesson-title">
|
<span class="lesson-icon">
|
||||||
<span class="lesson-icon">
|
{% if lesson.lesson_type == 'video' %}{{ icons.icon('video', 15) }}
|
||||||
{% if lesson.lesson_type == 'video' %}{{ icons.icon('video', 16) }}
|
{% elif lesson.lesson_type == 'audio' %}{{ icons.icon('music', 15) }}
|
||||||
{% elif lesson.lesson_type == 'audio' %}{{ icons.icon('music', 16) }}
|
{% elif lesson.lesson_type == 'quiz' %}{{ icons.icon('clipboard', 15) }}
|
||||||
{% elif lesson.lesson_type == 'quiz' %}{{ icons.icon('clipboard', 16) }}
|
{% elif lesson.lesson_type == 'mixed' %}{{ icons.icon('package', 15) }}
|
||||||
{% elif lesson.lesson_type == 'mixed' %}{{ icons.icon('package', 16) }}
|
{% else %}{{ icons.icon('file-text', 15) }}{% endif %}
|
||||||
{% else %}{{ icons.icon('file-text', 16) }}{% endif %}
|
</span>
|
||||||
</span>
|
<span class="lesson-name" title="{{ lesson.title }}">{{ lesson.title }}</span>
|
||||||
<span class="lesson-name" title="{{ lesson.title }}">{{ lesson.title }}</span>
|
{% if lesson.duration_seconds and lesson.duration_seconds >= 60 %}
|
||||||
</div>
|
<span class="lesson-duration">{{ lesson.duration_seconds|format_duration }}</span>
|
||||||
<div class="lesson-meta">
|
{% endif %}
|
||||||
<span class="lesson-type {{ lesson.lesson_type }}">{{ lesson.lesson_type|title }}</span>
|
{% if lesson.completed %}
|
||||||
{% if lesson.duration_seconds and lesson.duration_seconds >= 60 %}
|
<span class="status-icon completed">{{ icons.icon('check', 13) }}</span>
|
||||||
<span class="lesson-duration">{{ lesson.duration_seconds|format_duration }}</span>
|
{% elif percent_watched %}
|
||||||
{% endif %}
|
<span class="watched-badge">{{ percent_watched }}%</span>
|
||||||
{% if lesson.completed %}
|
|
||||||
<span class="status-icon completed">{{ icons.icon('check', 14) }}</span>
|
|
||||||
{% elif percent_watched %}
|
|
||||||
<span class="watched-badge">{{ percent_watched }}% watched</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="status-icon pending">{{ icons.icon('circle', 14) }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if percent_watched %}
|
|
||||||
<div class="lesson-progress-track"><div class="lesson-progress-fill" style="width: {{ percent_watched }}%;"></div></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -682,6 +682,89 @@
|
|||||||
display: block;
|
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 {
|
.library-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -1376,13 +1459,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleTree(element) {
|
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 content = element.nextElementSibling;
|
||||||
const toggle = element.querySelector('.tree-toggle');
|
|
||||||
|
|
||||||
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 (toggle) toggle.textContent = isExpanded ? '▼' : '▶';
|
|
||||||
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
|
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1410,11 +1493,7 @@
|
|||||||
const saved = getExpandedSectionPaths();
|
const saved = getExpandedSectionPaths();
|
||||||
if (!saved.size) return;
|
if (!saved.size) return;
|
||||||
document.querySelectorAll('.tree-content[data-section-path]').forEach(function(content) {
|
document.querySelectorAll('.tree-content[data-section-path]').forEach(function(content) {
|
||||||
if (!saved.has(content.dataset.sectionPath)) return;
|
if (saved.has(content.dataset.sectionPath)) content.classList.add('expanded');
|
||||||
content.classList.add('expanded');
|
|
||||||
const header = content.previousElementSibling;
|
|
||||||
const toggle = header ? header.querySelector('.tree-toggle') : null;
|
|
||||||
if (toggle) toggle.textContent = '▼';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
restoreExpandedSections();
|
restoreExpandedSections();
|
||||||
@@ -1430,9 +1509,6 @@
|
|||||||
const shouldExpand = !contents.every(c => c.classList.contains('expanded'));
|
const shouldExpand = !contents.every(c => c.classList.contains('expanded'));
|
||||||
contents.forEach(function(content) {
|
contents.forEach(function(content) {
|
||||||
content.classList.toggle('expanded', shouldExpand);
|
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 (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, shouldExpand);
|
||||||
});
|
});
|
||||||
if (btnEl) btnEl.textContent = shouldExpand ? 'Collapse all' : 'Expand all';
|
if (btnEl) btnEl.textContent = shouldExpand ? 'Collapse all' : 'Expand all';
|
||||||
|
|||||||
+54
-124
@@ -223,8 +223,10 @@
|
|||||||
/* Course-outline sidebar tree - scoped under .lesson-sidebar so
|
/* Course-outline sidebar tree - scoped under .lesson-sidebar so
|
||||||
these don't collide with this page's own .lesson-title (the H1)
|
these don't collide with this page's own .lesson-title (the H1)
|
||||||
and similarly-named rules. Mirrors course_dashboard.html's tree
|
and similarly-named rules. Mirrors course_dashboard.html's tree
|
||||||
styling (shared macro, see _course_tree.html) at a denser size
|
styling (shared macro, see _course_tree.html): a flat list, no
|
||||||
for the narrower column. */
|
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 {
|
.lesson-sidebar .tree-container-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -246,40 +248,33 @@
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .tree-item {
|
.lesson-sidebar .tree-item {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .tree-header {
|
.lesson-sidebar .tree-header {
|
||||||
background: var(--bg-tertiary);
|
padding: 5px 6px;
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 6px;
|
||||||
transition: background 0.3s;
|
transition: background 0.15s;
|
||||||
border-left: 3px solid #666;
|
|
||||||
}
|
}
|
||||||
.lesson-sidebar .tree-header:hover {
|
.lesson-sidebar .tree-header:hover {
|
||||||
background: var(--bg-tertiary-hover);
|
background: var(--bg-tertiary);
|
||||||
}
|
}
|
||||||
.lesson-sidebar .tree-header.directory {
|
.lesson-sidebar .tree-toggle-icon {
|
||||||
border-left-color: var(--accent);
|
display: inline-block;
|
||||||
}
|
width: 12px;
|
||||||
.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;
|
|
||||||
flex-shrink: 0;
|
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 {
|
.lesson-sidebar .tree-name {
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -288,132 +283,72 @@
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .tree-stats {
|
.lesson-sidebar .tree-stats {
|
||||||
font-size: 0.75em;
|
font-size: 0.7em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-left: 8px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
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 {
|
.lesson-sidebar .tree-content {
|
||||||
margin-left: 16px;
|
margin-left: 14px;
|
||||||
margin-top: 5px;
|
margin-top: 1px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .tree-content.expanded {
|
.lesson-sidebar .tree-content.expanded {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .lesson-item {
|
.lesson-sidebar .lesson-item {
|
||||||
background: var(--bg-tertiary);
|
padding: 4px 6px;
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
margin-bottom: 5px;
|
margin-bottom: 0;
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex: 1;
|
transition: background 0.15s;
|
||||||
min-width: 0;
|
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 {
|
.lesson-sidebar .lesson-icon {
|
||||||
font-size: 1em;
|
font-size: 0.85em;
|
||||||
|
color: var(--text-muted);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .lesson-name {
|
.lesson-sidebar .lesson-name {
|
||||||
font-size: 0.85em;
|
font-size: 0.8em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .lesson-meta {
|
.lesson-sidebar .lesson-item.current .lesson-name {
|
||||||
font-size: 0.75em;
|
font-weight: 600;
|
||||||
color: var(--text-muted);
|
color: var(--accent);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.lesson-sidebar .lesson-type {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
.lesson-sidebar .lesson-duration {
|
.lesson-sidebar .lesson-duration {
|
||||||
font-size: 0.75em;
|
font-size: 0.7em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .status-icon {
|
.lesson-sidebar .status-icon {
|
||||||
font-size: 1.1em;
|
font-size: 0.9em;
|
||||||
font-weight: bold;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.lesson-sidebar .status-icon.completed {
|
.lesson-sidebar .status-icon.completed {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
.lesson-sidebar .status-icon.pending {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
video, audio {
|
video, audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -1262,12 +1197,13 @@
|
|||||||
|
|
||||||
// ---- Course outline sidebar (shared tree markup/macro with the
|
// ---- Course outline sidebar (shared tree markup/macro with the
|
||||||
// dashboard's loaded-course view - see _course_tree.html) ----
|
// 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) {
|
function toggleTree(element) {
|
||||||
const content = element.nextElementSibling;
|
const content = element.nextElementSibling;
|
||||||
const toggleBtn = element.querySelector('.tree-toggle');
|
|
||||||
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 (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
|
if (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, isExpanded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1292,9 +1228,6 @@
|
|||||||
|
|
||||||
function expandSection(content) {
|
function expandSection(content) {
|
||||||
content.classList.add('expanded');
|
content.classList.add('expanded');
|
||||||
const header = content.previousElementSibling;
|
|
||||||
const toggleBtn = header ? header.querySelector('.tree-toggle') : null;
|
|
||||||
if (toggleBtn) toggleBtn.textContent = '▼';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(function restoreExpandedSections() {
|
(function restoreExpandedSections() {
|
||||||
@@ -1334,9 +1267,6 @@
|
|||||||
const shouldExpand = !contents.every(c => c.classList.contains('expanded'));
|
const shouldExpand = !contents.every(c => c.classList.contains('expanded'));
|
||||||
contents.forEach(function(content) {
|
contents.forEach(function(content) {
|
||||||
content.classList.toggle('expanded', shouldExpand);
|
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 (content.dataset.sectionPath) saveSectionExpanded(content.dataset.sectionPath, shouldExpand);
|
||||||
});
|
});
|
||||||
if (btnEl) btnEl.textContent = shouldExpand ? 'Collapse all' : 'Expand all';
|
if (btnEl) btnEl.textContent = shouldExpand ? 'Collapse all' : 'Expand all';
|
||||||
|
|||||||
+19
-13
@@ -164,14 +164,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px 10px;
|
padding: 5px 8px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--bg-tertiary);
|
background: none;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.curate-row:hover {
|
.curate-row:hover {
|
||||||
background: var(--bg-tertiary-hover);
|
background: var(--bg-tertiary);
|
||||||
}
|
}
|
||||||
.curate-row.is-hidden {
|
.curate-row.is-hidden {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
@@ -188,15 +188,24 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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 {
|
.curate-toggle {
|
||||||
font-size: 1.1em;
|
display: inline-block;
|
||||||
width: 18px;
|
width: 12px;
|
||||||
text-align: center;
|
|
||||||
flex-shrink: 0;
|
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 {
|
.curate-children {
|
||||||
margin-left: 22px;
|
margin-left: 16px;
|
||||||
margin-top: 4px;
|
margin-top: 1px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.curate-children.expanded {
|
.curate-children.expanded {
|
||||||
@@ -1456,7 +1465,7 @@
|
|||||||
<div class="curate-row ${hiddenClass}" onclick="toggleCurateDir(this, '${safePath}')">
|
<div class="curate-row ${hiddenClass}" onclick="toggleCurateDir(this, '${safePath}')">
|
||||||
${checkbox}
|
${checkbox}
|
||||||
<div class="curate-row-name">
|
<div class="curate-row-name">
|
||||||
<span class="curate-toggle">▶</span>
|
<span class="curate-toggle">▸</span>
|
||||||
<span>${icon}</span>
|
<span>${icon}</span>
|
||||||
<span class="name">${escapeHtml(item.name)}</span>
|
<span class="name">${escapeHtml(item.name)}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1477,16 +1486,13 @@
|
|||||||
|
|
||||||
function toggleCurateDir(rowEl, path) {
|
function toggleCurateDir(rowEl, path) {
|
||||||
const content = rowEl.nextElementSibling;
|
const content = rowEl.nextElementSibling;
|
||||||
const toggleIcon = rowEl.querySelector('.curate-toggle');
|
|
||||||
if (!content || !content.classList.contains('curate-children')) return;
|
if (!content || !content.classList.contains('curate-children')) return;
|
||||||
|
|
||||||
if (content.classList.contains('expanded')) {
|
if (content.classList.contains('expanded')) {
|
||||||
content.classList.remove('expanded');
|
content.classList.remove('expanded');
|
||||||
if (toggleIcon) toggleIcon.textContent = '▶';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
content.classList.add('expanded');
|
content.classList.add('expanded');
|
||||||
if (toggleIcon) toggleIcon.textContent = '▼';
|
|
||||||
|
|
||||||
if (content.dataset.loaded === 'true') return;
|
if (content.dataset.loaded === 'true') return;
|
||||||
content.innerHTML = '<div class="curate-empty-hint">Loading...</div>';
|
content.innerHTML = '<div class="curate-empty-hint">Loading...</div>';
|
||||||
|
|||||||
Reference in New Issue
Block a user