Visual polish: theme-aware colors, collapsible cards, skeletons, hover consistency
Templates only, no backend changes: - Fixed the real bug: JS-generated status/loading text and a few component styles used hardcoded hex (#007acc, #999/#666, #28a745, #ff6b6b) instead of the CSS variables the theme system already provides, so they didn't track the user's chosen theme/accent - most visible on Light or any named theme other than the default. Added --success/--error to all 5 templates' :root blocks and replaced every non-:root occurrence. Left the .lesson-type badge colors and generic UI chrome grays alone (intentional, not theme-dependent). Verified live by switching to Nord and Light themes. - Dashboard cards (Library Stats, Next Up, Pick Back Up, Recently Added, Continue Watching, Recently Viewed) are now collapsible, state remembered per device via localStorage - the no-course dashboard had grown to 7 stacked cards. - Course page's three action buttons get consistent compact sizing and icons instead of a plain stack. - Library browser and transcript search show shimmering skeleton rows instead of bare "Loading..."/"Searching..." text while a fetch is in flight. - Hover treatment made consistent: transcript results get the same lift .lesson-item already had, heatmap cells get a GitHub-style hover scale-up, and Notes Hub links get a background-chip hover. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -324,6 +324,47 @@ export, a Next Up queue, and an activity heatmap.
|
||||
into the existing Library Stats card, using the same per-day counts the
|
||||
shared scan already computes for the streak stat.
|
||||
|
||||
## Visual polish pass (this session)
|
||||
|
||||
Templates-only, no backend changes. Confirmed by inventory
|
||||
(`grep -noE '#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b'` across all 5 templates)
|
||||
before touching anything, to separate real bugs from the `:root` theme
|
||||
palette definitions themselves.
|
||||
|
||||
- **Theme-aware colors (the actual bug)** — JS-generated status/loading
|
||||
text and a few component styles used hardcoded hex instead of the CSS
|
||||
variables the theme system already provides: `#007acc` instead of
|
||||
`var(--accent)` (the course completion-% number, lesson_view.html's
|
||||
notification banner), `#999`/`#666` instead of `var(--text-muted)` for
|
||||
loading/empty/error hint text, and `#28a745`/`#ff6b6b` (success/error)
|
||||
hardcoded in ~25 places with no shared variable at all. Added
|
||||
`--success`/`--error` to all 5 templates' `:root` blocks (fixed values,
|
||||
not theme-varied - semantic status colors stay recognizable across
|
||||
themes by convention) and replaced every non-`:root` occurrence.
|
||||
Deliberately left alone: the `.lesson-type` badge colors (video/audio/
|
||||
text/quiz/mixed - intentional fixed content-type coding) and generic UI
|
||||
chrome grays (progress-bar track, disabled-button background) unrelated
|
||||
to theme. Verified live by switching to Nord and Light themes and
|
||||
confirming the completion-% number and status text actually track the
|
||||
chosen accent/muted colors instead of staying stuck on the old defaults.
|
||||
- **Dashboard card collapse** — click a card's `<h2>` to collapse it,
|
||||
remembered in `localStorage` per card, no backend involved. Applied to
|
||||
the six stackable no-course-dashboard cards (Library Stats, Next Up,
|
||||
Pick Back Up, Recently Added, Continue Watching, Recently Viewed) - the
|
||||
Library browser card stays always-visible as the primary action.
|
||||
- **Course page action buttons** — the three stats-card buttons (Mark all
|
||||
completed / Add to Next Up / Download Study Guide) now use consistent
|
||||
`.btn-sm` compact sizing with icons, in a `.course-actions` flex row
|
||||
instead of a plain inline-styled stack.
|
||||
- **Skeleton loading states** — the Library browser and transcript search
|
||||
show shimmering placeholder rows (`skeletonRowsHtml()`, pure CSS
|
||||
gradient-position animation) while a fetch is in flight, instead of
|
||||
bare "Loading…"/"Searching…" text.
|
||||
- **Hover consistency** — `.transcript-result` now gets the same lift
|
||||
hover `.lesson-item` already had; `.heatmap-day` gets a GitHub-style
|
||||
hover scale-up; `.note-card-link` (Notes Hub) gets a background-chip
|
||||
hover instead of just underline.
|
||||
|
||||
## Workflow that's been in use
|
||||
Edit locally → `git add . && git commit -m "..." && git push` to the private
|
||||
Gitea repo → redeploy the stack in Dockhand (which builds from the fresh
|
||||
|
||||
+153
-60
@@ -28,6 +28,8 @@
|
||||
--font-size-base: 16px;
|
||||
--container-max-width: 1600px;
|
||||
--radius: 8px;
|
||||
--success: #28a745;
|
||||
--error: #ff6b6b;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f2f2f2;
|
||||
@@ -301,17 +303,77 @@
|
||||
border-radius: 2px;
|
||||
background: var(--accent);
|
||||
opacity: 0.12;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.heatmap-day.level-1 { opacity: 0.4; }
|
||||
.heatmap-day.level-2 { opacity: 0.7; }
|
||||
.heatmap-day.level-3 { opacity: 1; }
|
||||
|
||||
.heatmap-day:hover {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.course-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
height: 48px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 5px;
|
||||
background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-tertiary-hover) 50%, var(--bg-tertiary) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.collapsible-header {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapsible-header::after {
|
||||
content: '▾';
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.collapsible-header.collapsed::after {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
overflow: hidden;
|
||||
max-height: 3000px;
|
||||
transition: max-height 0.3s ease, opacity 0.2s ease, margin-top 0.3s ease;
|
||||
margin-top: 12px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-body.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.library-breadcrumb {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
@@ -375,11 +437,11 @@
|
||||
}
|
||||
|
||||
.tree-header.lesson {
|
||||
border-left-color: #28a745;
|
||||
border-left-color: var(--success);
|
||||
}
|
||||
|
||||
.tree-header.completed {
|
||||
border-left-color: #28a745;
|
||||
border-left-color: var(--success);
|
||||
background: #1e3d1e;
|
||||
}
|
||||
|
||||
@@ -466,7 +528,7 @@
|
||||
}
|
||||
|
||||
.lesson-item.completed {
|
||||
border-left-color: #28a745;
|
||||
border-left-color: var(--success);
|
||||
background: #1e3d1e;
|
||||
}
|
||||
|
||||
@@ -493,7 +555,7 @@
|
||||
}
|
||||
|
||||
.lesson-item.completed .lesson-progress-fill {
|
||||
background: #28a745;
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.watched-badge {
|
||||
@@ -573,10 +635,12 @@
|
||||
margin-bottom: 5px;
|
||||
border-left: 3px solid var(--accent);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.transcript-result:hover {
|
||||
background: var(--bg-tertiary-hover);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.transcript-result-title {
|
||||
@@ -641,16 +705,16 @@
|
||||
}
|
||||
|
||||
.status-icon.completed {
|
||||
color: #28a745;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-icon.pending {
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.last-accessed {
|
||||
background: #2d5a2d;
|
||||
border-left-color: #28a745;
|
||||
border-left-color: var(--success);
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
@@ -722,7 +786,7 @@
|
||||
<div class="container">
|
||||
<a href="/reset_course">← Select Different Course</a>
|
||||
<a href="#stats">Progress</a>
|
||||
<span style="color: #666; margin-left: auto;">
|
||||
<span style="color: var(--text-muted); margin-left: auto;">
|
||||
{{ stats.total_lessons }} lessons
|
||||
</span>
|
||||
</div>
|
||||
@@ -736,7 +800,7 @@
|
||||
<strong>{{ stats.completed_lessons }}/{{ stats.total_lessons }}</strong> lessons completed
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="color: #007acc; font-size: 1.2em;">
|
||||
<div style="color: var(--accent); font-size: 1.2em;">
|
||||
{{ "%.1f"|format(stats.completion_percentage) }}%
|
||||
</div>
|
||||
{% if stats.remaining_display %}
|
||||
@@ -751,15 +815,15 @@
|
||||
</div>
|
||||
|
||||
{% if stats.total_lessons %}
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 5px;">
|
||||
<button class="btn btn-secondary" onclick="markCourseWatched()">
|
||||
Mark all as completed
|
||||
<div class="course-actions">
|
||||
<button class="btn btn-secondary btn-sm" onclick="markCourseWatched()">
|
||||
✓ Mark all as completed
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="queue-toggle-btn" onclick="toggleQueued()">
|
||||
<button class="btn btn-secondary btn-sm" id="queue-toggle-btn" onclick="toggleQueued()">
|
||||
{{ '📌 Remove from Next Up' if is_queued else '📌 Add to Next Up' }}
|
||||
</button>
|
||||
{% if has_note %}
|
||||
<a class="btn btn-secondary" href="/course/study-guide">Download Study Guide</a>
|
||||
<a class="btn btn-secondary btn-sm" href="/course/study-guide">📄 Download Study Guide</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -879,48 +943,50 @@
|
||||
{% if library_stats and library_stats.total_courses %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2>📊 Library Stats</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.total_courses }}</div>
|
||||
<div class="stats-tile-label">Courses</div>
|
||||
<h2 class="collapsible-header" data-card-id="library-stats">📊 Library Stats</h2>
|
||||
<div class="card-body">
|
||||
<div class="stats-grid">
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.total_courses }}</div>
|
||||
<div class="stats-tile-label">Courses</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.completed_lessons }}/{{ library_stats.lessons_tracked }}</div>
|
||||
<div class="stats-tile-label">Lessons completed</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.watched_display }}</div>
|
||||
<div class="stats-tile-label">Watched</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.streak_days }}</div>
|
||||
<div class="stats-tile-label">Day streak</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.completed_lessons }}/{{ library_stats.lessons_tracked }}</div>
|
||||
<div class="stats-tile-label">Lessons completed</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.watched_display }}</div>
|
||||
<div class="stats-tile-label">Watched</div>
|
||||
</div>
|
||||
<div class="stats-tile">
|
||||
<div class="stats-tile-value">{{ library_stats.streak_days }}</div>
|
||||
<div class="stats-tile-label">Day streak</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if activity_heatmap %}
|
||||
<div class="heatmap-scroll">
|
||||
<div class="heatmap-grid">
|
||||
{% for week in activity_heatmap|batch(7) %}
|
||||
<div class="heatmap-week">
|
||||
{% for day in week %}
|
||||
{% set level = 0 if day.count == 0 else (1 if day.count == 1 else (2 if day.count == 2 else 3)) %}
|
||||
<div class="heatmap-day level-{{ level }}"
|
||||
title="{{ day.date }}: {{ day.count }} lesson{{ '' if day.count == 1 else 's' }}"></div>
|
||||
{% if activity_heatmap %}
|
||||
<div class="heatmap-scroll">
|
||||
<div class="heatmap-grid">
|
||||
{% for week in activity_heatmap|batch(7) %}
|
||||
<div class="heatmap-week">
|
||||
{% for day in week %}
|
||||
{% set level = 0 if day.count == 0 else (1 if day.count == 1 else (2 if day.count == 2 else 3)) %}
|
||||
<div class="heatmap-day level-{{ level }}"
|
||||
title="{{ day.date }}: {{ day.count }} lesson{{ '' if day.count == 1 else 's' }}"></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if next_up %}
|
||||
<div class="container">
|
||||
<div class="card" id="next-up-card">
|
||||
<h2>📌 Next Up</h2>
|
||||
<div id="next-up-list" style="margin-top: 12px;">
|
||||
<h2 class="collapsible-header" data-card-id="next-up">📌 Next Up</h2>
|
||||
<div class="card-body" id="next-up-list">
|
||||
{% for item in next_up %}
|
||||
<div class="lesson-item" data-path="{{ item.path }}">
|
||||
<div class="lesson-title" style="cursor: pointer;" onclick="loadCoursePath('{{ item.path|replace("'", "\\'") }}')">
|
||||
@@ -946,8 +1012,8 @@
|
||||
{% if stale_courses %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2>⏰ Pick Back Up</h2>
|
||||
<div style="margin-top: 12px;">
|
||||
<h2 class="collapsible-header" data-card-id="pick-back-up">⏰ Pick Back Up</h2>
|
||||
<div class="card-body">
|
||||
{% for item in stale_courses %}
|
||||
{{ render_course_row(item, 'Last touched ' + item.last_touched_display) }}
|
||||
{% endfor %}
|
||||
@@ -958,8 +1024,8 @@
|
||||
{% if recently_added %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2>🆕 Recently Added</h2>
|
||||
<div style="margin-top: 12px;">
|
||||
<h2 class="collapsible-header" data-card-id="recently-added">🆕 Recently Added</h2>
|
||||
<div class="card-body">
|
||||
{% for item in recently_added %}
|
||||
{{ render_course_row(item, item.added_display) }}
|
||||
{% endfor %}
|
||||
@@ -970,8 +1036,8 @@
|
||||
{% if continue_watching %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2>▶ Continue Watching</h2>
|
||||
<div style="margin-top: 12px;">
|
||||
<h2 class="collapsible-header" data-card-id="continue-watching">▶ Continue Watching</h2>
|
||||
<div class="card-body">
|
||||
{% for view in continue_watching %}
|
||||
{{ render_view_row(view) }}
|
||||
{% endfor %}
|
||||
@@ -982,8 +1048,8 @@
|
||||
{% if recent_views %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h2>🕐 Recently Viewed</h2>
|
||||
<div style="margin-top: 12px;">
|
||||
<h2 class="collapsible-header" data-card-id="recently-viewed">🕐 Recently Viewed</h2>
|
||||
<div class="card-body">
|
||||
{% for view in recent_views %}
|
||||
{{ render_view_row(view) }}
|
||||
{% endfor %}
|
||||
@@ -1077,7 +1143,7 @@
|
||||
if (transcriptResults) transcriptResults.innerHTML = '';
|
||||
|
||||
const container = document.getElementById('library-groups');
|
||||
container.innerHTML = '<p style="color:#999; padding: 6px 0;">Loading...</p>';
|
||||
container.innerHTML = skeletonRowsHtml(3);
|
||||
const url = path ? `/library?path=${encodeURIComponent(path)}` : '/library';
|
||||
fetch(url)
|
||||
.then(r => r.json())
|
||||
@@ -1087,7 +1153,7 @@
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML =
|
||||
'<p style="color:#ff6b6b;">Could not reach the library scanner.</p>';
|
||||
'<p style="color:var(--error);">Could not reach the library scanner.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1125,7 +1191,7 @@
|
||||
const reason = (data.errors && data.errors.length)
|
||||
? data.errors.join(' ')
|
||||
: (isRoot ? `No course folders found under ${data.library_path}.` : 'No courses in here.');
|
||||
container.innerHTML = `<p style="color:#999; padding: 6px 0;">${reason}${isRoot ? ' You can still load a course by path below.' : ''}</p>`;
|
||||
container.innerHTML = `<p style="color:var(--text-muted); padding: 6px 0;">${reason}${isRoot ? ' You can still load a course by path below.' : ''}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1138,6 +1204,12 @@
|
||||
fetchLibraryLevel(path);
|
||||
}
|
||||
|
||||
// A few shimmering placeholder rows for in-flight fetches, instead
|
||||
// of bare "Loading..." text.
|
||||
function skeletonRowsHtml(count) {
|
||||
return Array(count).fill('<div class="skeleton-row"></div>').join('');
|
||||
}
|
||||
|
||||
// Fallback if a course's thumbnail image 404s/errors after the row
|
||||
// already rendered (e.g. the file was removed on disk in between) -
|
||||
// swaps back to the plain icon rather than showing a broken image.
|
||||
@@ -1215,7 +1287,7 @@
|
||||
searchActive = true;
|
||||
const container = document.getElementById('library-groups');
|
||||
const transcriptContainer = document.getElementById('transcript-results');
|
||||
container.innerHTML = '<p style="color:#999; padding: 6px 0;">Searching...</p>';
|
||||
container.innerHTML = skeletonRowsHtml(2);
|
||||
if (transcriptContainer) transcriptContainer.innerHTML = '';
|
||||
|
||||
fetch(`/library/search?q=${encodeURIComponent(query)}`)
|
||||
@@ -1225,13 +1297,13 @@
|
||||
`<span class="crumb current" style="max-width: none;">Search results for "${query}"</span>`;
|
||||
lastLibraryItems = data.results;
|
||||
if (!data.results.length) {
|
||||
container.innerHTML = `<p style="color:#999; padding: 6px 0;">No courses match "${query}".</p>`;
|
||||
container.innerHTML = `<p style="color:var(--text-muted); padding: 6px 0;">No courses match "${query}".</p>`;
|
||||
return;
|
||||
}
|
||||
renderItemRows(sortLibraryItems(data.results));
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML = '<p style="color:#ff6b6b;">Search failed.</p>';
|
||||
container.innerHTML = '<p style="color:var(--error);">Search failed.</p>';
|
||||
});
|
||||
|
||||
const searchTranscripts = document.getElementById('search-transcripts-toggle');
|
||||
@@ -1243,7 +1315,7 @@
|
||||
function runTranscriptSearch(query) {
|
||||
const transcriptContainer = document.getElementById('transcript-results');
|
||||
if (!transcriptContainer) return;
|
||||
transcriptContainer.innerHTML = '<p style="color:#999; padding: 6px 0;">Searching transcripts...</p>';
|
||||
transcriptContainer.innerHTML = skeletonRowsHtml(2);
|
||||
fetch(`/library/search-transcripts?q=${encodeURIComponent(query)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
@@ -1373,7 +1445,28 @@
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// ---- Collapsible dashboard cards (client-only, remembered per device) ----
|
||||
function initCollapsibleCards() {
|
||||
document.querySelectorAll('.collapsible-header').forEach(function(header) {
|
||||
const cardId = header.dataset.cardId;
|
||||
const body = header.nextElementSibling;
|
||||
if (!cardId || !body) return;
|
||||
|
||||
if (localStorage.getItem('offlineu-collapsed-' + cardId) === '1') {
|
||||
header.classList.add('collapsed');
|
||||
body.classList.add('collapsed');
|
||||
}
|
||||
|
||||
header.addEventListener('click', function() {
|
||||
const isCollapsed = body.classList.toggle('collapsed');
|
||||
header.classList.toggle('collapsed', isCollapsed);
|
||||
localStorage.setItem('offlineu-collapsed-' + cardId, isCollapsed ? '1' : '0');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadLibrary);
|
||||
document.addEventListener('DOMContentLoaded', initCollapsibleCards);
|
||||
|
||||
</script>
|
||||
<script src="/static/theme.js"></script>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
--font-size-base: 16px;
|
||||
--container-max-width: 1600px;
|
||||
--radius: 8px;
|
||||
--success: #28a745;
|
||||
--error: #ff6b6b;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f2f2f2;
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
--font-size-base: 16px;
|
||||
--container-max-width: 1600px;
|
||||
--radius: 8px;
|
||||
--success: #28a745;
|
||||
--error: #ff6b6b;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f2f2f2;
|
||||
@@ -364,7 +366,7 @@
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h4>{{ text_file.split('/')[-1] }}</h4>
|
||||
<div class="text-content" id="content-{{ loop.index0 }}">
|
||||
<div style="text-align: center; color: #666;">Loading content...</div>
|
||||
<div style="text-align: center; color: var(--text-muted);">Loading content...</div>
|
||||
</div>
|
||||
<a href="/files/{{ text_file|replace('\\', '/') }}" class="file-link" target="_blank">
|
||||
📄 Open {{ text_file.split('/')[-1] }} in new tab
|
||||
@@ -450,7 +452,7 @@
|
||||
// fetching them as text would show garbled binary content,
|
||||
// so just point at the download/open link instead.
|
||||
contentDiv.innerHTML =
|
||||
'<div style="color: #999;">Preview isn\'t available for this file type. ' +
|
||||
'<div style="color: var(--text-muted);">Preview isn\'t available for this file type. ' +
|
||||
'Use the "Open in new tab" link below to view or download it.</div>';
|
||||
} else {
|
||||
// For text files, fetch and display content
|
||||
@@ -479,8 +481,8 @@
|
||||
console.error('Error loading content:', error);
|
||||
if (contentDiv) {
|
||||
contentDiv.innerHTML =
|
||||
'<div style="color: #ff6b6b;">Error loading content: ' + error.message + '</div>' +
|
||||
'<div style="color: #999; font-size: 0.9em; margin-top: 10px;">File path: ' + filePath + '</div>';
|
||||
'<div style="color: var(--error);">Error loading content: ' + error.message + '</div>' +
|
||||
'<div style="color: var(--text-muted); font-size: 0.9em; margin-top: 10px;">File path: ' + filePath + '</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -762,7 +764,7 @@
|
||||
const btn = document.querySelector('button[onclick="markCompleted()"]');
|
||||
if (btn) {
|
||||
btn.textContent = 'Completed ✓';
|
||||
btn.style.background = '#28a745';
|
||||
btn.style.background = 'var(--success)';
|
||||
btn.disabled = true;
|
||||
}
|
||||
isCompleted = true;
|
||||
@@ -775,7 +777,7 @@
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${type === 'success' ? '#28a745' : '#007acc'};
|
||||
background: ${type === 'success' ? 'var(--success)' : 'var(--accent)'};
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 5px;
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
--font-size-base: 16px;
|
||||
--container-max-width: 1600px;
|
||||
--radius: 8px;
|
||||
--success: #28a745;
|
||||
--error: #ff6b6b;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f2f2f2;
|
||||
@@ -171,13 +173,19 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
.note-card-link {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
font-size: 0.85em;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.note-card-link:hover {
|
||||
background: var(--bg-tertiary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.note-card-link:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+21
-19
@@ -22,6 +22,8 @@
|
||||
--font-size-base: 16px;
|
||||
--container-max-width: 1600px;
|
||||
--radius: 8px;
|
||||
--success: #28a745;
|
||||
--error: #ff6b6b;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f2f2f2;
|
||||
@@ -279,7 +281,7 @@
|
||||
.curate-row-status {
|
||||
flex-basis: 100%;
|
||||
font-size: 0.8em;
|
||||
color: #ff6b6b;
|
||||
color: var(--error);
|
||||
}
|
||||
.bulk-rename-row {
|
||||
display: flex;
|
||||
@@ -307,7 +309,7 @@
|
||||
}
|
||||
#save-status {
|
||||
font-size: 0.9em;
|
||||
color: #28a745;
|
||||
color: var(--success);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
@@ -658,7 +660,7 @@
|
||||
renderCurateLevel(data, container);
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML = '<p style="color:#ff6b6b;">Could not reach the library scanner.</p>';
|
||||
container.innerHTML = '<p style="color:var(--error);">Could not reach the library scanner.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -834,7 +836,7 @@
|
||||
const preview = document.getElementById('bulk-rename-preview');
|
||||
preview.innerHTML = '';
|
||||
if (!pattern) {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Enter text to find first.';
|
||||
return;
|
||||
}
|
||||
@@ -847,7 +849,7 @@
|
||||
renderBulkRenamePreview();
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server.';
|
||||
});
|
||||
}
|
||||
@@ -898,7 +900,7 @@
|
||||
const failed = results.filter(r => !r.success);
|
||||
const activeCourseReset = results.some(r => r.active_course_reset);
|
||||
|
||||
status.style.color = failed.length ? '#ff6b6b' : '#28a745';
|
||||
status.style.color = failed.length ? 'var(--error)' : 'var(--success)';
|
||||
let message = `${succeeded} renamed`;
|
||||
if (failed.length) {
|
||||
message += `, ${failed.length} failed: ` +
|
||||
@@ -919,7 +921,7 @@
|
||||
loadCurateLevel(null, document.getElementById('curate-tree'), true);
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server.';
|
||||
});
|
||||
}
|
||||
@@ -935,16 +937,16 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
status.style.color = '#28a745';
|
||||
status.style.color = 'var(--success)';
|
||||
status.textContent = '✓ Saved — reload the main page to see it take effect';
|
||||
showSaved();
|
||||
} else {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = data.error || 'Could not save';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server';
|
||||
});
|
||||
}
|
||||
@@ -955,7 +957,7 @@
|
||||
const coursePath = input.value.trim();
|
||||
|
||||
if (!coursePath) {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Please enter a course path.';
|
||||
return;
|
||||
}
|
||||
@@ -971,16 +973,16 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
status.style.color = '#28a745';
|
||||
status.style.color = 'var(--success)';
|
||||
status.textContent = `✓ Loaded "${data.course_name}" — redirecting...`;
|
||||
setTimeout(() => { window.location.href = '/'; }, 1000);
|
||||
} else {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = data.error || 'Could not load course';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server';
|
||||
});
|
||||
}
|
||||
@@ -1055,17 +1057,17 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
status.style.color = '#28a745';
|
||||
status.style.color = 'var(--success)';
|
||||
status.textContent = 'Saved.';
|
||||
document.getElementById('outline-token').value = '';
|
||||
loadOutlineConfig();
|
||||
} else {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = data.error || 'Could not save.';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server.';
|
||||
});
|
||||
}
|
||||
@@ -1077,11 +1079,11 @@
|
||||
fetch('/api/outline/test')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
status.style.color = data.success ? '#28a745' : '#ff6b6b';
|
||||
status.style.color = data.success ? 'var(--success)' : 'var(--error)';
|
||||
status.textContent = data.success ? 'Connected.' : (data.error || 'Connection failed.');
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = '#ff6b6b';
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server.';
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user