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:
2026-08-26 10:45:38 -04:00
co-authored by Claude Sonnet 5
parent 016acacdb5
commit bf0a02d008
7 changed files with 197 additions and 187 deletions
+1
View File
@@ -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
+7 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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 %}
+88 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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>';