Spaces:
Running
Running
Upload 6 files
Browse files- index.html +11 -5
- scripts/app.js +85 -7
- scripts/sidebar.js +30 -7
- styles/main.css +124 -2
index.html
CHANGED
|
@@ -60,7 +60,8 @@
|
|
| 60 |
</nav>
|
| 61 |
<div class="header-actions">
|
| 62 |
<button class="btn-icon" id="btn-theme" title="Toggle theme">π</button>
|
| 63 |
-
<button class="btn-icon" id="btn-refresh"
|
|
|
|
| 64 |
<button class="btn-icon" id="btn-settings" title="Settings">βοΈ</button>
|
| 65 |
<button class="btn-icon" id="btn-about" title="About">π</button>
|
| 66 |
</div>
|
|
@@ -106,7 +107,7 @@
|
|
| 106 |
<!-- Bookmarks -->
|
| 107 |
<section class="sidebar-section">
|
| 108 |
<h3 class="sidebar-title collapsible" data-target="bookmarks-content">
|
| 109 |
-
π Bookmarks
|
| 110 |
<span class="section-toggle">βΌ</span>
|
| 111 |
</h3>
|
| 112 |
<div class="section-content" id="bookmarks-content">
|
|
@@ -161,6 +162,9 @@
|
|
| 161 |
<!-- Sidebar Toggle (Mobile) -->
|
| 162 |
<button class="sidebar-toggle" id="sidebar-toggle" title="Toggle Sidebar">π</button>
|
| 163 |
|
|
|
|
|
|
|
|
|
|
| 164 |
<!-- Footer -->
|
| 165 |
<footer class="footer">
|
| 166 |
<p>
|
|
@@ -182,6 +186,8 @@
|
|
| 182 |
<kbd>S</kbd> Sidebar
|
| 183 |
<span class="divider">β’</span>
|
| 184 |
<kbd>T</kbd> Theme
|
|
|
|
|
|
|
| 185 |
</div>
|
| 186 |
|
| 187 |
<!-- About Modal -->
|
|
@@ -287,10 +293,10 @@
|
|
| 287 |
<input type="checkbox" id="opt-show-descriptions" checked />
|
| 288 |
Show article descriptions
|
| 289 |
</label>
|
| 290 |
-
<
|
|
|
|
| 291 |
<input type="number" id="opt-max-items" value="10" min="5" max="100" />
|
| 292 |
-
|
| 293 |
-
</label>
|
| 294 |
</section>
|
| 295 |
</div>
|
| 296 |
</div>
|
|
|
|
| 60 |
</nav>
|
| 61 |
<div class="header-actions">
|
| 62 |
<button class="btn-icon" id="btn-theme" title="Toggle theme">π</button>
|
| 63 |
+
<button class="btn-icon" id="btn-refresh"
|
| 64 |
+
title="Refresh feeds (Shift+Click = force refresh)">π</button>
|
| 65 |
<button class="btn-icon" id="btn-settings" title="Settings">βοΈ</button>
|
| 66 |
<button class="btn-icon" id="btn-about" title="About">π</button>
|
| 67 |
</div>
|
|
|
|
| 107 |
<!-- Bookmarks -->
|
| 108 |
<section class="sidebar-section">
|
| 109 |
<h3 class="sidebar-title collapsible" data-target="bookmarks-content">
|
| 110 |
+
π Bookmarks <span class="bookmark-count" id="bookmark-count"></span>
|
| 111 |
<span class="section-toggle">βΌ</span>
|
| 112 |
</h3>
|
| 113 |
<div class="section-content" id="bookmarks-content">
|
|
|
|
| 162 |
<!-- Sidebar Toggle (Mobile) -->
|
| 163 |
<button class="sidebar-toggle" id="sidebar-toggle" title="Toggle Sidebar">π</button>
|
| 164 |
|
| 165 |
+
<!-- Back to Top Button -->
|
| 166 |
+
<button class="back-to-top" id="back-to-top" title="Back to top" aria-label="Scroll back to top">β</button>
|
| 167 |
+
|
| 168 |
<!-- Footer -->
|
| 169 |
<footer class="footer">
|
| 170 |
<p>
|
|
|
|
| 186 |
<kbd>S</kbd> Sidebar
|
| 187 |
<span class="divider">β’</span>
|
| 188 |
<kbd>T</kbd> Theme
|
| 189 |
+
<span class="divider">β’</span>
|
| 190 |
+
<kbd>β</kbd> Top
|
| 191 |
</div>
|
| 192 |
|
| 193 |
<!-- About Modal -->
|
|
|
|
| 293 |
<input type="checkbox" id="opt-show-descriptions" checked />
|
| 294 |
Show article descriptions
|
| 295 |
</label>
|
| 296 |
+
<div class="number-input-row">
|
| 297 |
+
<label for="opt-max-items">Max items per source</label>
|
| 298 |
<input type="number" id="opt-max-items" value="10" min="5" max="100" />
|
| 299 |
+
</div>
|
|
|
|
| 300 |
</section>
|
| 301 |
</div>
|
| 302 |
</div>
|
scripts/app.js
CHANGED
|
@@ -269,10 +269,12 @@ class RSSHubApp {
|
|
| 269 |
return;
|
| 270 |
}
|
| 271 |
|
| 272 |
-
// Escape to close modals
|
| 273 |
if (e.key === "Escape") {
|
| 274 |
this.elements.modalAbout.classList.remove("active");
|
| 275 |
this.elements.modalSettings.classList.remove("active");
|
|
|
|
|
|
|
| 276 |
}
|
| 277 |
|
| 278 |
// R to refresh (Shift+R for force refresh)
|
|
@@ -311,8 +313,17 @@ class RSSHubApp {
|
|
| 311 |
if (e.key === "t" && !e.ctrlKey && !e.metaKey) {
|
| 312 |
this.toggleTheme();
|
| 313 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
});
|
| 315 |
|
|
|
|
|
|
|
|
|
|
| 316 |
// Category navigation
|
| 317 |
this.elements.navButtons.forEach(btn => {
|
| 318 |
btn.addEventListener("click", () => {
|
|
@@ -416,6 +427,51 @@ class RSSHubApp {
|
|
| 416 |
);
|
| 417 |
}
|
| 418 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
/**
|
| 420 |
* Set active language filter
|
| 421 |
*/
|
|
@@ -898,30 +954,53 @@ class RSSHubApp {
|
|
| 898 |
}
|
| 899 |
|
| 900 |
/**
|
| 901 |
-
* Add custom feed
|
| 902 |
*/
|
| 903 |
-
addCustomFeed() {
|
| 904 |
const nameInput = document.getElementById("custom-feed-name");
|
| 905 |
const urlInput = document.getElementById("custom-feed-url");
|
| 906 |
const categorySelect = document.getElementById("custom-feed-category");
|
|
|
|
| 907 |
|
| 908 |
const name = nameInput.value.trim();
|
| 909 |
const url = urlInput.value.trim();
|
| 910 |
const category = categorySelect.value;
|
| 911 |
|
| 912 |
if (!name || !url) {
|
| 913 |
-
|
| 914 |
return;
|
| 915 |
}
|
| 916 |
|
| 917 |
-
// Validate URL
|
| 918 |
try {
|
| 919 |
new URL(url);
|
| 920 |
} catch {
|
| 921 |
-
|
| 922 |
return;
|
| 923 |
}
|
| 924 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 925 |
// Generate unique ID
|
| 926 |
const id = "custom_" + Date.now();
|
| 927 |
|
|
@@ -1088,4 +1167,3 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 1088 |
app = new RSSHubApp();
|
| 1089 |
window.app = app; // Expose for event handlers
|
| 1090 |
});
|
| 1091 |
-
|
|
|
|
| 269 |
return;
|
| 270 |
}
|
| 271 |
|
| 272 |
+
// Escape to close modals and sidebar
|
| 273 |
if (e.key === "Escape") {
|
| 274 |
this.elements.modalAbout.classList.remove("active");
|
| 275 |
this.elements.modalSettings.classList.remove("active");
|
| 276 |
+
// Also close mobile sidebar
|
| 277 |
+
this.elements.sidebar?.classList.remove("open");
|
| 278 |
}
|
| 279 |
|
| 280 |
// R to refresh (Shift+R for force refresh)
|
|
|
|
| 313 |
if (e.key === "t" && !e.ctrlKey && !e.metaKey) {
|
| 314 |
this.toggleTheme();
|
| 315 |
}
|
| 316 |
+
|
| 317 |
+
// Home or ArrowUp (when not in input) to scroll to top
|
| 318 |
+
if (e.key === "Home" || (e.key === "ArrowUp" && e.ctrlKey)) {
|
| 319 |
+
e.preventDefault();
|
| 320 |
+
this.scrollToTop();
|
| 321 |
+
}
|
| 322 |
});
|
| 323 |
|
| 324 |
+
// Back to top button
|
| 325 |
+
this.setupBackToTop();
|
| 326 |
+
|
| 327 |
// Category navigation
|
| 328 |
this.elements.navButtons.forEach(btn => {
|
| 329 |
btn.addEventListener("click", () => {
|
|
|
|
| 427 |
);
|
| 428 |
}
|
| 429 |
|
| 430 |
+
/**
|
| 431 |
+
* Setup Back to Top button behavior
|
| 432 |
+
*/
|
| 433 |
+
setupBackToTop() {
|
| 434 |
+
const backToTopBtn = document.getElementById("back-to-top");
|
| 435 |
+
if (!backToTopBtn) return;
|
| 436 |
+
|
| 437 |
+
// Show/hide button based on scroll position
|
| 438 |
+
let ticking = false;
|
| 439 |
+
window.addEventListener(
|
| 440 |
+
"scroll",
|
| 441 |
+
() => {
|
| 442 |
+
if (!ticking) {
|
| 443 |
+
requestAnimationFrame(() => {
|
| 444 |
+
const scrollY = window.scrollY || document.documentElement.scrollTop;
|
| 445 |
+
// Show button after scrolling 400px
|
| 446 |
+
if (scrollY > 400) {
|
| 447 |
+
backToTopBtn.classList.add("visible");
|
| 448 |
+
} else {
|
| 449 |
+
backToTopBtn.classList.remove("visible");
|
| 450 |
+
}
|
| 451 |
+
ticking = false;
|
| 452 |
+
});
|
| 453 |
+
ticking = true;
|
| 454 |
+
}
|
| 455 |
+
},
|
| 456 |
+
{ passive: true }
|
| 457 |
+
);
|
| 458 |
+
|
| 459 |
+
// Click handler
|
| 460 |
+
backToTopBtn.addEventListener("click", () => {
|
| 461 |
+
this.scrollToTop();
|
| 462 |
+
});
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
/**
|
| 466 |
+
* Smooth scroll to top of page
|
| 467 |
+
*/
|
| 468 |
+
scrollToTop() {
|
| 469 |
+
window.scrollTo({
|
| 470 |
+
top: 0,
|
| 471 |
+
behavior: "smooth"
|
| 472 |
+
});
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
/**
|
| 476 |
* Set active language filter
|
| 477 |
*/
|
|
|
|
| 954 |
}
|
| 955 |
|
| 956 |
/**
|
| 957 |
+
* Add custom feed with validation
|
| 958 |
*/
|
| 959 |
+
async addCustomFeed() {
|
| 960 |
const nameInput = document.getElementById("custom-feed-name");
|
| 961 |
const urlInput = document.getElementById("custom-feed-url");
|
| 962 |
const categorySelect = document.getElementById("custom-feed-category");
|
| 963 |
+
const addBtn = document.getElementById("btn-add-feed");
|
| 964 |
|
| 965 |
const name = nameInput.value.trim();
|
| 966 |
const url = urlInput.value.trim();
|
| 967 |
const category = categorySelect.value;
|
| 968 |
|
| 969 |
if (!name || !url) {
|
| 970 |
+
this.showToast("Please enter both name and URL", "error");
|
| 971 |
return;
|
| 972 |
}
|
| 973 |
|
| 974 |
+
// Validate URL format
|
| 975 |
try {
|
| 976 |
new URL(url);
|
| 977 |
} catch {
|
| 978 |
+
this.showToast("Please enter a valid URL", "error");
|
| 979 |
return;
|
| 980 |
}
|
| 981 |
|
| 982 |
+
// Show loading state
|
| 983 |
+
const originalText = addBtn.textContent;
|
| 984 |
+
addBtn.textContent = "Testing...";
|
| 985 |
+
addBtn.disabled = true;
|
| 986 |
+
|
| 987 |
+
// Test if the URL is actually a valid RSS feed
|
| 988 |
+
try {
|
| 989 |
+
const result = await this.parser.fetchFeed({ url, name, id: "test" });
|
| 990 |
+
if (result.status !== "success") {
|
| 991 |
+
throw new Error(result.error || "Invalid RSS feed");
|
| 992 |
+
}
|
| 993 |
+
} catch (e) {
|
| 994 |
+
addBtn.textContent = originalText;
|
| 995 |
+
addBtn.disabled = false;
|
| 996 |
+
this.showToast(`Invalid feed: ${e.message}`, "error");
|
| 997 |
+
return;
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
// Restore button
|
| 1001 |
+
addBtn.textContent = originalText;
|
| 1002 |
+
addBtn.disabled = false;
|
| 1003 |
+
|
| 1004 |
// Generate unique ID
|
| 1005 |
const id = "custom_" + Date.now();
|
| 1006 |
|
|
|
|
| 1167 |
app = new RSSHubApp();
|
| 1168 |
window.app = app; // Expose for event handlers
|
| 1169 |
});
|
|
|
scripts/sidebar.js
CHANGED
|
@@ -86,12 +86,26 @@ class SidebarManager {
|
|
| 86 |
this.handleSearch("");
|
| 87 |
});
|
| 88 |
|
| 89 |
-
// Clear bookmarks
|
| 90 |
-
this.elements.btnClearBookmarks?.addEventListener("click",
|
| 91 |
-
|
|
|
|
| 92 |
this.bookmarks = [];
|
| 93 |
this.saveBookmarks();
|
| 94 |
this.renderBookmarks();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
});
|
| 97 |
}
|
|
@@ -146,7 +160,8 @@ class SidebarManager {
|
|
| 146 |
}
|
| 147 |
|
| 148 |
// Show loading indicator immediately for better UX
|
| 149 |
-
this.elements.searchResults.innerHTML =
|
|
|
|
| 150 |
|
| 151 |
const queryLower = query.toLowerCase();
|
| 152 |
const results = this.allArticles
|
|
@@ -179,7 +194,8 @@ class SidebarManager {
|
|
| 179 |
|
| 180 |
highlightMatch(text, query) {
|
| 181 |
const escaped = this.escapeHtml(text);
|
| 182 |
-
const
|
|
|
|
| 183 |
return escaped.replace(regex, "<mark>$1</mark>");
|
| 184 |
}
|
| 185 |
|
|
@@ -237,6 +253,12 @@ class SidebarManager {
|
|
| 237 |
}
|
| 238 |
|
| 239 |
renderBookmarks() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
if (this.bookmarks.length === 0) {
|
| 241 |
this.elements.bookmarksList.innerHTML =
|
| 242 |
'<span class="empty-hint">No bookmarks yet. Click β on articles to save them.</span>';
|
|
@@ -443,9 +465,10 @@ class SidebarManager {
|
|
| 443 |
const isHot = count >= maxCount * 0.7;
|
| 444 |
return `
|
| 445 |
<button class="trending-tag ${isHot ? "hot" : ""}"
|
| 446 |
-
onclick="sidebar.filterByTag('${this.escapeHtml(word)}')"
|
|
|
|
| 447 |
${word}
|
| 448 |
-
<span class="tag-count">${count}</span>
|
| 449 |
</button>
|
| 450 |
`;
|
| 451 |
})
|
|
|
|
| 86 |
this.handleSearch("");
|
| 87 |
});
|
| 88 |
|
| 89 |
+
// Clear bookmarks with double-click protection
|
| 90 |
+
this.elements.btnClearBookmarks?.addEventListener("click", e => {
|
| 91 |
+
// Use data attribute for confirmation state
|
| 92 |
+
if (e.target.dataset.confirmClear === "true") {
|
| 93 |
this.bookmarks = [];
|
| 94 |
this.saveBookmarks();
|
| 95 |
this.renderBookmarks();
|
| 96 |
+
e.target.textContent = "Clear All";
|
| 97 |
+
e.target.dataset.confirmClear = "false";
|
| 98 |
+
// Refresh main feed to update bookmark icons
|
| 99 |
+
if (this.app) this.app.renderFeeds();
|
| 100 |
+
this.app?.showToast("All bookmarks cleared", "info");
|
| 101 |
+
} else {
|
| 102 |
+
e.target.textContent = "Click again to confirm";
|
| 103 |
+
e.target.dataset.confirmClear = "true";
|
| 104 |
+
// Reset after 3 seconds
|
| 105 |
+
setTimeout(() => {
|
| 106 |
+
e.target.textContent = "Clear All";
|
| 107 |
+
e.target.dataset.confirmClear = "false";
|
| 108 |
+
}, 3000);
|
| 109 |
}
|
| 110 |
});
|
| 111 |
}
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
// Show loading indicator immediately for better UX
|
| 163 |
+
this.elements.searchResults.innerHTML =
|
| 164 |
+
'<span class="search-hint"><span class="search-spinner"></span> Searching...</span>';
|
| 165 |
|
| 166 |
const queryLower = query.toLowerCase();
|
| 167 |
const results = this.allArticles
|
|
|
|
| 194 |
|
| 195 |
highlightMatch(text, query) {
|
| 196 |
const escaped = this.escapeHtml(text);
|
| 197 |
+
const escapedQuery = this.escapeHtml(query);
|
| 198 |
+
const regex = new RegExp(`(${this.escapeRegex(escapedQuery)})`, "gi");
|
| 199 |
return escaped.replace(regex, "<mark>$1</mark>");
|
| 200 |
}
|
| 201 |
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
renderBookmarks() {
|
| 256 |
+
// Update bookmark count badge
|
| 257 |
+
const countBadge = document.getElementById("bookmark-count");
|
| 258 |
+
if (countBadge) {
|
| 259 |
+
countBadge.textContent = this.bookmarks.length > 0 ? `(${this.bookmarks.length})` : "";
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
if (this.bookmarks.length === 0) {
|
| 263 |
this.elements.bookmarksList.innerHTML =
|
| 264 |
'<span class="empty-hint">No bookmarks yet. Click β on articles to save them.</span>';
|
|
|
|
| 465 |
const isHot = count >= maxCount * 0.7;
|
| 466 |
return `
|
| 467 |
<button class="trending-tag ${isHot ? "hot" : ""}"
|
| 468 |
+
onclick="sidebar.filterByTag('${this.escapeHtml(word)}')"
|
| 469 |
+
aria-label="Filter by ${this.escapeHtml(word)}, ${count} articles">
|
| 470 |
${word}
|
| 471 |
+
<span class="tag-count" aria-hidden="true">${count}</span>
|
| 472 |
</button>
|
| 473 |
`;
|
| 474 |
})
|
styles/main.css
CHANGED
|
@@ -79,6 +79,9 @@
|
|
| 79 |
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
| 80 |
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
| 81 |
|
|
|
|
|
|
|
|
|
|
| 82 |
/* Theme indicator */
|
| 83 |
--theme-icon: "βοΈ";
|
| 84 |
}
|
|
@@ -103,6 +106,9 @@
|
|
| 103 |
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
| 104 |
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
| 105 |
|
|
|
|
|
|
|
|
|
|
| 106 |
--theme-icon: "βοΈ";
|
| 107 |
}
|
| 108 |
}
|
|
@@ -435,7 +441,7 @@ a:hover {
|
|
| 435 |
|
| 436 |
/* Subtle zebra striping for better visual distinction */
|
| 437 |
.article-item-wrapper:nth-child(odd) {
|
| 438 |
-
background: rgba(255, 255, 255, 0.015);
|
| 439 |
}
|
| 440 |
|
| 441 |
.article-item-wrapper:nth-child(even) {
|
|
@@ -814,6 +820,35 @@ a:hover {
|
|
| 814 |
margin-right: 8px;
|
| 815 |
}
|
| 816 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
.hint {
|
| 818 |
font-size: 0.8rem;
|
| 819 |
color: var(--text-muted);
|
|
@@ -896,9 +931,17 @@ a:hover {
|
|
| 896 |
|
| 897 |
.status-bar {
|
| 898 |
flex-direction: column;
|
| 899 |
-
gap:
|
| 900 |
text-align: center;
|
| 901 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
}
|
| 903 |
|
| 904 |
/* ============================================
|
|
@@ -954,6 +997,14 @@ a:hover {
|
|
| 954 |
transition: transform var(--transition-fast);
|
| 955 |
}
|
| 956 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
.sidebar-title.collapsed .section-toggle {
|
| 958 |
transform: rotate(-90deg);
|
| 959 |
}
|
|
@@ -1069,6 +1120,19 @@ a:hover {
|
|
| 1069 |
margin-top: 2px;
|
| 1070 |
}
|
| 1071 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1072 |
/* === Bookmarks === */
|
| 1073 |
.bookmarks-list {
|
| 1074 |
display: flex;
|
|
@@ -1379,6 +1443,64 @@ a:hover {
|
|
| 1379 |
transform: scale(1.1);
|
| 1380 |
}
|
| 1381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1382 |
/* === Article Bookmark Button === */
|
| 1383 |
.article-bookmark {
|
| 1384 |
width: 32px;
|
|
|
|
| 79 |
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
| 80 |
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
| 81 |
|
| 82 |
+
/* Zebra striping for light theme */
|
| 83 |
+
--zebra-odd: rgba(0, 0, 0, 0.02);
|
| 84 |
+
|
| 85 |
/* Theme indicator */
|
| 86 |
--theme-icon: "βοΈ";
|
| 87 |
}
|
|
|
|
| 106 |
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
| 107 |
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
| 108 |
|
| 109 |
+
/* Zebra striping for light theme */
|
| 110 |
+
--zebra-odd: rgba(0, 0, 0, 0.02);
|
| 111 |
+
|
| 112 |
--theme-icon: "βοΈ";
|
| 113 |
}
|
| 114 |
}
|
|
|
|
| 441 |
|
| 442 |
/* Subtle zebra striping for better visual distinction */
|
| 443 |
.article-item-wrapper:nth-child(odd) {
|
| 444 |
+
background: var(--zebra-odd, rgba(255, 255, 255, 0.015));
|
| 445 |
}
|
| 446 |
|
| 447 |
.article-item-wrapper:nth-child(even) {
|
|
|
|
| 820 |
margin-right: 8px;
|
| 821 |
}
|
| 822 |
|
| 823 |
+
/* Number input row (for max items) */
|
| 824 |
+
.number-input-row {
|
| 825 |
+
display: flex;
|
| 826 |
+
align-items: center;
|
| 827 |
+
justify-content: space-between;
|
| 828 |
+
gap: 12px;
|
| 829 |
+
padding: 8px 0;
|
| 830 |
+
color: var(--text-secondary);
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
.number-input-row label {
|
| 834 |
+
flex: 1;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
.number-input-row input[type="number"] {
|
| 838 |
+
width: 70px;
|
| 839 |
+
padding: 6px 10px;
|
| 840 |
+
background: var(--bg-tertiary);
|
| 841 |
+
border: 1px solid var(--border-color);
|
| 842 |
+
border-radius: var(--radius-sm);
|
| 843 |
+
color: var(--text-primary);
|
| 844 |
+
text-align: center;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.number-input-row input[type="number"]:focus {
|
| 848 |
+
outline: none;
|
| 849 |
+
border-color: var(--ivy-green);
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
.hint {
|
| 853 |
font-size: 0.8rem;
|
| 854 |
color: var(--text-muted);
|
|
|
|
| 931 |
|
| 932 |
.status-bar {
|
| 933 |
flex-direction: column;
|
| 934 |
+
gap: 8px;
|
| 935 |
text-align: center;
|
| 936 |
}
|
| 937 |
+
|
| 938 |
+
.status-bar .lang-filter {
|
| 939 |
+
order: -1;
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
.status-bar .status-text {
|
| 943 |
+
font-size: 0.8rem;
|
| 944 |
+
}
|
| 945 |
}
|
| 946 |
|
| 947 |
/* ============================================
|
|
|
|
| 997 |
transition: transform var(--transition-fast);
|
| 998 |
}
|
| 999 |
|
| 1000 |
+
/* Bookmark count badge */
|
| 1001 |
+
.bookmark-count {
|
| 1002 |
+
font-size: 0.75rem;
|
| 1003 |
+
color: var(--ivy-green);
|
| 1004 |
+
font-weight: 600;
|
| 1005 |
+
margin-left: 4px;
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
.sidebar-title.collapsed .section-toggle {
|
| 1009 |
transform: rotate(-90deg);
|
| 1010 |
}
|
|
|
|
| 1120 |
margin-top: 2px;
|
| 1121 |
}
|
| 1122 |
|
| 1123 |
+
/* Search spinner */
|
| 1124 |
+
.search-spinner {
|
| 1125 |
+
display: inline-block;
|
| 1126 |
+
width: 12px;
|
| 1127 |
+
height: 12px;
|
| 1128 |
+
border: 2px solid var(--border-color);
|
| 1129 |
+
border-top-color: var(--ivy-green);
|
| 1130 |
+
border-radius: 50%;
|
| 1131 |
+
animation: spin 0.8s linear infinite;
|
| 1132 |
+
margin-right: 6px;
|
| 1133 |
+
vertical-align: middle;
|
| 1134 |
+
}
|
| 1135 |
+
|
| 1136 |
/* === Bookmarks === */
|
| 1137 |
.bookmarks-list {
|
| 1138 |
display: flex;
|
|
|
|
| 1443 |
transform: scale(1.1);
|
| 1444 |
}
|
| 1445 |
|
| 1446 |
+
/* === Back to Top Button === */
|
| 1447 |
+
.back-to-top {
|
| 1448 |
+
position: fixed;
|
| 1449 |
+
bottom: 20px;
|
| 1450 |
+
right: 20px;
|
| 1451 |
+
width: 44px;
|
| 1452 |
+
height: 44px;
|
| 1453 |
+
background: var(--bg-tertiary);
|
| 1454 |
+
border: 1px solid var(--border-color);
|
| 1455 |
+
border-radius: 50%;
|
| 1456 |
+
font-size: 1.2rem;
|
| 1457 |
+
color: var(--text-secondary);
|
| 1458 |
+
cursor: pointer;
|
| 1459 |
+
box-shadow: var(--shadow-md);
|
| 1460 |
+
z-index: 200;
|
| 1461 |
+
opacity: 0;
|
| 1462 |
+
visibility: hidden;
|
| 1463 |
+
transform: translateY(20px);
|
| 1464 |
+
transition: all var(--transition-normal);
|
| 1465 |
+
}
|
| 1466 |
+
|
| 1467 |
+
.back-to-top.visible {
|
| 1468 |
+
opacity: 1;
|
| 1469 |
+
visibility: visible;
|
| 1470 |
+
transform: translateY(0);
|
| 1471 |
+
}
|
| 1472 |
+
|
| 1473 |
+
.back-to-top:hover {
|
| 1474 |
+
background: var(--ivy-green);
|
| 1475 |
+
border-color: var(--ivy-green);
|
| 1476 |
+
color: #fff;
|
| 1477 |
+
transform: scale(1.1);
|
| 1478 |
+
}
|
| 1479 |
+
|
| 1480 |
+
.back-to-top:focus {
|
| 1481 |
+
outline: 2px solid var(--ivy-green);
|
| 1482 |
+
outline-offset: 2px;
|
| 1483 |
+
}
|
| 1484 |
+
|
| 1485 |
+
.back-to-top:focus:not(:focus-visible) {
|
| 1486 |
+
outline: none;
|
| 1487 |
+
}
|
| 1488 |
+
|
| 1489 |
+
/* Position adjustment when keyboard hint is visible */
|
| 1490 |
+
@media (min-width: 901px) {
|
| 1491 |
+
.back-to-top {
|
| 1492 |
+
bottom: 60px; /* Above keyboard hint */
|
| 1493 |
+
}
|
| 1494 |
+
}
|
| 1495 |
+
|
| 1496 |
+
/* Mobile/Tablet: position to not overlap sidebar toggle */
|
| 1497 |
+
@media (max-width: 1100px) {
|
| 1498 |
+
.back-to-top {
|
| 1499 |
+
right: 80px; /* Left of sidebar toggle */
|
| 1500 |
+
bottom: 20px;
|
| 1501 |
+
}
|
| 1502 |
+
}
|
| 1503 |
+
|
| 1504 |
/* === Article Bookmark Button === */
|
| 1505 |
.article-bookmark {
|
| 1506 |
width: 32px;
|