fix(a11y): improve CCRD keyboard and dialog accessibility
Browse filesKeyboard-operable dynamic controls, ARIA state, modal focus management, and live status semantics. Search/data contracts unchanged.
- static/app.js +130 -27
- static/index.html +20 -20
- static/style.css +39 -1
static/app.js
CHANGED
|
@@ -64,6 +64,8 @@ let initComplete = false;
|
|
| 64 |
let snippetObserver = null;
|
| 65 |
let snippetActive = 0;
|
| 66 |
let previewController = null;
|
|
|
|
|
|
|
| 67 |
let keepalivePending = null;
|
| 68 |
let lastKeepaliveAt = Date.now();
|
| 69 |
const snippetQueue = [];
|
|
@@ -321,6 +323,7 @@ function setCachedSearch(key, value) {
|
|
| 321 |
}
|
| 322 |
|
| 323 |
function cacheDom() {
|
|
|
|
| 324 |
DOM.headerTitle = $("#header-title");
|
| 325 |
DOM.headerLogo = $("#header-logo");
|
| 326 |
DOM.searchInput = $("#search-input");
|
|
@@ -336,6 +339,7 @@ function cacheDom() {
|
|
| 336 |
DOM.sidebarTitle = $("#sidebar-title");
|
| 337 |
DOM.sidebarContent = $("#sidebar-content");
|
| 338 |
DOM.resultCount = $("#result-count");
|
|
|
|
| 339 |
DOM.resultsContainer = $("#results-container");
|
| 340 |
DOM.resultsList = $("#results-list");
|
| 341 |
DOM.resultsLoading = $("#results-loading");
|
|
@@ -372,6 +376,7 @@ function cacheDom() {
|
|
| 372 |
DOM.multiZipDownload = $("#multi-zip-download");
|
| 373 |
DOM.multiSelectedCount = $("#multi-selected-count");
|
| 374 |
DOM.randomBookBtn = $("#random-book-btn");
|
|
|
|
| 375 |
DOM.overlay = $("#overlay");
|
| 376 |
DOM.toast = $("#toast");
|
| 377 |
}
|
|
@@ -542,10 +547,10 @@ function renderHistoryDropdown() {
|
|
| 542 |
setHistoryDropdownOpen(false);
|
| 543 |
return;
|
| 544 |
}
|
| 545 |
-
|
| 546 |
-
<div class="history-item"
|
| 547 |
-
<span class="history-text">${escapeHTML(item)}</span>
|
| 548 |
-
<button type="button" class="history-del" data-del="${escapeHTML(item)}" aria-label="删除历史">×</button>
|
| 549 |
</div>`).join("") + '<div class="history-footer"><button type="button" class="history-clear-all">清空历史</button></div>';
|
| 550 |
setHistoryDropdownOpen(true);
|
| 551 |
}
|
|
@@ -591,6 +596,7 @@ function applyMobileMode() {
|
|
| 591 |
if (DOM.mobileToggleIcon) {
|
| 592 |
DOM.mobileToggleIcon.className = STATE.isMobile ? "ui-icon ui-icon-phone" : "ui-icon ui-icon-desktop";
|
| 593 |
}
|
|
|
|
| 594 |
if (DOM.sidebarExpandBtn) {
|
| 595 |
DOM.sidebarExpandBtn.style.display = (!STATE.isMobile && STATE.source) ? "" : "none";
|
| 596 |
if (STATE.isMobile) {
|
|
@@ -942,11 +948,11 @@ async function doSearch() {
|
|
| 942 |
|
| 943 |
function resultPathHtml(item) {
|
| 944 |
const sourceSeparator = (item.Folder || []).length ? '<span class="path-sep">/</span>' : "";
|
| 945 |
-
const sourcePrefix = `<span class="path-folder" data-source="${escapeHTML(item.Source)}" data-folder="">${highlightText(item.SourceName, STATE.query)}${sourceSeparator}</span>`;
|
| 946 |
const rest = (item.Folder || []).map((part, index) => {
|
| 947 |
const path = item.Folder.slice(0, index + 1).join("/");
|
| 948 |
const separator = index < item.Folder.length - 1 ? '<span class="path-sep">/</span>' : "";
|
| 949 |
-
return `<span class="path-folder" data-source="${escapeHTML(item.Source)}" data-folder="${escapeHTML(path)}">${highlightText(part, STATE.query)}${separator}</span>`;
|
| 950 |
}).join("");
|
| 951 |
return sourcePrefix + rest;
|
| 952 |
}
|
|
@@ -960,8 +966,8 @@ function resultItemHtml(item) {
|
|
| 960 |
: STATE.fulltext ? highlightText(item.snippet, STATE.query, snippetHighlights) : escapeHTML(item.snippet || "");
|
| 961 |
const snippetHtml = `<div class="result-snippet" data-snippet-doc-id="${escapeHTML(item.doc_id)}" data-snippet-pending="${pendingSnippet ? "1" : "0"}">${snippet}</div>`;
|
| 962 |
return `
|
| 963 |
-
<div class="result-item" data-doc-id="${escapeHTML(item.doc_id)}">
|
| 964 |
-
<input type="checkbox" class="result-checkbox" data-doc-id="${escapeHTML(item.doc_id)}" ${STATE.selectedIds.has(item.doc_id) ? "checked" : ""}>
|
| 965 |
<div class="result-file-icon">${ICON_HTML.file}</div>
|
| 966 |
<div class="result-info">
|
| 967 |
<div class="result-title">${titleHtml}</div>
|
|
@@ -969,8 +975,8 @@ function resultItemHtml(item) {
|
|
| 969 |
<div class="result-meta"><span class="result-size">${escapeHTML(formatSize(item.Size))}</span></div>
|
| 970 |
</div>
|
| 971 |
<div class="result-actions">
|
| 972 |
-
<button class="result-action-btn" data-action="preview" data-doc-id="${escapeHTML(item.doc_id)}">在线预览</button>
|
| 973 |
-
<a class="result-action-btn primary" href="/api/download/${encodeURIComponent(item.doc_id)}">下载</a>
|
| 974 |
</div>
|
| 975 |
${snippetHtml}
|
| 976 |
</div>`;
|
|
@@ -1096,7 +1102,52 @@ async function loadExactSnippet(task) {
|
|
| 1096 |
task.node.innerHTML = highlightText(data.snippet || "", task.query, data.highlights || []);
|
| 1097 |
}
|
| 1098 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1099 |
async function openPreview(docId, keyword, options = {}) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1100 |
if (options.sync !== false) {
|
| 1101 |
STATE.previewDocId = docId;
|
| 1102 |
syncUrl(history.state && history.state.preview === true);
|
|
@@ -1132,12 +1183,18 @@ async function openPreview(docId, keyword, options = {}) {
|
|
| 1132 |
<button type="button" class="icon-btn-sm preview-theme-btn" data-action="toggle-preview-theme" aria-label="${STATE.isDark ? "切换到白天模式" : "切换到黑夜模式"}" title="${STATE.isDark ? "白天模式" : "黑夜模式"}">${themeIcon(STATE.isDark)}</button>
|
| 1133 |
</div>
|
| 1134 |
</div>
|
| 1135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1136 |
const previewBody = DOM.previewPanel.querySelector(".preview-body");
|
| 1137 |
if (shouldHighlight) previewBody.innerHTML = text;
|
| 1138 |
else previewBody.textContent = rawText;
|
| 1139 |
DOM.previewPanel.style.display = STATE.isMobile ? "flex" : "block";
|
| 1140 |
document.body.classList.add("preview-open");
|
|
|
|
|
|
|
| 1141 |
if (motionAllowed()) {
|
| 1142 |
DOM.previewPanel.classList.remove("preview-enter");
|
| 1143 |
void DOM.previewPanel.offsetWidth;
|
|
@@ -1175,18 +1232,14 @@ function closePreview() {
|
|
| 1175 |
return;
|
| 1176 |
}
|
| 1177 |
STATE.previewDocId = null;
|
| 1178 |
-
|
| 1179 |
-
DOM.previewPanel.style.display = "none";
|
| 1180 |
-
document.body.classList.remove("preview-open");
|
| 1181 |
observeResultSnippets();
|
| 1182 |
syncUrl();
|
| 1183 |
}
|
| 1184 |
|
| 1185 |
function restorePreviewFromUrl(options = {}) {
|
| 1186 |
if (!STATE.previewDocId) {
|
| 1187 |
-
|
| 1188 |
-
DOM.previewPanel.style.display = "none";
|
| 1189 |
-
document.body.classList.remove("preview-open");
|
| 1190 |
return;
|
| 1191 |
}
|
| 1192 |
openPreview(STATE.previewDocId, STATE.query, { sync: false, scroll: options.scroll === true });
|
|
@@ -1386,6 +1439,7 @@ function refreshFolderTreeSelectionState() {
|
|
| 1386 |
const collapsed = !!STATE.folderCollapsed[path];
|
| 1387 |
toggle.classList.toggle("expanded", !collapsed);
|
| 1388 |
toggle.setAttribute("aria-label", collapsed ? "展开子文件夹" : "收起子文件夹");
|
|
|
|
| 1389 |
toggle.setAttribute("title", collapsed ? "展开" : "收起");
|
| 1390 |
});
|
| 1391 |
}
|
|
@@ -1491,7 +1545,7 @@ function renderFolderNodes(container, nodes, depth) {
|
|
| 1491 |
row.className = "filter-folder-item";
|
| 1492 |
row.dataset.folderPath = node.path;
|
| 1493 |
row.style.setProperty("--fdepth", itemDepth);
|
| 1494 |
-
row.innerHTML = `${hasChildren ? `<button type="button" class="tree-toggle" data-folder-path="${escapeHTML(node.path)}" aria-label="展开子文件夹" title="展开"><span class="tree-toggle-glyph" aria-hidden="true"></span></button>` : '<span class="tree-toggle-placeholder"></span>'}<input type="checkbox" data-folder-path="${escapeHTML(node.path)}" value="${escapeHTML(node.path)}"><span class="folder-name">${escapeHTML(node.name)}</span><span class="folder-count">${(node.count || 0).toLocaleString()}</span>`;
|
| 1495 |
const toggle = row.querySelector(".tree-toggle");
|
| 1496 |
if (toggle) {
|
| 1497 |
toggle.addEventListener("click", (event) => {
|
|
@@ -1570,17 +1624,17 @@ function retrySidebar(source, path, routeId, requestSequence) {
|
|
| 1570 |
|
| 1571 |
function renderSourceListSidebar(sources) {
|
| 1572 |
DOM.sidebarTitle.textContent = "数据包";
|
| 1573 |
-
DOM.sidebarContent.innerHTML = (sources || []).map((source) => `<
|
| 1574 |
}
|
| 1575 |
|
| 1576 |
function browserContentHTML(data, path, includeBreadcrumb) {
|
| 1577 |
const parts = path ? path.split("/") : [];
|
| 1578 |
-
let html = '<
|
| 1579 |
if (includeBreadcrumb) {
|
| 1580 |
-
html += `<div class="sidebar-breadcrumb"><
|
| 1581 |
}
|
| 1582 |
-
html += (data.folders || []).map((item) => `<
|
| 1583 |
-
html += (data.files || []).map((item) => `<
|
| 1584 |
return html;
|
| 1585 |
}
|
| 1586 |
|
|
@@ -1649,12 +1703,26 @@ async function openFolder(path, routeId = routeRenderId) {
|
|
| 1649 |
}
|
| 1650 |
|
| 1651 |
function updateSidebarVisibility() {
|
|
|
|
| 1652 |
DOM.leftSidebar.classList.toggle("collapsed", !STATE.leftSidebarOpen);
|
| 1653 |
DOM.rightSidebar.classList.toggle("collapsed", !STATE.rightSidebarOpen);
|
| 1654 |
DOM.leftSidebar.classList.toggle("open", STATE.leftSidebarOpen);
|
| 1655 |
DOM.rightSidebar.classList.toggle("open", STATE.rightSidebarOpen);
|
| 1656 |
DOM.overlay.style.display = "";
|
| 1657 |
DOM.overlay.classList.toggle("open", STATE.isMobile && (STATE.leftSidebarOpen || STATE.rightSidebarOpen));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1658 |
}
|
| 1659 |
|
| 1660 |
function recoverSidebarState() {
|
|
@@ -1746,11 +1814,13 @@ function attachEvents() {
|
|
| 1746 |
if (STATE.isMobile && STATE.leftSidebarOpen) STATE.rightSidebarOpen = false;
|
| 1747 |
updateSidebarVisibility();
|
| 1748 |
syncUrl();
|
|
|
|
| 1749 |
});
|
| 1750 |
if (DOM.sidebarExpandBtn) {
|
| 1751 |
DOM.sidebarExpandBtn.addEventListener("click", () => {
|
| 1752 |
DOM.leftSidebar.classList.toggle("expanded-wide");
|
| 1753 |
DOM.sidebarExpandBtn.textContent = DOM.leftSidebar.classList.contains("expanded-wide") ? "→" : "↔";
|
|
|
|
| 1754 |
syncUrl();
|
| 1755 |
});
|
| 1756 |
}
|
|
@@ -1759,11 +1829,13 @@ function attachEvents() {
|
|
| 1759 |
if (STATE.isMobile && STATE.rightSidebarOpen) STATE.leftSidebarOpen = false;
|
| 1760 |
updateSidebarVisibility();
|
| 1761 |
syncUrl();
|
|
|
|
| 1762 |
});
|
| 1763 |
DOM.closeFiltersBtn.addEventListener("click", () => {
|
| 1764 |
STATE.rightSidebarOpen = false;
|
| 1765 |
updateSidebarVisibility();
|
| 1766 |
syncUrl();
|
|
|
|
| 1767 |
});
|
| 1768 |
DOM.overlay.addEventListener("click", () => {
|
| 1769 |
STATE.leftSidebarOpen = false;
|
|
@@ -1778,6 +1850,7 @@ function attachEvents() {
|
|
| 1778 |
localStorage.setItem("mobileMode", STATE.isMobile ? "mobile" : "desktop");
|
| 1779 |
applyUiUrlState();
|
| 1780 |
updateSidebarVisibility();
|
|
|
|
| 1781 |
syncUrl();
|
| 1782 |
});
|
| 1783 |
let historyDropdownActive = false;
|
|
@@ -2027,6 +2100,12 @@ function attachEvents() {
|
|
| 2027 |
doSearch();
|
| 2028 |
}
|
| 2029 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2030 |
DOM.sidebarContent.addEventListener("click", async (event) => {
|
| 2031 |
const sourceItem = event.target.closest("[data-source]");
|
| 2032 |
if (sourceItem) {
|
|
@@ -2083,12 +2162,38 @@ function attachEvents() {
|
|
| 2083 |
if (event.target.closest("[data-action='toggle-preview-theme']")) toggleTheme();
|
| 2084 |
});
|
| 2085 |
document.addEventListener("keydown", (event) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2086 |
if (event.key === "Escape") {
|
| 2087 |
setHistoryDropdownOpen(false);
|
| 2088 |
-
if (STATE.previewDocId) closePreview();
|
| 2089 |
STATE.leftSidebarOpen = false;
|
| 2090 |
STATE.rightSidebarOpen = false;
|
| 2091 |
updateSidebarVisibility();
|
|
|
|
| 2092 |
return;
|
| 2093 |
}
|
| 2094 |
if (event.key === "/" && document.activeElement !== DOM.searchInput) {
|
|
@@ -2110,9 +2215,7 @@ function attachEvents() {
|
|
| 2110 |
if (nextPreviewDocId) {
|
| 2111 |
restorePreviewFromUrl({ scroll: false });
|
| 2112 |
} else {
|
| 2113 |
-
|
| 2114 |
-
DOM.previewPanel.style.display = "none";
|
| 2115 |
-
document.body.classList.remove("preview-open");
|
| 2116 |
}
|
| 2117 |
return;
|
| 2118 |
}
|
|
|
|
| 64 |
let snippetObserver = null;
|
| 65 |
let snippetActive = 0;
|
| 66 |
let previewController = null;
|
| 67 |
+
let previewReturnFocus = null;
|
| 68 |
+
let previewReturnFocusSelector = "";
|
| 69 |
let keepalivePending = null;
|
| 70 |
let lastKeepaliveAt = Date.now();
|
| 71 |
const snippetQueue = [];
|
|
|
|
| 323 |
}
|
| 324 |
|
| 325 |
function cacheDom() {
|
| 326 |
+
DOM.header = $("#header");
|
| 327 |
DOM.headerTitle = $("#header-title");
|
| 328 |
DOM.headerLogo = $("#header-logo");
|
| 329 |
DOM.searchInput = $("#search-input");
|
|
|
|
| 339 |
DOM.sidebarTitle = $("#sidebar-title");
|
| 340 |
DOM.sidebarContent = $("#sidebar-content");
|
| 341 |
DOM.resultCount = $("#result-count");
|
| 342 |
+
DOM.statusBar = $("#status-bar");
|
| 343 |
DOM.resultsContainer = $("#results-container");
|
| 344 |
DOM.resultsList = $("#results-list");
|
| 345 |
DOM.resultsLoading = $("#results-loading");
|
|
|
|
| 376 |
DOM.multiZipDownload = $("#multi-zip-download");
|
| 377 |
DOM.multiSelectedCount = $("#multi-selected-count");
|
| 378 |
DOM.randomBookBtn = $("#random-book-btn");
|
| 379 |
+
DOM.footer = $("#footer");
|
| 380 |
DOM.overlay = $("#overlay");
|
| 381 |
DOM.toast = $("#toast");
|
| 382 |
}
|
|
|
|
| 547 |
setHistoryDropdownOpen(false);
|
| 548 |
return;
|
| 549 |
}
|
| 550 |
+
DOM.searchHistoryDropdown.innerHTML = items.map((item) => `
|
| 551 |
+
<div class="history-item">
|
| 552 |
+
<button type="button" class="history-select" data-query="${escapeHTML(item)}" aria-label="搜索历史:${escapeHTML(item)}"><span class="history-text">${escapeHTML(item)}</span></button>
|
| 553 |
+
<button type="button" class="history-del" data-del="${escapeHTML(item)}" aria-label="删除历史:${escapeHTML(item)}">×</button>
|
| 554 |
</div>`).join("") + '<div class="history-footer"><button type="button" class="history-clear-all">清空历史</button></div>';
|
| 555 |
setHistoryDropdownOpen(true);
|
| 556 |
}
|
|
|
|
| 596 |
if (DOM.mobileToggleIcon) {
|
| 597 |
DOM.mobileToggleIcon.className = STATE.isMobile ? "ui-icon ui-icon-phone" : "ui-icon ui-icon-desktop";
|
| 598 |
}
|
| 599 |
+
DOM.mobileToggleBtn.setAttribute("aria-pressed", String(STATE.isMobile));
|
| 600 |
if (DOM.sidebarExpandBtn) {
|
| 601 |
DOM.sidebarExpandBtn.style.display = (!STATE.isMobile && STATE.source) ? "" : "none";
|
| 602 |
if (STATE.isMobile) {
|
|
|
|
| 948 |
|
| 949 |
function resultPathHtml(item) {
|
| 950 |
const sourceSeparator = (item.Folder || []).length ? '<span class="path-sep">/</span>' : "";
|
| 951 |
+
const sourcePrefix = `<span class="path-folder" role="button" tabindex="0" data-source="${escapeHTML(item.Source)}" data-folder="" aria-label="按数据包筛选:${escapeHTML(item.SourceName)}">${highlightText(item.SourceName, STATE.query)}${sourceSeparator}</span>`;
|
| 952 |
const rest = (item.Folder || []).map((part, index) => {
|
| 953 |
const path = item.Folder.slice(0, index + 1).join("/");
|
| 954 |
const separator = index < item.Folder.length - 1 ? '<span class="path-sep">/</span>' : "";
|
| 955 |
+
return `<span class="path-folder" role="button" tabindex="0" data-source="${escapeHTML(item.Source)}" data-folder="${escapeHTML(path)}" aria-label="按文件夹筛选:${escapeHTML(path)}">${highlightText(part, STATE.query)}${separator}</span>`;
|
| 956 |
}).join("");
|
| 957 |
return sourcePrefix + rest;
|
| 958 |
}
|
|
|
|
| 966 |
: STATE.fulltext ? highlightText(item.snippet, STATE.query, snippetHighlights) : escapeHTML(item.snippet || "");
|
| 967 |
const snippetHtml = `<div class="result-snippet" data-snippet-doc-id="${escapeHTML(item.doc_id)}" data-snippet-pending="${pendingSnippet ? "1" : "0"}">${snippet}</div>`;
|
| 968 |
return `
|
| 969 |
+
<div class="result-item" role="listitem" data-doc-id="${escapeHTML(item.doc_id)}">
|
| 970 |
+
<input type="checkbox" class="result-checkbox" aria-label="选择文件:${escapeHTML(item.File)}" data-doc-id="${escapeHTML(item.doc_id)}" ${STATE.selectedIds.has(item.doc_id) ? "checked" : ""}>
|
| 971 |
<div class="result-file-icon">${ICON_HTML.file}</div>
|
| 972 |
<div class="result-info">
|
| 973 |
<div class="result-title">${titleHtml}</div>
|
|
|
|
| 975 |
<div class="result-meta"><span class="result-size">${escapeHTML(formatSize(item.Size))}</span></div>
|
| 976 |
</div>
|
| 977 |
<div class="result-actions">
|
| 978 |
+
<button class="result-action-btn" data-action="preview" data-doc-id="${escapeHTML(item.doc_id)}" aria-label="预览:${escapeHTML(item.File)}">在线预览</button>
|
| 979 |
+
<a class="result-action-btn primary" href="/api/download/${encodeURIComponent(item.doc_id)}" aria-label="下载:${escapeHTML(item.File)}">下载</a>
|
| 980 |
</div>
|
| 981 |
${snippetHtml}
|
| 982 |
</div>`;
|
|
|
|
| 1102 |
task.node.innerHTML = highlightText(data.snippet || "", task.query, data.highlights || []);
|
| 1103 |
}
|
| 1104 |
|
| 1105 |
+
function previewFocusableElements() {
|
| 1106 |
+
if (!DOM.previewPanel || DOM.previewPanel.hidden) return [];
|
| 1107 |
+
return Array.from(DOM.previewPanel.querySelectorAll("button, a[href], input, select, textarea, [tabindex]:not([tabindex='-1'])"))
|
| 1108 |
+
.filter((node) => !node.disabled && !node.hidden && node.getClientRects().length);
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
function setPreviewModalState(open) {
|
| 1112 |
+
const background = [DOM.header, DOM.leftSidebar, DOM.rightSidebar, DOM.statusBar, DOM.resultsContainer, DOM.loadInfo, DOM.footer];
|
| 1113 |
+
for (const node of background) {
|
| 1114 |
+
if (!node) continue;
|
| 1115 |
+
node.inert = open;
|
| 1116 |
+
if (open) node.setAttribute("aria-hidden", "true");
|
| 1117 |
+
else node.removeAttribute("aria-hidden");
|
| 1118 |
+
}
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
function restorePreviewFocus() {
|
| 1122 |
+
const node = previewReturnFocus;
|
| 1123 |
+
const selector = previewReturnFocusSelector;
|
| 1124 |
+
previewReturnFocus = null;
|
| 1125 |
+
previewReturnFocusSelector = "";
|
| 1126 |
+
requestAnimationFrame(() => {
|
| 1127 |
+
const target = node && node.isConnected ? node : selector ? document.querySelector(selector) : null;
|
| 1128 |
+
if (!target || target.inert) return;
|
| 1129 |
+
target.focus({ preventScroll: true });
|
| 1130 |
+
});
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
function clearPreviewDom() {
|
| 1134 |
+
DOM.previewPanel.innerHTML = "";
|
| 1135 |
+
DOM.previewPanel.hidden = true;
|
| 1136 |
+
DOM.previewPanel.style.display = "none";
|
| 1137 |
+
DOM.previewPanel.removeAttribute("aria-labelledby");
|
| 1138 |
+
document.body.classList.remove("preview-open");
|
| 1139 |
+
setPreviewModalState(false);
|
| 1140 |
+
updateSidebarVisibility();
|
| 1141 |
+
restorePreviewFocus();
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
async function openPreview(docId, keyword, options = {}) {
|
| 1145 |
+
if (!STATE.previewDocId && !DOM.previewPanel.contains(document.activeElement)) {
|
| 1146 |
+
previewReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
| 1147 |
+
if (previewReturnFocus && previewReturnFocus.matches("[data-action='preview'][data-doc-id]")) {
|
| 1148 |
+
previewReturnFocusSelector = `[data-action='preview'][data-doc-id="${CSS.escape(previewReturnFocus.dataset.docId)}"]`;
|
| 1149 |
+
}
|
| 1150 |
+
}
|
| 1151 |
if (options.sync !== false) {
|
| 1152 |
STATE.previewDocId = docId;
|
| 1153 |
syncUrl(history.state && history.state.preview === true);
|
|
|
|
| 1183 |
<button type="button" class="icon-btn-sm preview-theme-btn" data-action="toggle-preview-theme" aria-label="${STATE.isDark ? "切换到白天模式" : "切换到黑夜模式"}" title="${STATE.isDark ? "白天模式" : "黑夜模式"}">${themeIcon(STATE.isDark)}</button>
|
| 1184 |
</div>
|
| 1185 |
</div>
|
| 1186 |
+
<pre class="preview-body"></pre>`;
|
| 1187 |
+
const previewTitle = DOM.previewPanel.querySelector(".preview-title");
|
| 1188 |
+
if (previewTitle) previewTitle.id = "preview-title";
|
| 1189 |
+
DOM.previewPanel.setAttribute("aria-labelledby", "preview-title");
|
| 1190 |
+
DOM.previewPanel.hidden = false;
|
| 1191 |
const previewBody = DOM.previewPanel.querySelector(".preview-body");
|
| 1192 |
if (shouldHighlight) previewBody.innerHTML = text;
|
| 1193 |
else previewBody.textContent = rawText;
|
| 1194 |
DOM.previewPanel.style.display = STATE.isMobile ? "flex" : "block";
|
| 1195 |
document.body.classList.add("preview-open");
|
| 1196 |
+
setPreviewModalState(true);
|
| 1197 |
+
requestAnimationFrame(() => DOM.previewPanel.querySelector("[data-action='close-preview']")?.focus({ preventScroll: true }));
|
| 1198 |
if (motionAllowed()) {
|
| 1199 |
DOM.previewPanel.classList.remove("preview-enter");
|
| 1200 |
void DOM.previewPanel.offsetWidth;
|
|
|
|
| 1232 |
return;
|
| 1233 |
}
|
| 1234 |
STATE.previewDocId = null;
|
| 1235 |
+
clearPreviewDom();
|
|
|
|
|
|
|
| 1236 |
observeResultSnippets();
|
| 1237 |
syncUrl();
|
| 1238 |
}
|
| 1239 |
|
| 1240 |
function restorePreviewFromUrl(options = {}) {
|
| 1241 |
if (!STATE.previewDocId) {
|
| 1242 |
+
clearPreviewDom();
|
|
|
|
|
|
|
| 1243 |
return;
|
| 1244 |
}
|
| 1245 |
openPreview(STATE.previewDocId, STATE.query, { sync: false, scroll: options.scroll === true });
|
|
|
|
| 1439 |
const collapsed = !!STATE.folderCollapsed[path];
|
| 1440 |
toggle.classList.toggle("expanded", !collapsed);
|
| 1441 |
toggle.setAttribute("aria-label", collapsed ? "展开子文件夹" : "收起子文件夹");
|
| 1442 |
+
toggle.setAttribute("aria-expanded", String(!collapsed));
|
| 1443 |
toggle.setAttribute("title", collapsed ? "展开" : "收起");
|
| 1444 |
});
|
| 1445 |
}
|
|
|
|
| 1545 |
row.className = "filter-folder-item";
|
| 1546 |
row.dataset.folderPath = node.path;
|
| 1547 |
row.style.setProperty("--fdepth", itemDepth);
|
| 1548 |
+
row.innerHTML = `${hasChildren ? `<button type="button" class="tree-toggle" data-folder-path="${escapeHTML(node.path)}" aria-label="展开子文件夹:${escapeHTML(node.name)}" aria-expanded="false" title="展开"><span class="tree-toggle-glyph" aria-hidden="true"></span></button>` : '<span class="tree-toggle-placeholder"></span>'}<input type="checkbox" data-folder-path="${escapeHTML(node.path)}" value="${escapeHTML(node.path)}" aria-label="选择文件夹:${escapeHTML(node.name)}"><span class="folder-name">${escapeHTML(node.name)}</span><span class="folder-count">${(node.count || 0).toLocaleString()}</span>`;
|
| 1549 |
const toggle = row.querySelector(".tree-toggle");
|
| 1550 |
if (toggle) {
|
| 1551 |
toggle.addEventListener("click", (event) => {
|
|
|
|
| 1624 |
|
| 1625 |
function renderSourceListSidebar(sources) {
|
| 1626 |
DOM.sidebarTitle.textContent = "数据包";
|
| 1627 |
+
DOM.sidebarContent.innerHTML = (sources || []).map((source) => `<button type="button" class="repo-list-item" data-source="${escapeHTML(source.slug)}" aria-label="打开数据包:${escapeHTML(source.name)}">${ICON_HTML.source}<span class="repo-name">${escapeHTML(source.name)}</span><span class="repo-count">${(source.count || 0).toLocaleString()}</span></button>`).join("");
|
| 1628 |
}
|
| 1629 |
|
| 1630 |
function browserContentHTML(data, path, includeBreadcrumb) {
|
| 1631 |
const parts = path ? path.split("/") : [];
|
| 1632 |
+
let html = '<button type="button" class="back-to-global" id="back-home">返回全局搜索</button>';
|
| 1633 |
if (includeBreadcrumb) {
|
| 1634 |
+
html += `<div class="sidebar-breadcrumb" aria-label="文件夹路径"><button type="button" class="crumb-item" data-folder-nav="">根目录</button>${parts.map((part, index) => `<span class="crumb-sep" aria-hidden="true">/</span><button type="button" class="crumb-item" data-folder-nav="${escapeHTML(parts.slice(0, index + 1).join("/"))}">${escapeHTML(part)}</button>`).join("")}</div>`;
|
| 1635 |
}
|
| 1636 |
+
html += (data.folders || []).map((item) => `<button type="button" class="browser-item" data-folder-open="${escapeHTML(item.path)}" aria-label="打开文件夹:${escapeHTML(item.name)}">${ICON_HTML.folder}<span class="browser-name">${escapeHTML(item.name)}</span><span class="browser-count">${(item.count || 0).toLocaleString()}</span></button>`).join("");
|
| 1637 |
+
html += (data.files || []).map((item) => `<button type="button" class="browser-item" data-doc-id="${escapeHTML(item.doc_id)}" aria-label="预览文件:${escapeHTML(item.name)}.txt">${ICON_HTML.file}<span class="browser-name">${escapeHTML(item.name)}.txt</span><span class="browser-size">${escapeHTML(formatSize(item.size))}</span></button>`).join("");
|
| 1638 |
return html;
|
| 1639 |
}
|
| 1640 |
|
|
|
|
| 1703 |
}
|
| 1704 |
|
| 1705 |
function updateSidebarVisibility() {
|
| 1706 |
+
const modalOpen = Boolean(STATE.previewDocId && DOM.previewPanel && !DOM.previewPanel.hidden);
|
| 1707 |
DOM.leftSidebar.classList.toggle("collapsed", !STATE.leftSidebarOpen);
|
| 1708 |
DOM.rightSidebar.classList.toggle("collapsed", !STATE.rightSidebarOpen);
|
| 1709 |
DOM.leftSidebar.classList.toggle("open", STATE.leftSidebarOpen);
|
| 1710 |
DOM.rightSidebar.classList.toggle("open", STATE.rightSidebarOpen);
|
| 1711 |
DOM.overlay.style.display = "";
|
| 1712 |
DOM.overlay.classList.toggle("open", STATE.isMobile && (STATE.leftSidebarOpen || STATE.rightSidebarOpen));
|
| 1713 |
+
DOM.hamburgerBtn.setAttribute("aria-expanded", String(STATE.leftSidebarOpen));
|
| 1714 |
+
DOM.hamburgerBtn.setAttribute("aria-label", STATE.leftSidebarOpen ? "关闭数据包导航" : "打开数据包导航");
|
| 1715 |
+
DOM.settingsBtn.setAttribute("aria-expanded", String(STATE.rightSidebarOpen));
|
| 1716 |
+
DOM.settingsBtn.setAttribute("aria-label", STATE.rightSidebarOpen ? "关闭搜索过滤" : "打开搜索过滤");
|
| 1717 |
+
DOM.leftSidebar.setAttribute("aria-hidden", String(!STATE.leftSidebarOpen || modalOpen));
|
| 1718 |
+
DOM.rightSidebar.setAttribute("aria-hidden", String(!STATE.rightSidebarOpen || modalOpen));
|
| 1719 |
+
DOM.leftSidebar.inert = !STATE.leftSidebarOpen || modalOpen;
|
| 1720 |
+
DOM.rightSidebar.inert = !STATE.rightSidebarOpen || modalOpen;
|
| 1721 |
+
if (DOM.sidebarExpandBtn) {
|
| 1722 |
+
const expanded = DOM.leftSidebar.classList.contains("expanded-wide");
|
| 1723 |
+
DOM.sidebarExpandBtn.setAttribute("aria-expanded", String(expanded));
|
| 1724 |
+
DOM.sidebarExpandBtn.setAttribute("aria-label", expanded ? "收起数据包导航" : "展开数据包导航");
|
| 1725 |
+
}
|
| 1726 |
}
|
| 1727 |
|
| 1728 |
function recoverSidebarState() {
|
|
|
|
| 1814 |
if (STATE.isMobile && STATE.leftSidebarOpen) STATE.rightSidebarOpen = false;
|
| 1815 |
updateSidebarVisibility();
|
| 1816 |
syncUrl();
|
| 1817 |
+
if (!STATE.leftSidebarOpen) DOM.hamburgerBtn.focus({ preventScroll: true });
|
| 1818 |
});
|
| 1819 |
if (DOM.sidebarExpandBtn) {
|
| 1820 |
DOM.sidebarExpandBtn.addEventListener("click", () => {
|
| 1821 |
DOM.leftSidebar.classList.toggle("expanded-wide");
|
| 1822 |
DOM.sidebarExpandBtn.textContent = DOM.leftSidebar.classList.contains("expanded-wide") ? "→" : "↔";
|
| 1823 |
+
DOM.sidebarExpandBtn.setAttribute("aria-expanded", String(DOM.leftSidebar.classList.contains("expanded-wide")));
|
| 1824 |
syncUrl();
|
| 1825 |
});
|
| 1826 |
}
|
|
|
|
| 1829 |
if (STATE.isMobile && STATE.rightSidebarOpen) STATE.leftSidebarOpen = false;
|
| 1830 |
updateSidebarVisibility();
|
| 1831 |
syncUrl();
|
| 1832 |
+
if (!STATE.rightSidebarOpen) DOM.settingsBtn.focus({ preventScroll: true });
|
| 1833 |
});
|
| 1834 |
DOM.closeFiltersBtn.addEventListener("click", () => {
|
| 1835 |
STATE.rightSidebarOpen = false;
|
| 1836 |
updateSidebarVisibility();
|
| 1837 |
syncUrl();
|
| 1838 |
+
DOM.settingsBtn.focus({ preventScroll: true });
|
| 1839 |
});
|
| 1840 |
DOM.overlay.addEventListener("click", () => {
|
| 1841 |
STATE.leftSidebarOpen = false;
|
|
|
|
| 1850 |
localStorage.setItem("mobileMode", STATE.isMobile ? "mobile" : "desktop");
|
| 1851 |
applyUiUrlState();
|
| 1852 |
updateSidebarVisibility();
|
| 1853 |
+
DOM.mobileToggleBtn.setAttribute("aria-pressed", String(STATE.isMobile));
|
| 1854 |
syncUrl();
|
| 1855 |
});
|
| 1856 |
let historyDropdownActive = false;
|
|
|
|
| 2100 |
doSearch();
|
| 2101 |
}
|
| 2102 |
});
|
| 2103 |
+
DOM.resultsList.addEventListener("keydown", (event) => {
|
| 2104 |
+
const folder = event.target.closest(".path-folder[role='button']");
|
| 2105 |
+
if (!folder || (event.key !== "Enter" && event.key !== " ")) return;
|
| 2106 |
+
event.preventDefault();
|
| 2107 |
+
folder.click();
|
| 2108 |
+
});
|
| 2109 |
DOM.sidebarContent.addEventListener("click", async (event) => {
|
| 2110 |
const sourceItem = event.target.closest("[data-source]");
|
| 2111 |
if (sourceItem) {
|
|
|
|
| 2162 |
if (event.target.closest("[data-action='toggle-preview-theme']")) toggleTheme();
|
| 2163 |
});
|
| 2164 |
document.addEventListener("keydown", (event) => {
|
| 2165 |
+
if (STATE.previewDocId && !DOM.previewPanel.hidden && DOM.previewPanel.style.display !== "none") {
|
| 2166 |
+
if (event.key === "Escape") {
|
| 2167 |
+
event.preventDefault();
|
| 2168 |
+
closePreview();
|
| 2169 |
+
return;
|
| 2170 |
+
}
|
| 2171 |
+
if (event.key === "Tab") {
|
| 2172 |
+
const focusable = previewFocusableElements();
|
| 2173 |
+
if (!focusable.length) {
|
| 2174 |
+
event.preventDefault();
|
| 2175 |
+
DOM.previewPanel.focus({ preventScroll: true });
|
| 2176 |
+
return;
|
| 2177 |
+
}
|
| 2178 |
+
const first = focusable[0];
|
| 2179 |
+
const last = focusable[focusable.length - 1];
|
| 2180 |
+
if (event.shiftKey && document.activeElement === first) {
|
| 2181 |
+
event.preventDefault();
|
| 2182 |
+
last.focus();
|
| 2183 |
+
} else if (!event.shiftKey && document.activeElement === last) {
|
| 2184 |
+
event.preventDefault();
|
| 2185 |
+
first.focus();
|
| 2186 |
+
}
|
| 2187 |
+
return;
|
| 2188 |
+
}
|
| 2189 |
+
return;
|
| 2190 |
+
}
|
| 2191 |
if (event.key === "Escape") {
|
| 2192 |
setHistoryDropdownOpen(false);
|
|
|
|
| 2193 |
STATE.leftSidebarOpen = false;
|
| 2194 |
STATE.rightSidebarOpen = false;
|
| 2195 |
updateSidebarVisibility();
|
| 2196 |
+
DOM.hamburgerBtn.focus({ preventScroll: true });
|
| 2197 |
return;
|
| 2198 |
}
|
| 2199 |
if (event.key === "/" && document.activeElement !== DOM.searchInput) {
|
|
|
|
| 2215 |
if (nextPreviewDocId) {
|
| 2216 |
restorePreviewFromUrl({ scroll: false });
|
| 2217 |
} else {
|
| 2218 |
+
clearPreviewDom();
|
|
|
|
|
|
|
| 2219 |
}
|
| 2220 |
return;
|
| 2221 |
}
|
static/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
| 16 |
|
| 17 |
<header id="header">
|
| 18 |
<div class="header-left">
|
| 19 |
-
<button id="hamburger-btn" class="icon-btn" aria-label="
|
| 20 |
<a id="header-logo" class="header-logo" href="https://huggingface.co/VOMEBOOK" title="VOMEBOOK">
|
| 21 |
<span id="header-title">VOMEBOOK</span>
|
| 22 |
</a>
|
|
@@ -24,22 +24,22 @@
|
|
| 24 |
<div class="header-center">
|
| 25 |
<div class="search-box">
|
| 26 |
<span class="search-icon"><span class="ui-icon ui-icon-search" aria-hidden="true"></span></span>
|
| 27 |
-
<input type="search" id="search-input" class="search-input" placeholder="全文搜索 TXT 正文..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
| 28 |
<kbd class="search-kbd">/</kbd>
|
| 29 |
-
<div id="search-history-dropdown" class="search-history-dropdown" style="display:none"></div>
|
| 30 |
</div>
|
| 31 |
</div>
|
| 32 |
<div class="header-right">
|
| 33 |
-
<button id="settings-btn" class="icon-btn" aria-label="搜索
|
| 34 |
-
<button id="mobile-toggle-btn" class="icon-btn" aria-label="切换手机或电脑模式" title="手机/电脑模式切换"><span id="mobile-toggle-icon" class="ui-icon ui-icon-devices" aria-hidden="true"></span></button>
|
| 35 |
<button id="theme-btn" class="icon-btn" aria-label="切换到白天模式" title="白天模式"><svg class="theme-icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M20 15.5A8 8 0 1 1 8.5 4 6.5 6.5 0 0 0 20 15.5Z"/></svg></button>
|
| 36 |
</div>
|
| 37 |
|
| 38 |
</header>
|
| 39 |
<div id="app-body" class="app-body">
|
| 40 |
|
| 41 |
-
<aside id="left-sidebar" class="sidebar left-sidebar">
|
| 42 |
-
<div class="sidebar-header"><span id="sidebar-title">数据包</span><button id="sidebar-expand-btn" class="icon-btn-sm" title="展开/收起侧边栏" style="display:none">↔</button></div>
|
| 43 |
<div id="sidebar-content" class="sidebar-content"><div class="sidebar-loading">加载中...</div></div>
|
| 44 |
|
| 45 |
</aside>
|
|
@@ -47,14 +47,14 @@
|
|
| 47 |
<main id="main-content" class="main-content">
|
| 48 |
<div id="status-bar" class="status-bar">
|
| 49 |
<div class="status-left">
|
| 50 |
-
<span id="result-count" class="result-count"></span>
|
| 51 |
<label class="multi-toggle" id="multi-toggle-label" style="display:none"><input type="checkbox" id="multi-select-toggle"><span>多选</span></label>
|
| 52 |
</div>
|
| 53 |
<div class="status-right">
|
| 54 |
<button id="clear-filters-btn" class="text-btn" style="display:none">清空筛选</button>
|
| 55 |
<div class="sort-group">
|
| 56 |
<span class="sort-label">排序:</span>
|
| 57 |
-
<select id="sort-select" class="sort-select">
|
| 58 |
<option value="relevance">相关度</option>
|
| 59 |
<option value="name">文件名</option>
|
| 60 |
<option value="size">文件大小</option>
|
|
@@ -69,8 +69,8 @@
|
|
| 69 |
<button id="multi-zip-download" class="multi-action-btn primary">合并下载</button>
|
| 70 |
<span id="multi-selected-count" class="multi-selected-count"></span>
|
| 71 |
</div>
|
| 72 |
-
<div id="results-container" class="results-container" aria-busy="false">
|
| 73 |
-
<div id="results-list" class="results-list"></div>
|
| 74 |
<div id="results-loading" class="loading-spinner" style="display:none"><div class="spinner"></div></div>
|
| 75 |
<div id="empty-state" class="empty-state" style="display:none">
|
| 76 |
<div class="empty-title">没有找到结果</div>
|
|
@@ -78,13 +78,13 @@
|
|
| 78 |
<button id="empty-random-btn" class="random-big-btn">随机来一篇</button>
|
| 79 |
</div>
|
| 80 |
</div>
|
| 81 |
-
<div id="load-info" class="load-info" style="display:none">已加载 <span id="loaded-count">0</span> / <span id="total-count">0</span> 条</div>
|
| 82 |
-
<div id="preview-panel" class="preview-panel" style="display:none"></div>
|
| 83 |
|
| 84 |
</main>
|
| 85 |
|
| 86 |
-
<aside id="right-sidebar" class="sidebar right-sidebar">
|
| 87 |
-
<div class="sidebar-header"><span>搜索过滤</span><button id="close-filters-btn" class="icon-btn-sm" aria-label="关闭过滤">×</button></div>
|
| 88 |
<div class="sidebar-content">
|
| 89 |
<div class="filter-section" id="fulltext-toggle-section">
|
| 90 |
<label class="toggle-row">
|
|
@@ -125,11 +125,11 @@
|
|
| 125 |
<div class="filter-section">
|
| 126 |
<div class="filter-section-title">文件大小</div>
|
| 127 |
<div class="filter-size-row">
|
| 128 |
-
<input type="number" id="filter-min-size" class="size-input" placeholder="最小" min="0" step="0.1">
|
| 129 |
-
<select id="filter-min-unit" class="size-unit"><option value="B">B</option><option value="KB">KB</option><option value="MB" selected>MB</option><option value="GB">GB</option></select>
|
| 130 |
<span class="size-sep">—</span>
|
| 131 |
-
<input type="number" id="filter-max-size" class="size-input" placeholder="最大" min="0" step="0.1">
|
| 132 |
-
<select id="filter-max-unit" class="size-unit"><option value="B">B</option><option value="KB">KB</option><option value="MB" selected>MB</option><option value="GB">GB</option></select>
|
| 133 |
</div>
|
| 134 |
</div>
|
| 135 |
</div>
|
|
@@ -137,7 +137,7 @@
|
|
| 137 |
</aside>
|
| 138 |
</div>
|
| 139 |
|
| 140 |
-
<footer class="footer">
|
| 141 |
<div class="hitokoto"></div>
|
| 142 |
<button id="random-book-btn" class="random-book-btn" title="随机来一篇">
|
| 143 |
<span class="ui-icon ui-icon-book" aria-hidden="true"></span>
|
|
|
|
| 16 |
|
| 17 |
<header id="header">
|
| 18 |
<div class="header-left">
|
| 19 |
+
<button id="hamburger-btn" class="icon-btn" aria-label="打开数据包导航" aria-controls="left-sidebar" aria-expanded="true"><span class="ui-icon ui-icon-menu" aria-hidden="true"></span></button>
|
| 20 |
<a id="header-logo" class="header-logo" href="https://huggingface.co/VOMEBOOK" title="VOMEBOOK">
|
| 21 |
<span id="header-title">VOMEBOOK</span>
|
| 22 |
</a>
|
|
|
|
| 24 |
<div class="header-center">
|
| 25 |
<div class="search-box">
|
| 26 |
<span class="search-icon"><span class="ui-icon ui-icon-search" aria-hidden="true"></span></span>
|
| 27 |
+
<input type="search" id="search-input" class="search-input" aria-label="搜索 TXT 文件和正文" placeholder="全文搜索 TXT 正文..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
| 28 |
<kbd class="search-kbd">/</kbd>
|
| 29 |
+
<div id="search-history-dropdown" class="search-history-dropdown" role="region" aria-label="搜索历史" style="display:none"></div>
|
| 30 |
</div>
|
| 31 |
</div>
|
| 32 |
<div class="header-right">
|
| 33 |
+
<button id="settings-btn" class="icon-btn" aria-label="打开搜索过滤" aria-controls="right-sidebar" aria-expanded="false"><span class="ui-icon ui-icon-sliders" aria-hidden="true"></span></button>
|
| 34 |
+
<button id="mobile-toggle-btn" class="icon-btn" aria-label="切换手机或电脑模式" aria-pressed="false" title="手机/电脑模式切换"><span id="mobile-toggle-icon" class="ui-icon ui-icon-devices" aria-hidden="true"></span></button>
|
| 35 |
<button id="theme-btn" class="icon-btn" aria-label="切换到白天模式" title="白天模式"><svg class="theme-icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M20 15.5A8 8 0 1 1 8.5 4 6.5 6.5 0 0 0 20 15.5Z"/></svg></button>
|
| 36 |
</div>
|
| 37 |
|
| 38 |
</header>
|
| 39 |
<div id="app-body" class="app-body">
|
| 40 |
|
| 41 |
+
<aside id="left-sidebar" class="sidebar left-sidebar" aria-labelledby="sidebar-title">
|
| 42 |
+
<div class="sidebar-header"><span id="sidebar-title">数据包</span><button id="sidebar-expand-btn" class="icon-btn-sm" aria-label="展开数据包导航" aria-expanded="false" title="展开/收起侧边栏" style="display:none">↔</button></div>
|
| 43 |
<div id="sidebar-content" class="sidebar-content"><div class="sidebar-loading">加载中...</div></div>
|
| 44 |
|
| 45 |
</aside>
|
|
|
|
| 47 |
<main id="main-content" class="main-content">
|
| 48 |
<div id="status-bar" class="status-bar">
|
| 49 |
<div class="status-left">
|
| 50 |
+
<span id="result-count" class="result-count" role="status"></span>
|
| 51 |
<label class="multi-toggle" id="multi-toggle-label" style="display:none"><input type="checkbox" id="multi-select-toggle"><span>多选</span></label>
|
| 52 |
</div>
|
| 53 |
<div class="status-right">
|
| 54 |
<button id="clear-filters-btn" class="text-btn" style="display:none">清空筛选</button>
|
| 55 |
<div class="sort-group">
|
| 56 |
<span class="sort-label">排序:</span>
|
| 57 |
+
<select id="sort-select" class="sort-select" aria-label="排序方式">
|
| 58 |
<option value="relevance">相关度</option>
|
| 59 |
<option value="name">文件名</option>
|
| 60 |
<option value="size">文件大小</option>
|
|
|
|
| 69 |
<button id="multi-zip-download" class="multi-action-btn primary">合并下载</button>
|
| 70 |
<span id="multi-selected-count" class="multi-selected-count"></span>
|
| 71 |
</div>
|
| 72 |
+
<div id="results-container" class="results-container" aria-busy="false" role="region" aria-label="搜索结果">
|
| 73 |
+
<div id="results-list" class="results-list" role="list"></div>
|
| 74 |
<div id="results-loading" class="loading-spinner" style="display:none"><div class="spinner"></div></div>
|
| 75 |
<div id="empty-state" class="empty-state" style="display:none">
|
| 76 |
<div class="empty-title">没有找到结果</div>
|
|
|
|
| 78 |
<button id="empty-random-btn" class="random-big-btn">随机来一篇</button>
|
| 79 |
</div>
|
| 80 |
</div>
|
| 81 |
+
<div id="load-info" class="load-info" role="status" aria-live="polite" aria-atomic="true" style="display:none">已加载 <span id="loaded-count">0</span> / <span id="total-count">0</span> 条</div>
|
| 82 |
+
<div id="preview-panel" class="preview-panel" role="dialog" aria-modal="true" aria-label="文件预览" tabindex="-1" hidden style="display:none"></div>
|
| 83 |
|
| 84 |
</main>
|
| 85 |
|
| 86 |
+
<aside id="right-sidebar" class="sidebar right-sidebar" aria-label="搜索过滤">
|
| 87 |
+
<div class="sidebar-header"><span>搜索过滤</span><button id="close-filters-btn" class="icon-btn-sm" aria-label="关闭搜索过滤">×</button></div>
|
| 88 |
<div class="sidebar-content">
|
| 89 |
<div class="filter-section" id="fulltext-toggle-section">
|
| 90 |
<label class="toggle-row">
|
|
|
|
| 125 |
<div class="filter-section">
|
| 126 |
<div class="filter-section-title">文件大小</div>
|
| 127 |
<div class="filter-size-row">
|
| 128 |
+
<input type="number" id="filter-min-size" class="size-input" aria-label="最小文件大小" placeholder="最小" min="0" step="0.1">
|
| 129 |
+
<select id="filter-min-unit" class="size-unit" aria-label="最小文件大小单位"><option value="B">B</option><option value="KB">KB</option><option value="MB" selected>MB</option><option value="GB">GB</option></select>
|
| 130 |
<span class="size-sep">—</span>
|
| 131 |
+
<input type="number" id="filter-max-size" class="size-input" aria-label="最大文件大小" placeholder="最大" min="0" step="0.1">
|
| 132 |
+
<select id="filter-max-unit" class="size-unit" aria-label="最大文件大小单位"><option value="B">B</option><option value="KB">KB</option><option value="MB" selected>MB</option><option value="GB">GB</option></select>
|
| 133 |
</div>
|
| 134 |
</div>
|
| 135 |
</div>
|
|
|
|
| 137 |
</aside>
|
| 138 |
</div>
|
| 139 |
|
| 140 |
+
<footer id="footer" class="footer">
|
| 141 |
<div class="hitokoto"></div>
|
| 142 |
<button id="random-book-btn" class="random-book-btn" title="随机来一篇">
|
| 143 |
<span class="ui-icon ui-icon-book" aria-hidden="true"></span>
|
static/style.css
CHANGED
|
@@ -321,12 +321,23 @@ ul, li {
|
|
| 321 |
}
|
| 322 |
|
| 323 |
.search-history-dropdown .history-text {
|
| 324 |
-
flex: 1;
|
| 325 |
overflow: hidden;
|
| 326 |
text-overflow: ellipsis;
|
| 327 |
white-space: nowrap;
|
| 328 |
}
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
.search-history-dropdown .history-del {
|
| 331 |
flex-shrink: 0;
|
| 332 |
width: 22px;
|
|
@@ -713,6 +724,10 @@ body.preview-open #preview-panel .preview-header {
|
|
| 713 |
}
|
| 714 |
|
| 715 |
.sidebar-breadcrumb .crumb-item {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
color: var(--primary);
|
| 717 |
cursor: pointer;
|
| 718 |
padding: 2px 4px;
|
|
@@ -743,6 +758,11 @@ body.preview-open #preview-panel .preview-header {
|
|
| 743 |
}
|
| 744 |
|
| 745 |
.back-to-global {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
display: flex;
|
| 747 |
align-items: center;
|
| 748 |
gap: 6px;
|
|
@@ -767,6 +787,11 @@ body.preview-open #preview-panel .preview-header {
|
|
| 767 |
}
|
| 768 |
|
| 769 |
.browser-item {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 770 |
display: flex;
|
| 771 |
align-items: center;
|
| 772 |
gap: 8px;
|
|
@@ -853,6 +878,11 @@ body.light .left-sidebar.expanded-wide .browser-item:nth-child(even) {
|
|
| 853 |
}
|
| 854 |
|
| 855 |
.repo-list-item {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
display: flex;
|
| 857 |
align-items: center;
|
| 858 |
gap: 10px;
|
|
@@ -1313,6 +1343,14 @@ body.mobile .multi-toggle { padding: 1px 4px; font-size: 11px; }
|
|
| 1313 |
}
|
| 1314 |
|
| 1315 |
.result-path .path-folder {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1316 |
cursor: pointer;
|
| 1317 |
color: var(--secondary);
|
| 1318 |
transition: color var(--transition);
|
|
|
|
| 321 |
}
|
| 322 |
|
| 323 |
.search-history-dropdown .history-text {
|
|
|
|
| 324 |
overflow: hidden;
|
| 325 |
text-overflow: ellipsis;
|
| 326 |
white-space: nowrap;
|
| 327 |
}
|
| 328 |
|
| 329 |
+
.search-history-dropdown .history-select {
|
| 330 |
+
flex: 1;
|
| 331 |
+
min-width: 0;
|
| 332 |
+
padding: 0;
|
| 333 |
+
border: 0;
|
| 334 |
+
background: none;
|
| 335 |
+
color: inherit;
|
| 336 |
+
font: inherit;
|
| 337 |
+
text-align: left;
|
| 338 |
+
overflow: hidden;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
.search-history-dropdown .history-del {
|
| 342 |
flex-shrink: 0;
|
| 343 |
width: 22px;
|
|
|
|
| 724 |
}
|
| 725 |
|
| 726 |
.sidebar-breadcrumb .crumb-item {
|
| 727 |
+
border: 0;
|
| 728 |
+
background: none;
|
| 729 |
+
font: inherit;
|
| 730 |
+
text-align: left;
|
| 731 |
color: var(--primary);
|
| 732 |
cursor: pointer;
|
| 733 |
padding: 2px 4px;
|
|
|
|
| 758 |
}
|
| 759 |
|
| 760 |
.back-to-global {
|
| 761 |
+
width: 100%;
|
| 762 |
+
border: 0;
|
| 763 |
+
background: none;
|
| 764 |
+
font: inherit;
|
| 765 |
+
text-align: left;
|
| 766 |
display: flex;
|
| 767 |
align-items: center;
|
| 768 |
gap: 6px;
|
|
|
|
| 787 |
}
|
| 788 |
|
| 789 |
.browser-item {
|
| 790 |
+
width: 100%;
|
| 791 |
+
border: 0;
|
| 792 |
+
background: none;
|
| 793 |
+
font: inherit;
|
| 794 |
+
text-align: left;
|
| 795 |
display: flex;
|
| 796 |
align-items: center;
|
| 797 |
gap: 8px;
|
|
|
|
| 878 |
}
|
| 879 |
|
| 880 |
.repo-list-item {
|
| 881 |
+
width: 100%;
|
| 882 |
+
border: 0;
|
| 883 |
+
background: none;
|
| 884 |
+
font: inherit;
|
| 885 |
+
text-align: left;
|
| 886 |
display: flex;
|
| 887 |
align-items: center;
|
| 888 |
gap: 10px;
|
|
|
|
| 1343 |
}
|
| 1344 |
|
| 1345 |
.result-path .path-folder {
|
| 1346 |
+
display: inline;
|
| 1347 |
+
appearance: none;
|
| 1348 |
+
-webkit-appearance: none;
|
| 1349 |
+
border: 0;
|
| 1350 |
+
background: none;
|
| 1351 |
+
padding: 0;
|
| 1352 |
+
font: inherit;
|
| 1353 |
+
text-align: left;
|
| 1354 |
cursor: pointer;
|
| 1355 |
color: var(--secondary);
|
| 1356 |
transition: color var(--transition);
|