mirror of
https://github.com/NotAShelf/nvf.git
synced 2026-04-27 20:05:23 +00:00
deploy: 35a64b0c64
This commit is contained in:
parent
0f6f61fa3d
commit
d39bd7cecd
64 changed files with 8748 additions and 249256 deletions
518
assets/main.js
518
assets/main.js
|
|
@ -61,13 +61,16 @@ function createMobileElements() {
|
|||
const mobileSearchPopup = document.createElement("div");
|
||||
mobileSearchPopup.id = "mobile-search-popup";
|
||||
mobileSearchPopup.className = "mobile-search-popup";
|
||||
mobileSearchPopup.setAttribute("role", "dialog");
|
||||
mobileSearchPopup.setAttribute("aria-modal", "true");
|
||||
mobileSearchPopup.setAttribute("aria-label", "Search");
|
||||
mobileSearchPopup.innerHTML = `
|
||||
<div class="mobile-search-container">
|
||||
<div class="mobile-search-container" role="document">
|
||||
<div class="mobile-search-header">
|
||||
<input type="text" id="mobile-search-input" placeholder="Search..." />
|
||||
<button id="close-mobile-search" class="close-mobile-search" aria-label="Close search">×</button>
|
||||
<input type="search" id="mobile-search-input" placeholder="Search..." aria-label="Search" autocomplete="off" />
|
||||
<button type="button" id="close-mobile-search" class="close-mobile-search" aria-label="Close search">×</button>
|
||||
</div>
|
||||
<div id="mobile-search-results" class="mobile-search-results"></div>
|
||||
<div id="mobile-search-results" class="mobile-search-results" role="region" aria-live="polite" aria-label="Search results"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -85,40 +88,51 @@ function createMobileElements() {
|
|||
}
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections with state persistence
|
||||
function initCollapsibleSections() {
|
||||
// Target sections in both desktop and mobile sidebars
|
||||
const sections = document.querySelectorAll(
|
||||
".sidebar .sidebar-section, .mobile-sidebar-content .sidebar-section",
|
||||
);
|
||||
// Highlight search terms on target pages
|
||||
function highlightTextInContent(container, terms) {
|
||||
if (!container || !terms || terms.length === 0) return;
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionId = section.dataset.section;
|
||||
if (!sectionId) return;
|
||||
// Create a case-insensitive regex pattern
|
||||
const pattern = terms
|
||||
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||
.join("|");
|
||||
const regex = new RegExp(`(${pattern})`, "gi");
|
||||
|
||||
const storageKey = `sidebar-section-${sectionId}`;
|
||||
const savedState = localStorage.getItem(storageKey);
|
||||
// Elements to skip highlighting
|
||||
const skipTags = new Set(["SCRIPT", "STYLE", "CODE", "PRE", "MARK"]);
|
||||
|
||||
// Restore saved state (default is open)
|
||||
if (savedState === "closed") {
|
||||
section.removeAttribute("open");
|
||||
function highlightNode(node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent;
|
||||
// Use match instead of test to avoid regex state issues
|
||||
if (text.match(regex)) {
|
||||
const span = document.createElement("span");
|
||||
// Create a fresh regex for replace to avoid state issues
|
||||
const replaceRegex = new RegExp(`(${pattern})`, "gi");
|
||||
span.innerHTML = text.replace(
|
||||
replaceRegex,
|
||||
'<mark class="search-highlight">$1</mark>',
|
||||
);
|
||||
node.replaceWith(...Array.from(span.childNodes));
|
||||
}
|
||||
} else if (
|
||||
node.nodeType === Node.ELEMENT_NODE &&
|
||||
!skipTags.has(node.tagName)
|
||||
) {
|
||||
Array.from(node.childNodes).forEach(highlightNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Save state on toggle and sync between desktop/mobile
|
||||
section.addEventListener("toggle", () => {
|
||||
localStorage.setItem(storageKey, section.open ? "open" : "closed");
|
||||
highlightNode(container);
|
||||
|
||||
// Sync state between desktop and mobile versions
|
||||
const allWithSameSection = document.querySelectorAll(
|
||||
`.sidebar-section[data-section="${sectionId}"]`,
|
||||
);
|
||||
allWithSameSection.forEach((el) => {
|
||||
if (el !== section) {
|
||||
el.open = section.open;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// Scroll to first highlight after a brief delay
|
||||
setTimeout(() => {
|
||||
const firstHighlight = container.querySelector(".search-highlight");
|
||||
if (firstHighlight) {
|
||||
firstHighlight.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
firstHighlight.classList.add("search-highlight-active");
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Initialize scroll spy
|
||||
|
|
@ -208,22 +222,100 @@ function initScrollSpy() {
|
|||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Apply sidebar state immediately before DOM rendering
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
try {
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
document.documentElement.classList.add("sidebar-collapsed");
|
||||
document.body.classList.add("sidebar-collapsed");
|
||||
}
|
||||
} catch {
|
||||
// localStorage unavailable
|
||||
}
|
||||
|
||||
if (!document.querySelector(".mobile-sidebar-fab")) {
|
||||
createMobileElements();
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections
|
||||
// after mobile elements are created
|
||||
initCollapsibleSections();
|
||||
|
||||
// Initialize scroll spy for page TOC
|
||||
initScrollSpy();
|
||||
|
||||
// Template container for collapsed sidebar content (prevents Ctrl+F from finding hidden content)
|
||||
const sidebarHiddenContainer = document.createElement("template");
|
||||
|
||||
// Handle sidebar section toggles - move content to template when collapsed
|
||||
document
|
||||
.querySelectorAll(".sidebar-section > .sidebar-section-content")
|
||||
.forEach((content) => {
|
||||
const details = content.parentElement;
|
||||
const toggleContent = () => {
|
||||
if (details.hasAttribute("open")) {
|
||||
// Section opened - move content back to DOM
|
||||
if (sidebarHiddenContainer.content.contains(content)) {
|
||||
const summary = details.querySelector("summary");
|
||||
details.insertBefore(
|
||||
content,
|
||||
summary ? summary.nextSibling : details.firstChild,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Section closed - move content to template (removes from DOM, Ctrl+F won't find it)
|
||||
if (content.parentElement === details) {
|
||||
sidebarHiddenContainer.content.appendChild(content);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Use MutationObserver to detect open/close changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === "open") {
|
||||
toggleContent();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(details, { attributes: true });
|
||||
|
||||
// Initial state check
|
||||
if (!details.hasAttribute("open")) {
|
||||
sidebarHiddenContainer.content.appendChild(content);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle sidebar collapse/expand - move entire sidebar to template when collapsed
|
||||
const sidebar = document.querySelector(".sidebar");
|
||||
const sidebarObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === "class") {
|
||||
const isCollapsed =
|
||||
document.documentElement.classList.contains("sidebar-collapsed");
|
||||
if (isCollapsed) {
|
||||
// Sidebar collapsed - move to template
|
||||
if (sidebar.parentElement) {
|
||||
sidebarHiddenContainer.content.appendChild(sidebar);
|
||||
}
|
||||
} else {
|
||||
// Sidebar expanded - move back to DOM
|
||||
if (sidebarHiddenContainer.content.contains(sidebar)) {
|
||||
const layout = document.querySelector(".layout");
|
||||
const contentEl = document.querySelector(".content");
|
||||
if (layout) {
|
||||
layout.insertBefore(sidebar, contentEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (sidebar) {
|
||||
sidebarObserver.observe(document.documentElement, { attributes: true });
|
||||
|
||||
// Initial state - if collapsed, move sidebar to template
|
||||
if (document.documentElement.classList.contains("sidebar-collapsed")) {
|
||||
sidebarHiddenContainer.content.appendChild(sidebar);
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop Sidebar Toggle
|
||||
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
||||
|
||||
|
|
@ -239,10 +331,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
document.body.classList.toggle("sidebar-collapsed");
|
||||
|
||||
// Use documentElement to check state and save to localStorage
|
||||
const isCollapsed = document.documentElement.classList.contains(
|
||||
"sidebar-collapsed",
|
||||
);
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
||||
const isCollapsed =
|
||||
document.documentElement.classList.contains("sidebar-collapsed");
|
||||
try {
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
||||
} catch {
|
||||
// localStorage unavailable
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -505,13 +600,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
const optionsContainer = document.querySelector(".options-container");
|
||||
if (!optionsContainer) return;
|
||||
|
||||
// Only inject the style if it doesn't already exist
|
||||
if (!document.head.querySelector("style[data-options-hidden]")) {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.setAttribute("data-options-hidden", "");
|
||||
styleEl.textContent = ".option-hidden{display:none!important}";
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
// Template container for hidden options
|
||||
const hiddenOptionsContainer = document.createElement("template");
|
||||
hiddenOptionsContainer.id = "hidden-options-container";
|
||||
document.body.appendChild(hiddenOptionsContainer);
|
||||
|
||||
// Create filter results counter
|
||||
const filterResults = document.createElement("div");
|
||||
|
|
@ -522,8 +614,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
);
|
||||
|
||||
// Detect if we're on a mobile device
|
||||
const isMobile = window.innerWidth < 768 ||
|
||||
/Mobi|Android/i.test(navigator.userAgent);
|
||||
const isMobile =
|
||||
window.innerWidth < 768 || /Mobi|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Cache all option elements and their searchable content
|
||||
const options = Array.from(document.querySelectorAll(".option"));
|
||||
|
|
@ -580,29 +672,26 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
|
||||
|
||||
if (startIdx < itemsToProcess.length) {
|
||||
// Process current chunk
|
||||
// Move visible items to container, hide others
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
const item = itemsToProcess[i];
|
||||
if (item.visible) {
|
||||
item.element.classList.remove("option-hidden");
|
||||
optionsContainer.appendChild(item.element);
|
||||
} else {
|
||||
item.element.classList.add("option-hidden");
|
||||
hiddenOptionsContainer.content.appendChild(item.element);
|
||||
}
|
||||
}
|
||||
|
||||
currentChunk++;
|
||||
pendingRender = requestAnimationFrame(processNextChunk);
|
||||
} else {
|
||||
// Finished processing all chunks
|
||||
pendingRender = null;
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
// Update counter at the very end for best performance
|
||||
if (filterResults.visibleCount !== undefined) {
|
||||
if (filterResults.visibleCount < totalCount) {
|
||||
filterResults.textContent =
|
||||
`Showing ${filterResults.visibleCount} of ${totalCount} options`;
|
||||
filterResults.textContent = `Showing ${filterResults.visibleCount} of ${totalCount} options`;
|
||||
filterResults.style.display = "block";
|
||||
} else {
|
||||
filterResults.style.display = "none";
|
||||
|
|
@ -611,9 +700,17 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
}
|
||||
}
|
||||
|
||||
// Initialize: keep all options visible by default
|
||||
// They will be moved to hidden container only when filtering
|
||||
function filterOptions() {
|
||||
const searchTerm = optionsFilter.value.toLowerCase().trim();
|
||||
|
||||
// Skip if search term hasn't changed
|
||||
if (filterOptions.lastTerm === searchTerm) {
|
||||
return;
|
||||
}
|
||||
filterOptions.lastTerm = searchTerm;
|
||||
|
||||
if (pendingRender) {
|
||||
cancelAnimationFrame(pendingRender);
|
||||
pendingRender = null;
|
||||
|
|
@ -622,12 +719,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
itemsToProcess = [];
|
||||
|
||||
if (searchTerm === "") {
|
||||
// Restore original DOM order when filter is cleared
|
||||
// Restore to original order
|
||||
const fragment = document.createDocumentFragment();
|
||||
originalOptionOrder.forEach((option) => {
|
||||
option.classList.remove("option-hidden");
|
||||
fragment.appendChild(option);
|
||||
hiddenOptionsContainer.content.appendChild(option);
|
||||
});
|
||||
while (hiddenOptionsContainer.content.firstChild) {
|
||||
fragment.appendChild(hiddenOptionsContainer.content.firstChild);
|
||||
}
|
||||
optionsContainer.appendChild(fragment);
|
||||
filterResults.style.display = "none";
|
||||
return;
|
||||
|
|
@ -640,55 +739,51 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
|
||||
const titleMatches = [];
|
||||
const descMatches = [];
|
||||
optionsData.forEach((data) => {
|
||||
let isTitleMatch = false;
|
||||
let isDescMatch = false;
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
isTitleMatch = data.name.includes(term);
|
||||
isDescMatch = !isTitleMatch && data.description.includes(term);
|
||||
} else {
|
||||
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
|
||||
isDescMatch = !isTitleMatch &&
|
||||
searchTerms.every((term) => data.description.includes(term));
|
||||
}
|
||||
const term = searchTerms[0];
|
||||
|
||||
for (let i = 0; i < optionsData.length; i++) {
|
||||
const data = optionsData[i];
|
||||
const isTitleMatch = data.name.includes(term);
|
||||
const isDescMatch = !isTitleMatch && data.description.includes(term);
|
||||
|
||||
if (isTitleMatch) {
|
||||
visibleCount++;
|
||||
titleMatches.push(data);
|
||||
} else if (isDescMatch) {
|
||||
visibleCount++;
|
||||
descMatches.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
if (searchTerms.length === 1) {
|
||||
const term = searchTerms[0];
|
||||
titleMatches.sort(
|
||||
(a, b) => a.name.indexOf(term) - b.name.indexOf(term),
|
||||
);
|
||||
descMatches.sort(
|
||||
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
||||
);
|
||||
}
|
||||
|
||||
titleMatches.sort((a, b) => a.name.indexOf(term) - b.name.indexOf(term));
|
||||
descMatches.sort(
|
||||
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
||||
);
|
||||
|
||||
const visibleElements = new Set();
|
||||
itemsToProcess = [];
|
||||
titleMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
for (let i = 0; i < titleMatches.length; i++) {
|
||||
const data = titleMatches[i];
|
||||
visibleElements.add(data.element);
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
descMatches.forEach((data) => {
|
||||
visibleCount++;
|
||||
}
|
||||
for (let i = 0; i < descMatches.length; i++) {
|
||||
const data = descMatches[i];
|
||||
visibleElements.add(data.element);
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
});
|
||||
optionsData.forEach((data) => {
|
||||
if (!itemsToProcess.some((item) => item.element === data.element)) {
|
||||
}
|
||||
for (let i = 0; i < optionsData.length; i++) {
|
||||
const data = optionsData[i];
|
||||
if (!visibleElements.has(data.element)) {
|
||||
itemsToProcess.push({ element: data.element, visible: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reorder DOM so all title matches, then desc matches, then hidden
|
||||
const fragment = document.createDocumentFragment();
|
||||
itemsToProcess.forEach((item) => {
|
||||
fragment.appendChild(item.element);
|
||||
});
|
||||
for (let i = 0; i < itemsToProcess.length; i++) {
|
||||
fragment.appendChild(itemsToProcess[i].element);
|
||||
}
|
||||
optionsContainer.appendChild(fragment);
|
||||
|
||||
filterResults.visibleCount = visibleCount;
|
||||
|
|
@ -700,7 +795,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
|
||||
// Set up event listeners
|
||||
optionsFilter.addEventListener("input", debouncedFilter);
|
||||
optionsFilter.addEventListener("change", filterOptions);
|
||||
|
||||
// Allow clearing with Escape key
|
||||
optionsFilter.addEventListener("keydown", function (e) {
|
||||
|
|
@ -717,7 +811,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
}
|
||||
});
|
||||
|
||||
// Initially trigger filter if there's a value
|
||||
// Run initial filter if there's a value
|
||||
if (optionsFilter.value) {
|
||||
filterOptions();
|
||||
}
|
||||
|
|
@ -737,4 +831,232 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Lib filter functionality
|
||||
const libFilter = document.getElementById("lib-filter");
|
||||
if (libFilter && document.querySelector(".lib-container")) {
|
||||
const libContainer = document.querySelector(".lib-container");
|
||||
|
||||
const hiddenLibContainer = document.createElement("template");
|
||||
hiddenLibContainer.id = "hidden-lib-container";
|
||||
document.body.appendChild(hiddenLibContainer);
|
||||
|
||||
const filterResults = document.createElement("div");
|
||||
filterResults.className = "filter-results";
|
||||
libFilter.parentNode.insertBefore(filterResults, libFilter.nextSibling);
|
||||
|
||||
const isMobile =
|
||||
window.innerWidth < 768 || /Mobi|Android/i.test(navigator.userAgent);
|
||||
|
||||
const libEntries = Array.from(document.querySelectorAll(".lib-entry"));
|
||||
const totalCount = libEntries.length;
|
||||
const originalLibOrder = libEntries.slice();
|
||||
|
||||
const libData = libEntries.map((entry) => {
|
||||
const nameElem = entry.querySelector(".lib-entry-name");
|
||||
const descriptionElem = entry.querySelector(".lib-entry-description");
|
||||
const id = entry.id ? entry.id.toLowerCase() : "";
|
||||
const name = nameElem ? nameElem.textContent.toLowerCase() : "";
|
||||
const description = descriptionElem
|
||||
? descriptionElem.textContent.toLowerCase()
|
||||
: "";
|
||||
|
||||
const keywords = (id + " " + name + " " + description)
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 1);
|
||||
|
||||
return {
|
||||
element: entry,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
keywords,
|
||||
searchText: (id + " " + name + " " + description).toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
const CHUNK_SIZE = isMobile ? 15 : 40;
|
||||
let pendingRender = null;
|
||||
let currentChunk = 0;
|
||||
let itemsToProcess = [];
|
||||
|
||||
function debounceLib(func, wait) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
function processNextChunkLib() {
|
||||
const startIdx = currentChunk * CHUNK_SIZE;
|
||||
const endIdx = Math.min(startIdx + CHUNK_SIZE, itemsToProcess.length);
|
||||
|
||||
if (startIdx < itemsToProcess.length) {
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
const item = itemsToProcess[i];
|
||||
if (item.visible) {
|
||||
libContainer.appendChild(item.element);
|
||||
} else {
|
||||
hiddenLibContainer.content.appendChild(item.element);
|
||||
}
|
||||
}
|
||||
|
||||
currentChunk++;
|
||||
pendingRender = requestAnimationFrame(processNextChunkLib);
|
||||
} else {
|
||||
pendingRender = null;
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
if (filterResults.visibleCount !== undefined) {
|
||||
if (filterResults.visibleCount < totalCount) {
|
||||
filterResults.textContent = `Showing ${filterResults.visibleCount} of ${totalCount} functions`;
|
||||
filterResults.style.display = "block";
|
||||
} else {
|
||||
filterResults.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterLib() {
|
||||
const searchTerm = libFilter.value.toLowerCase().trim();
|
||||
|
||||
if (filterLib.lastTerm === searchTerm) {
|
||||
return;
|
||||
}
|
||||
filterLib.lastTerm = searchTerm;
|
||||
|
||||
if (pendingRender) {
|
||||
cancelAnimationFrame(pendingRender);
|
||||
pendingRender = null;
|
||||
}
|
||||
currentChunk = 0;
|
||||
itemsToProcess = [];
|
||||
|
||||
if (searchTerm === "") {
|
||||
const fragment = document.createDocumentFragment();
|
||||
originalLibOrder.forEach((entry) => {
|
||||
hiddenLibContainer.content.appendChild(entry);
|
||||
});
|
||||
while (hiddenLibContainer.content.firstChild) {
|
||||
fragment.appendChild(hiddenLibContainer.content.firstChild);
|
||||
}
|
||||
libContainer.appendChild(fragment);
|
||||
filterResults.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const searchTerms = searchTerm
|
||||
.split(/\s+/)
|
||||
.filter((term) => term.length > 0);
|
||||
let visibleCount = 0;
|
||||
|
||||
const titleMatches = [];
|
||||
const descMatches = [];
|
||||
const term = searchTerms[0];
|
||||
|
||||
for (let i = 0; i < libData.length; i++) {
|
||||
const data = libData[i];
|
||||
const isTitleMatch = data.name.includes(term);
|
||||
const isDescMatch = !isTitleMatch && data.description.includes(term);
|
||||
|
||||
if (isTitleMatch) {
|
||||
visibleCount++;
|
||||
titleMatches.push(data);
|
||||
} else if (isDescMatch) {
|
||||
visibleCount++;
|
||||
descMatches.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
titleMatches.sort((a, b) => a.name.indexOf(term) - b.name.indexOf(term));
|
||||
descMatches.sort(
|
||||
(a, b) => a.description.indexOf(term) - b.description.indexOf(term),
|
||||
);
|
||||
|
||||
const visibleElements = new Set();
|
||||
itemsToProcess = [];
|
||||
for (let i = 0; i < titleMatches.length; i++) {
|
||||
const data = titleMatches[i];
|
||||
visibleElements.add(data.element);
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
}
|
||||
for (let i = 0; i < descMatches.length; i++) {
|
||||
const data = descMatches[i];
|
||||
visibleElements.add(data.element);
|
||||
itemsToProcess.push({ element: data.element, visible: true });
|
||||
}
|
||||
for (let i = 0; i < libData.length; i++) {
|
||||
const data = libData[i];
|
||||
if (!visibleElements.has(data.element)) {
|
||||
itemsToProcess.push({ element: data.element, visible: false });
|
||||
}
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < itemsToProcess.length; i++) {
|
||||
fragment.appendChild(itemsToProcess[i].element);
|
||||
}
|
||||
libContainer.appendChild(fragment);
|
||||
|
||||
filterResults.visibleCount = visibleCount;
|
||||
pendingRender = requestAnimationFrame(processNextChunkLib);
|
||||
}
|
||||
|
||||
const debouncedFilter = debounceLib(filterLib, isMobile ? 200 : 100);
|
||||
|
||||
libFilter.addEventListener("input", debouncedFilter);
|
||||
|
||||
libFilter.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
libFilter.value = "";
|
||||
filterLib();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (!document.hidden && libFilter.value) {
|
||||
filterLib();
|
||||
}
|
||||
});
|
||||
|
||||
if (libFilter.value) {
|
||||
filterLib();
|
||||
}
|
||||
|
||||
if (isMobile && totalCount > 50) {
|
||||
requestIdleCallback(() => {
|
||||
const sampleEntry = libEntries[0];
|
||||
if (sampleEntry) {
|
||||
const height = sampleEntry.offsetHeight;
|
||||
if (height > 0) {
|
||||
libEntries.forEach((entry) => {
|
||||
entry.style.containIntrinsicSize = `0 ${height}px`;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// URL-based search highlighting
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const highlightQuery = urlParams.get("highlight");
|
||||
if (highlightQuery && content) {
|
||||
// Simple tokenizer that doesn't depend on search engine
|
||||
const queryTerms = highlightQuery
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((term) => term.length >= 2); // min 2 chars like search engine
|
||||
|
||||
if (queryTerms.length > 0) {
|
||||
highlightTextInContent(content, queryTerms);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
504
assets/search.js
504
assets/search.js
|
|
@ -1,13 +1,25 @@
|
|||
if (!window.searchNamespace) window.searchNamespace = {};
|
||||
|
||||
class SearchEngine {
|
||||
// Characters to strip from search term ends for better matching
|
||||
static STRIP_TRAILING_CHARS_RE = /[.,!?;:'"…—–-]+$/g;
|
||||
|
||||
constructor() {
|
||||
this.documents = [];
|
||||
this.tokenMap = new Map();
|
||||
this.lowercaseCache = [];
|
||||
this.isLoaded = false;
|
||||
this.loadError = false;
|
||||
this.fullDocuments = null; // for lazy loading
|
||||
this.rootPath = window.searchNamespace?.rootPath || "";
|
||||
// Search configuration (loaded from search data)
|
||||
this.config = {
|
||||
minWordLength: 2,
|
||||
stopwords: [],
|
||||
boostTitle: 100.0,
|
||||
boostContent: 30.0,
|
||||
boostAnchor: 10.0,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if we can use Web Worker
|
||||
|
|
@ -53,19 +65,30 @@ class SearchEngine {
|
|||
throw new Error("Search data file not found at any expected location");
|
||||
}
|
||||
|
||||
console.log(`Loading search data from: ${usedPath}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const documents = await response.json();
|
||||
if (!Array.isArray(documents)) {
|
||||
throw new Error("Invalid search data format");
|
||||
// New format with config
|
||||
if (documents.documents && Array.isArray(documents.documents)) {
|
||||
this.config = {
|
||||
minWordLength: documents.min_word_length || 2,
|
||||
stopwords: documents.stopwords || [],
|
||||
boostTitle: documents.boost_title || 100.0,
|
||||
boostContent: documents.boost_content || 30.0,
|
||||
boostAnchor: documents.boost_anchor || 10.0,
|
||||
};
|
||||
this.initializeFromDocuments(documents.documents);
|
||||
} else {
|
||||
throw new Error("Invalid search data format");
|
||||
}
|
||||
} else {
|
||||
// Legacy format - just an array of documents
|
||||
this.initializeFromDocuments(documents);
|
||||
}
|
||||
|
||||
this.initializeFromDocuments(documents);
|
||||
this.isLoaded = true;
|
||||
console.log(`Loaded ${documents.length} documents for search`);
|
||||
} catch (error) {
|
||||
console.error("Error loading search data:", error);
|
||||
this.documents = [];
|
||||
|
|
@ -81,7 +104,6 @@ class SearchEngine {
|
|||
this.documents = [];
|
||||
} else {
|
||||
this.documents = documents;
|
||||
console.log(`Initialized with ${documents.length} documents`);
|
||||
}
|
||||
try {
|
||||
await this.buildTokenMap();
|
||||
|
|
@ -94,10 +116,13 @@ class SearchEngine {
|
|||
initializeIndex(indexData) {
|
||||
this.documents = indexData.documents || [];
|
||||
this.tokenMap = new Map(Object.entries(indexData.tokenMap || {}));
|
||||
this.lowercaseCache = this.documents.map((doc) => ({
|
||||
title: (doc.title || "").toLowerCase(),
|
||||
content: (doc.content || "").toLowerCase(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Build token map
|
||||
// This is helpful for faster searching with progressive loading
|
||||
// Build token map for faster searching
|
||||
buildTokenMap() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.tokenMap.clear();
|
||||
|
|
@ -111,6 +136,8 @@ class SearchEngine {
|
|||
const totalDocs = this.documents.length;
|
||||
let processedDocs = 0;
|
||||
|
||||
this.lowercaseCache = [];
|
||||
|
||||
try {
|
||||
// Process in chunks to avoid blocking UI
|
||||
const processChunk = (startIndex, chunkSize) => {
|
||||
|
|
@ -128,7 +155,14 @@ class SearchEngine {
|
|||
continue;
|
||||
}
|
||||
|
||||
const tokens = this.tokenize(doc.title + " " + doc.content);
|
||||
const lowerTitle = doc.title.toLowerCase();
|
||||
const lowerContent = doc.content.toLowerCase();
|
||||
this.lowercaseCache[i] = {
|
||||
title: lowerTitle,
|
||||
content: lowerContent,
|
||||
};
|
||||
|
||||
const tokens = this.tokenize(lowerTitle + " " + lowerContent);
|
||||
tokens.forEach((token) => {
|
||||
if (!this.tokenMap.has(token)) {
|
||||
this.tokenMap.set(token, []);
|
||||
|
|
@ -143,9 +177,6 @@ class SearchEngine {
|
|||
if (endIndex < totalDocs) {
|
||||
setTimeout(() => processChunk(endIndex, chunkSize), 0);
|
||||
} else {
|
||||
console.log(
|
||||
`Built token map with ${this.tokenMap.size} unique tokens from ${processedDocs} documents`,
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -308,8 +339,8 @@ class SearchEngine {
|
|||
}
|
||||
score = Math.min(1.0, score + boundaryBonus);
|
||||
|
||||
const lengthPenalty = Math.abs(query.length - n) /
|
||||
Math.max(query.length, m);
|
||||
const lengthPenalty =
|
||||
Math.abs(query.length - n) / Math.max(query.length, m);
|
||||
score -= lengthPenalty * 0.2;
|
||||
|
||||
return Math.max(0, Math.min(1.0, score));
|
||||
|
|
@ -319,7 +350,13 @@ class SearchEngine {
|
|||
if (!text || typeof text !== "string") return [];
|
||||
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const tokens = words.filter((word) => word.length > 2);
|
||||
const stopwordsSet = new Set(
|
||||
this.config.stopwords.map((w) => w.toLowerCase()),
|
||||
);
|
||||
const tokens = words.filter(
|
||||
(word) =>
|
||||
word.length >= this.config.minWordLength && !stopwordsSet.has(word),
|
||||
);
|
||||
return Array.from(new Set(tokens));
|
||||
}
|
||||
|
||||
|
|
@ -343,7 +380,6 @@ class SearchEngine {
|
|||
}
|
||||
|
||||
if (!this.isLoaded || this.documents.length === 0) {
|
||||
console.log("Search data not available");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -357,12 +393,23 @@ class SearchEngine {
|
|||
|
||||
const useFuzzySearch = rawQuery.length >= 3;
|
||||
|
||||
const candidateDocIds = new Set();
|
||||
searchTerms.forEach((term) => {
|
||||
if (this.tokenMap.has(term)) {
|
||||
const docIds = this.tokenMap.get(term);
|
||||
docIds.forEach((docId) => candidateDocIds.add(docId));
|
||||
}
|
||||
});
|
||||
|
||||
if (candidateDocIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pageMatches = new Map();
|
||||
const totalDocs = this.documents.length;
|
||||
let lastCheckTime = Date.now();
|
||||
const CHECK_INTERVAL = 16; // Check every ~16ms (one frame)
|
||||
|
||||
for (let docIdx = 0; docIdx < totalDocs; docIdx++) {
|
||||
for (const docIdx of candidateDocIds) {
|
||||
// Check for abort periodically
|
||||
if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
|
||||
if (options.signal?.aborted) {
|
||||
|
|
@ -384,33 +431,37 @@ class SearchEngine {
|
|||
pageMatches.set(docIdx, match);
|
||||
}
|
||||
|
||||
const lowerTitle = (
|
||||
typeof doc.title === "string" ? doc.title : ""
|
||||
).toLowerCase();
|
||||
const lowerContent = (
|
||||
typeof doc.content === "string" ? doc.content : ""
|
||||
).toLowerCase();
|
||||
const cached = this.lowercaseCache?.[docIdx];
|
||||
const lowerTitle =
|
||||
cached?.title ??
|
||||
(typeof doc.title === "string" ? doc.title : "").toLowerCase();
|
||||
const lowerContent =
|
||||
cached?.content ??
|
||||
(typeof doc.content === "string" ? doc.content : "").toLowerCase();
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyTitleScore = this.fuzzyMatch(rawQuery, lowerTitle);
|
||||
|
||||
if (fuzzyTitleScore !== null) {
|
||||
match.pageScore += fuzzyTitleScore * 100;
|
||||
match.pageScore += fuzzyTitleScore * this.config.boostTitle;
|
||||
}
|
||||
|
||||
const fuzzyContentScore = this.fuzzyMatch(rawQuery, lowerContent);
|
||||
|
||||
if (fuzzyContentScore !== null) {
|
||||
match.pageScore += fuzzyContentScore * 30;
|
||||
match.pageScore += fuzzyContentScore * this.config.boostContent;
|
||||
}
|
||||
}
|
||||
|
||||
searchTerms.forEach((term) => {
|
||||
if (lowerTitle.includes(term)) {
|
||||
match.pageScore += lowerTitle === term ? 20 : 10;
|
||||
match.pageScore +=
|
||||
lowerTitle === term
|
||||
? this.config.boostTitle / 5
|
||||
: this.config.boostTitle / 10;
|
||||
}
|
||||
if (lowerContent.includes(term)) {
|
||||
match.pageScore += 2;
|
||||
match.pageScore += this.config.boostContent / 15;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -629,9 +680,9 @@ class SearchEngine {
|
|||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = `search_${Date.now()}_${
|
||||
Math.random().toString(36).substring(2, 11)
|
||||
}`;
|
||||
const messageId = `search_${Date.now()}_${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 11)}`;
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error("Web Worker search timeout"));
|
||||
|
|
@ -664,14 +715,12 @@ class SearchEngine {
|
|||
worker.addEventListener("message", handleMessage);
|
||||
worker.addEventListener("error", handleError);
|
||||
|
||||
worker.postMessage(
|
||||
{
|
||||
messageId,
|
||||
type: "search",
|
||||
data: { query, limit },
|
||||
documents: this.documents,
|
||||
},
|
||||
);
|
||||
worker.postMessage({
|
||||
messageId,
|
||||
type: "search",
|
||||
data: { query, limit },
|
||||
documents: this.documents,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -681,7 +730,7 @@ class SearchEngine {
|
|||
return text
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[.,!?;:'"…—–-]+$/g, "")
|
||||
.replace(SearchEngine.STRIP_TRAILING_CHARS_RE, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
|
@ -829,13 +878,13 @@ class SearchEngine {
|
|||
let pageScore = 0;
|
||||
|
||||
if (titleMatch !== -1) {
|
||||
pageScore += 10;
|
||||
pageScore += this.config.boostTitle / 10;
|
||||
if (doc.title.toLowerCase() === lowerQuery) {
|
||||
pageScore += 20;
|
||||
pageScore += this.config.boostTitle / 5;
|
||||
}
|
||||
}
|
||||
if (contentMatch !== -1) {
|
||||
pageScore += 2;
|
||||
pageScore += this.config.boostContent / 15;
|
||||
}
|
||||
|
||||
// Find matching anchors
|
||||
|
|
@ -872,6 +921,105 @@ class SearchEngine {
|
|||
// Create Web Worker if supported - initialized lazily to use rootPath
|
||||
let searchWorker = null;
|
||||
|
||||
// Keyboard navigation helper class
|
||||
class SearchKeyboardNav {
|
||||
constructor(container, selector) {
|
||||
this.container = container;
|
||||
this.selector = selector;
|
||||
this.activeIndex = -1;
|
||||
this.items = [];
|
||||
this.navigationPending = false;
|
||||
}
|
||||
|
||||
updateItems() {
|
||||
this.items = Array.from(this.container.querySelectorAll(this.selector));
|
||||
if (this.activeIndex >= this.items.length) {
|
||||
this.activeIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setActive(-1);
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
setActive(index) {
|
||||
// Remove active class from previous item
|
||||
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
|
||||
this.items[this.activeIndex].classList.remove("search-result-active");
|
||||
}
|
||||
|
||||
this.activeIndex = index;
|
||||
|
||||
// Add active class to new item
|
||||
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
|
||||
this.items[this.activeIndex].classList.add("search-result-active");
|
||||
this.items[this.activeIndex].scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
moveDown() {
|
||||
if (this.items.length === 0) return;
|
||||
const newIndex = Math.min(this.activeIndex + 1, this.items.length - 1);
|
||||
this.setActive(newIndex);
|
||||
}
|
||||
|
||||
moveUp() {
|
||||
if (this.items.length === 0) return;
|
||||
const newIndex = Math.max(this.activeIndex - 1, -1);
|
||||
this.setActive(newIndex);
|
||||
}
|
||||
|
||||
moveToFirst() {
|
||||
if (this.items.length === 0) return;
|
||||
this.setActive(0);
|
||||
}
|
||||
|
||||
moveToLast() {
|
||||
if (this.items.length === 0) return;
|
||||
this.setActive(this.items.length - 1);
|
||||
}
|
||||
|
||||
select() {
|
||||
// Guard against double-navigation
|
||||
if (this.navigationPending) return false;
|
||||
|
||||
if (this.activeIndex >= 0 && this.activeIndex < this.items.length) {
|
||||
const link = this.items[this.activeIndex].querySelector("a");
|
||||
if (link) {
|
||||
this.navigationPending = true;
|
||||
|
||||
// Add search query to URL if it's a result link
|
||||
const currentQuery =
|
||||
this.container.closest(".search-container")?.querySelector("input")
|
||||
?.value || document.getElementById("search-page-input")?.value;
|
||||
if (currentQuery) {
|
||||
const url = new URL(link.href, window.location.origin);
|
||||
url.searchParams.set("highlight", currentQuery);
|
||||
|
||||
// Clear flag after navigation starts
|
||||
setTimeout(() => {
|
||||
this.navigationPending = false;
|
||||
}, 100);
|
||||
|
||||
window.location.href = url.toString();
|
||||
} else {
|
||||
// Clear flag before click to allow navigation
|
||||
setTimeout(() => {
|
||||
this.navigationPending = false;
|
||||
}, 100);
|
||||
link.click();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout = null;
|
||||
return function (...args) {
|
||||
|
|
@ -891,7 +1039,6 @@ function initializeSearchWorker() {
|
|||
? `${rootPath}assets/search-worker.js`
|
||||
: "/assets/search-worker.js";
|
||||
searchWorker = new Worker(workerPath);
|
||||
console.log("Web Worker initialized for background search");
|
||||
return searchWorker;
|
||||
} catch (error) {
|
||||
console.warn("Web Worker creation failed, using main thread:", error);
|
||||
|
|
@ -913,9 +1060,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
// Initialize search engine immediately
|
||||
window.searchNamespace.engine
|
||||
.loadData()
|
||||
.then(() => {
|
||||
console.log("Search data loaded successfully");
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((error) => {
|
||||
console.error("Failed to initialize search:", error);
|
||||
});
|
||||
|
|
@ -923,13 +1068,53 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
// Search page specific functionality
|
||||
const searchPageInput = document.getElementById("search-page-input");
|
||||
if (searchPageInput) {
|
||||
// Initialize keyboard navigation for search page
|
||||
const searchPageResults = document.getElementById("search-page-results");
|
||||
const searchPageKeyboardNav = new SearchKeyboardNav(
|
||||
searchPageResults,
|
||||
".search-result-item",
|
||||
);
|
||||
|
||||
// Keyboard navigation for search page
|
||||
searchPageInput.addEventListener("keydown", function (event) {
|
||||
const hasResults =
|
||||
searchPageResults &&
|
||||
searchPageResults.querySelector(".search-result-item");
|
||||
|
||||
if (!hasResults) return;
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
searchPageKeyboardNav.moveDown();
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
searchPageKeyboardNav.moveUp();
|
||||
} else if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
searchPageKeyboardNav.moveToFirst();
|
||||
} else if (event.key === "End") {
|
||||
event.preventDefault();
|
||||
searchPageKeyboardNav.moveToLast();
|
||||
} else if (
|
||||
event.key === "Enter" &&
|
||||
searchPageKeyboardNav.activeIndex >= 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
searchPageKeyboardNav.select();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
searchPageKeyboardNav.clear();
|
||||
searchPageInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
// Set up event listener with debouncing
|
||||
searchPageInput.addEventListener(
|
||||
"input",
|
||||
debounce(function () {
|
||||
const query = this.value.trim();
|
||||
if (query.length >= 2) {
|
||||
performSearch(query);
|
||||
performSearch(query, searchPageKeyboardNav);
|
||||
} else {
|
||||
const resultsContainer = document.getElementById(
|
||||
"search-page-results",
|
||||
|
|
@ -938,6 +1123,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
resultsContainer.innerHTML =
|
||||
"<p>Please enter at least 2 characters to search</p>";
|
||||
}
|
||||
searchPageKeyboardNav.clear();
|
||||
}
|
||||
}, 200),
|
||||
);
|
||||
|
|
@ -947,7 +1133,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
const query = params.get("q");
|
||||
if (query) {
|
||||
searchPageInput.value = query;
|
||||
performSearch(query);
|
||||
performSearch(query, searchPageKeyboardNav);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -956,6 +1142,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
if (searchInput) {
|
||||
const searchResults = document.getElementById("search-results");
|
||||
const searchContainer = searchInput.closest(".search-container");
|
||||
// Initialize keyboard navigation for desktop search
|
||||
const desktopKeyboardNav = new SearchKeyboardNav(
|
||||
searchResults,
|
||||
".search-result-item",
|
||||
);
|
||||
|
||||
searchInput.addEventListener(
|
||||
"input",
|
||||
|
|
@ -967,6 +1158,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
searchResults.innerHTML = "";
|
||||
searchResults.style.display = "none";
|
||||
if (searchContainer) searchContainer.classList.remove("has-results");
|
||||
desktopKeyboardNav.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -987,11 +1179,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
searchResults.innerHTML = results
|
||||
.map((result) => {
|
||||
const { doc, matchingAnchors } = result;
|
||||
const queryTerms = window.searchNamespace.engine.tokenize(
|
||||
searchTerm,
|
||||
);
|
||||
const highlightedTitle = window.searchNamespace.engine
|
||||
.highlightTerms(
|
||||
const queryTerms =
|
||||
window.searchNamespace.engine.tokenize(searchTerm);
|
||||
const highlightedTitle =
|
||||
window.searchNamespace.engine.highlightTerms(
|
||||
doc.title,
|
||||
queryTerms,
|
||||
);
|
||||
|
|
@ -1008,20 +1199,20 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
if (matchingAnchors && matchingAnchors.length > 0) {
|
||||
matchingAnchors.forEach((anchor) => {
|
||||
// Skip anchors that duplicate the page title
|
||||
const normalizedAnchor = window.searchNamespace.engine
|
||||
.normalizeForComparison(
|
||||
const normalizedAnchor =
|
||||
window.searchNamespace.engine.normalizeForComparison(
|
||||
anchor.text,
|
||||
);
|
||||
const normalizedTitle = window.searchNamespace.engine
|
||||
.normalizeForComparison(
|
||||
const normalizedTitle =
|
||||
window.searchNamespace.engine.normalizeForComparison(
|
||||
doc.title,
|
||||
);
|
||||
if (normalizedAnchor === normalizedTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightedAnchor = window.searchNamespace.engine
|
||||
.highlightTerms(
|
||||
const highlightedAnchor =
|
||||
window.searchNamespace.engine.highlightTerms(
|
||||
anchor.text,
|
||||
queryTerms,
|
||||
);
|
||||
|
|
@ -1039,6 +1230,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
.join("");
|
||||
searchResults.style.display = "block";
|
||||
if (searchContainer) searchContainer.classList.add("has-results");
|
||||
desktopKeyboardNav.updateItems();
|
||||
} else {
|
||||
searchResults.innerHTML =
|
||||
'<div class="search-result-item">No results found</div>';
|
||||
|
|
@ -1048,7 +1240,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
} catch (error) {
|
||||
console.error("Search error:", error);
|
||||
searchResults.innerHTML =
|
||||
'<div class="search-result-item">Search unavailable</div>';
|
||||
'<div class="search-result-item search-error" role="alert">Search unavailable. <a href="#" onclick="event.preventDefault(); window.searchNamespace.engine.loadData();">Retry</a></div>';
|
||||
searchResults.style.display = "block";
|
||||
if (searchContainer) searchContainer.classList.add("has-results");
|
||||
}
|
||||
|
|
@ -1063,6 +1255,35 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
) {
|
||||
searchResults.style.display = "none";
|
||||
if (searchContainer) searchContainer.classList.remove("has-results");
|
||||
desktopKeyboardNav.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation for desktop search
|
||||
searchInput.addEventListener("keydown", function (event) {
|
||||
if (searchResults.style.display !== "block") return;
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
desktopKeyboardNav.moveDown();
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
desktopKeyboardNav.moveUp();
|
||||
} else if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
desktopKeyboardNav.moveToFirst();
|
||||
} else if (event.key === "End") {
|
||||
event.preventDefault();
|
||||
desktopKeyboardNav.moveToLast();
|
||||
} else if (event.key === "Enter" && desktopKeyboardNav.activeIndex >= 0) {
|
||||
event.preventDefault();
|
||||
desktopKeyboardNav.select();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
searchResults.style.display = "none";
|
||||
if (searchContainer) searchContainer.classList.remove("has-results");
|
||||
desktopKeyboardNav.clear();
|
||||
searchInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1072,17 +1293,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
event.preventDefault();
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
// Close search results on Escape key
|
||||
if (
|
||||
event.key === "Escape" &&
|
||||
(document.activeElement === searchInput ||
|
||||
searchResults.style.display === "block")
|
||||
) {
|
||||
searchResults.style.display = "none";
|
||||
if (searchContainer) searchContainer.classList.remove("has-results");
|
||||
searchInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
setupDocumentEventHandlers(searchInput, searchResults, searchContainer);
|
||||
|
|
@ -1094,8 +1304,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
searchContainer,
|
||||
) {
|
||||
document.addEventListener("click", function (event) {
|
||||
const isMobileSearchActive = mobileSearchPopup &&
|
||||
mobileSearchPopup.classList.contains("active");
|
||||
const isMobileSearchActive =
|
||||
mobileSearchPopup && mobileSearchPopup.classList.contains("active");
|
||||
const isDesktopResultsVisible = searchResults.style.display === "block";
|
||||
|
||||
if (
|
||||
|
|
@ -1174,6 +1384,76 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
const mobileSearchResults = document.getElementById("mobile-search-results");
|
||||
const closeMobileSearchBtn = document.getElementById("close-mobile-search");
|
||||
|
||||
// Store cleanup function to prevent memory leaks
|
||||
let mobileFocusTrapCleanup = null;
|
||||
|
||||
function setupMobileFocusTrap() {
|
||||
if (!mobileSearchPopup || !mobileSearchPopup.classList.contains("active")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusableElements = [
|
||||
mobileSearchInput,
|
||||
closeMobileSearchBtn,
|
||||
...Array.from(mobileSearchResults.querySelectorAll("a[href]")),
|
||||
].filter((el) => el !== null);
|
||||
|
||||
if (focusableElements.length === 0) return;
|
||||
|
||||
const firstFocusable = focusableElements[0];
|
||||
const lastFocusable = focusableElements[focusableElements.length - 1];
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === "Tab") {
|
||||
if (e.shiftKey && document.activeElement === firstFocusable) {
|
||||
e.preventDefault();
|
||||
lastFocusable.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
|
||||
e.preventDefault();
|
||||
firstFocusable.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "Escape") {
|
||||
closeMobileSearch();
|
||||
}
|
||||
|
||||
// Arrow key navigation in results
|
||||
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
|
||||
const links = Array.from(
|
||||
mobileSearchResults.querySelectorAll("a[href]"),
|
||||
);
|
||||
if (links.length === 0) return;
|
||||
|
||||
const currentIndex = links.indexOf(document.activeElement);
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
if (currentIndex === -1) {
|
||||
links[0].focus();
|
||||
} else {
|
||||
const nextIndex = Math.min(currentIndex + 1, links.length - 1);
|
||||
links[nextIndex].focus();
|
||||
}
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
if (currentIndex > 0) {
|
||||
links[currentIndex - 1].focus();
|
||||
} else if (currentIndex === 0) {
|
||||
mobileSearchInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mobileSearchPopup.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
mobileSearchPopup.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}
|
||||
|
||||
function openMobileSearch() {
|
||||
if (mobileSearchPopup) {
|
||||
mobileSearchPopup.classList.add("active");
|
||||
|
|
@ -1182,12 +1462,23 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
if (mobileSearchInput) {
|
||||
mobileSearchInput.focus();
|
||||
}
|
||||
// Clean up previous session's listeners before setting up new ones
|
||||
if (mobileFocusTrapCleanup) {
|
||||
mobileFocusTrapCleanup();
|
||||
mobileFocusTrapCleanup = null;
|
||||
}
|
||||
mobileFocusTrapCleanup = setupMobileFocusTrap();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function closeMobileSearch() {
|
||||
if (mobileSearchPopup) {
|
||||
// Clean up event listeners before closing
|
||||
if (mobileFocusTrapCleanup) {
|
||||
mobileFocusTrapCleanup();
|
||||
mobileFocusTrapCleanup = null;
|
||||
}
|
||||
mobileSearchPopup.classList.remove("active");
|
||||
if (mobileSearchInput) {
|
||||
mobileSearchInput.value = "";
|
||||
|
|
@ -1235,11 +1526,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
mobileSearchResults.innerHTML = results
|
||||
.map((result) => {
|
||||
const { doc, matchingAnchors } = result;
|
||||
const queryTerms = window.searchNamespace.engine.tokenize(
|
||||
searchTerm,
|
||||
);
|
||||
const highlightedTitle = window.searchNamespace.engine
|
||||
.highlightTerms(
|
||||
const queryTerms =
|
||||
window.searchNamespace.engine.tokenize(searchTerm);
|
||||
const highlightedTitle =
|
||||
window.searchNamespace.engine.highlightTerms(
|
||||
doc.title,
|
||||
queryTerms,
|
||||
);
|
||||
|
|
@ -1258,25 +1548,25 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
if (matchingAnchors && matchingAnchors.length > 0) {
|
||||
matchingAnchors.forEach((anchor) => {
|
||||
// Skip anchors that duplicate the page title
|
||||
const normalizedAnchor = window.searchNamespace.engine
|
||||
.normalizeForComparison(
|
||||
const normalizedAnchor =
|
||||
window.searchNamespace.engine.normalizeForComparison(
|
||||
anchor.text,
|
||||
);
|
||||
const normalizedTitle = window.searchNamespace.engine
|
||||
.normalizeForComparison(
|
||||
const normalizedTitle =
|
||||
window.searchNamespace.engine.normalizeForComparison(
|
||||
doc.title,
|
||||
);
|
||||
if (normalizedAnchor === normalizedTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightedAnchor = window.searchNamespace.engine
|
||||
.highlightTerms(
|
||||
const highlightedAnchor =
|
||||
window.searchNamespace.engine.highlightTerms(
|
||||
anchor.text,
|
||||
queryTerms,
|
||||
);
|
||||
const sectionPreview = window.searchNamespace.engine
|
||||
.generateSectionPreview(
|
||||
const sectionPreview =
|
||||
window.searchNamespace.engine.generateSectionPreview(
|
||||
doc,
|
||||
anchor,
|
||||
searchTerm,
|
||||
|
|
@ -1298,6 +1588,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
})
|
||||
.join("");
|
||||
mobileSearchResults.style.display = "block";
|
||||
// Clean up previous listeners before setting up new ones
|
||||
if (mobileFocusTrapCleanup) {
|
||||
mobileFocusTrapCleanup();
|
||||
mobileFocusTrapCleanup = null;
|
||||
}
|
||||
mobileFocusTrapCleanup = setupMobileFocusTrap();
|
||||
} else {
|
||||
mobileSearchResults.innerHTML =
|
||||
'<div class="search-result-item">No results found</div>';
|
||||
|
|
@ -1308,7 +1604,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
// Verify once more
|
||||
if (mobileSearchInput.value.trim() !== searchTerm) return;
|
||||
mobileSearchResults.innerHTML =
|
||||
'<div class="search-result-item">Search unavailable</div>';
|
||||
'<div class="search-result-item search-error" role="alert">Search unavailable. <a href="#" onclick="event.preventDefault(); window.searchNamespace.engine.loadData();">Retry</a></div>';
|
||||
mobileSearchResults.style.display = "block";
|
||||
}
|
||||
}, 300);
|
||||
|
|
@ -1330,13 +1626,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
});
|
||||
});
|
||||
|
||||
async function performSearch(query) {
|
||||
async function performSearch(query, keyboardNav = null) {
|
||||
query = query.trim();
|
||||
const resultsContainer = document.getElementById("search-page-results");
|
||||
|
||||
if (query.length < 2) {
|
||||
resultsContainer.innerHTML =
|
||||
"<p>Please enter at least 2 characters to search</p>";
|
||||
if (keyboardNav) keyboardNav.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1348,6 +1645,7 @@ async function performSearch(query) {
|
|||
|
||||
// Show loading state
|
||||
resultsContainer.innerHTML = "<p>Searching...</p>";
|
||||
if (keyboardNav) keyboardNav.clear();
|
||||
|
||||
try {
|
||||
const results = await window.searchNamespace.engine.search(query, 50, {
|
||||
|
|
@ -1390,21 +1688,21 @@ async function performSearch(query) {
|
|||
if (matchingAnchors && matchingAnchors.length > 0) {
|
||||
matchingAnchors.forEach((anchor) => {
|
||||
// Skip anchors that have the same text as the page title to avoid duplication
|
||||
const normalizedAnchor = window.searchNamespace.engine
|
||||
.normalizeForComparison(anchor.text);
|
||||
const normalizedTitle = window.searchNamespace.engine
|
||||
.normalizeForComparison(doc.title);
|
||||
const normalizedAnchor =
|
||||
window.searchNamespace.engine.normalizeForComparison(anchor.text);
|
||||
const normalizedTitle =
|
||||
window.searchNamespace.engine.normalizeForComparison(doc.title);
|
||||
if (normalizedAnchor === normalizedTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightedAnchor = window.searchNamespace.engine
|
||||
.highlightTerms(
|
||||
const highlightedAnchor =
|
||||
window.searchNamespace.engine.highlightTerms(
|
||||
anchor.text,
|
||||
queryTerms,
|
||||
);
|
||||
const sectionPreview = window.searchNamespace.engine
|
||||
.generateSectionPreview(
|
||||
const sectionPreview =
|
||||
window.searchNamespace.engine.generateSectionPreview(
|
||||
doc,
|
||||
anchor,
|
||||
query,
|
||||
|
|
@ -1421,8 +1719,10 @@ async function performSearch(query) {
|
|||
}
|
||||
html += "</ul>";
|
||||
resultsContainer.innerHTML = html;
|
||||
if (keyboardNav) keyboardNav.updateItems();
|
||||
} else {
|
||||
resultsContainer.innerHTML = "<p>No results found</p>";
|
||||
if (keyboardNav) keyboardNav.clear();
|
||||
}
|
||||
|
||||
// Update URL with query
|
||||
|
|
@ -1434,6 +1734,14 @@ async function performSearch(query) {
|
|||
return;
|
||||
}
|
||||
console.error("Search error:", error);
|
||||
resultsContainer.innerHTML = "<p>Search temporarily unavailable</p>";
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="search-error" role="alert">
|
||||
<p>Search is temporarily unavailable. Please try again.</p>
|
||||
<button type="button" onclick="window.searchNamespace.engine.loadData().then(() => { this.closest('.search-error').innerHTML = '<p>Search reloaded. Please try your search again.</p>'; })">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
if (keyboardNav) keyboardNav.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
428
assets/style.css
428
assets/style.css
|
|
@ -113,6 +113,9 @@
|
|||
0 10px 15px -3px var(--shadow-color), 0 4px 6px -4px var(--shadow-color);
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition: 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* Fonts */
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
|
@ -631,7 +634,7 @@ a:hover {
|
|||
|
||||
/* Code Styling */
|
||||
code {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-family: var(--font-mono);
|
||||
background-color: var(--code-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.2em 0.4em;
|
||||
|
|
@ -806,6 +809,9 @@ img {
|
|||
padding: var(--space-4);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: border-color var(--transition);
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
|
|
@ -816,12 +822,18 @@ img {
|
|||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background-color: var(--sidebar-hover);
|
||||
}
|
||||
|
||||
.search-result-item.search-result-active {
|
||||
background-color: var(--sidebar-active);
|
||||
border-left: 3px solid var(--link-color);
|
||||
}
|
||||
|
||||
.search-result-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-2);
|
||||
|
|
@ -879,7 +891,8 @@ img {
|
|||
|
||||
#mobile-search-input {
|
||||
flex-grow: 1;
|
||||
padding: 10px 15px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
min-height: 48px;
|
||||
font-size: 1.1em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
|
|
@ -899,9 +912,14 @@ img {
|
|||
font-size: 2rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
padding: 0;
|
||||
margin-left: 0.5rem;
|
||||
line-height: 1;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-mobile-search:hover {
|
||||
|
|
@ -915,7 +933,10 @@ img {
|
|||
|
||||
/* Reuse desktop search result styling */
|
||||
.mobile-search-results .search-result-item {
|
||||
padding: 10px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
|
|
@ -926,13 +947,24 @@ img {
|
|||
.mobile-search-results .search-result-item a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-2) 0;
|
||||
min-height: 44px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mobile-search-results .search-result-item:hover {
|
||||
background-color: var(--sidebar-hover);
|
||||
}
|
||||
|
||||
/* Touch feedback for mobile devices */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.mobile-search-results .search-result-item:active {
|
||||
background-color: var(--sidebar-active);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-search-results .search-result-title {
|
||||
font-weight: 600;
|
||||
color: var(--heading-color);
|
||||
|
|
@ -981,6 +1013,50 @@ img {
|
|||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-keyboard-hints {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-3);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background-color: var(--sidebar-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-keyboard-hints .hint-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.search-keyboard-hints kbd {
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
background-color: var(--code-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.search-keyboard-hints kbd {
|
||||
background-color: var(--sidebar-bg);
|
||||
border-color: var(--border-color);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.search-keyboard-hints {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-page-results {
|
||||
margin-top: var(--space-6);
|
||||
}
|
||||
|
|
@ -1658,7 +1734,7 @@ h6:hover .copy-link {
|
|||
.toc-list details summary {
|
||||
cursor: pointer;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
|
|
@ -1683,11 +1759,10 @@ h6:hover .copy-link {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.toc-list details summary::after {
|
||||
.toc-list details summary::before {
|
||||
content: "▶";
|
||||
font-size: 0.65rem;
|
||||
margin-left: auto;
|
||||
margin-right: var(--space-1);
|
||||
margin-right: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
transition:
|
||||
|
|
@ -1695,7 +1770,7 @@ h6:hover .copy-link {
|
|||
color var(--transition-fast);
|
||||
}
|
||||
|
||||
.toc-list details[open] summary::after {
|
||||
.toc-list details[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
|
@ -1713,7 +1788,7 @@ h6:hover .copy-link {
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: var(--space-2);
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
@ -1800,6 +1875,151 @@ h6:hover .copy-link {
|
|||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
/* Lib entry specific styling */
|
||||
.lib-entry {
|
||||
scroll-margin-top: 80px;
|
||||
padding: var(--space-6);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
margin: var(--space-6) 0;
|
||||
background-color: var(--sidebar-bg);
|
||||
transition:
|
||||
background-color var(--transition),
|
||||
border-color var(--transition);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.lib-entry:target,
|
||||
.lib-entry.highlight {
|
||||
border-color: var(--link-color);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||
animation: highlight-pulse 1.5s ease;
|
||||
}
|
||||
|
||||
.content .lib-entry-name {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--space-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lib-entry-anchor {
|
||||
color: var(--heading-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.lib-entry-anchor:hover {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.lib-entry-declared {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.lib-entry-type {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.lib-entry-description {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.lib-entry-arguments h3,
|
||||
.lib-entry-examples h3,
|
||||
.lib-entry-notes h3,
|
||||
.lib-entry-warnings h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin: var(--space-4) 0 var(--space-2) 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.lib-entry-arguments dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lib-entry-arguments dt {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lib-entry-arguments dd {
|
||||
margin-left: var(--space-6);
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lib-entry-examples pre {
|
||||
margin: var(--space-2) 0;
|
||||
padding: var(--space-3);
|
||||
background-color: var(--code-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.lib-entry-examples code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.lib-entry-deprecated {
|
||||
margin-top: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--admonition-warning-color) 10%,
|
||||
transparent
|
||||
);
|
||||
border-left: 3px solid var(--admonition-warning-color);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.lib-entry-deprecated p {
|
||||
margin: 0;
|
||||
color: var(--admonition-warning-color);
|
||||
}
|
||||
|
||||
.lib-entry-notes ul,
|
||||
.lib-entry-warnings ul {
|
||||
margin: 0;
|
||||
padding-left: var(--space-6);
|
||||
}
|
||||
|
||||
.lib-entry-notes li,
|
||||
.lib-entry-warnings li {
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.lib-entry-warnings {
|
||||
margin-top: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--admonition-danger-color) 10%,
|
||||
transparent
|
||||
);
|
||||
border-left: 3px solid var(--admonition-danger-color);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.lib-entry-warnings h3 {
|
||||
color: var(--admonition-danger-color);
|
||||
}
|
||||
|
||||
/* Lib container */
|
||||
.lib-container {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Filter styling */
|
||||
.search-form {
|
||||
margin: var(--space-4) 0;
|
||||
|
|
@ -1808,6 +2028,7 @@ h6:hover .copy-link {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#lib-filter,
|
||||
#options-filter {
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
|
|
@ -1820,6 +2041,7 @@ h6:hover .copy-link {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#lib-filter:focus,
|
||||
#options-filter:focus {
|
||||
outline: none;
|
||||
border-color: var(--link-color);
|
||||
|
|
@ -2005,7 +2227,8 @@ h6:hover .copy-link {
|
|||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.sidebar-section summary {
|
||||
/* Only target direct child summary (section headers like "Documents", "Contents") */
|
||||
.sidebar-section > summary {
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
|
|
@ -2023,24 +2246,24 @@ h6:hover .copy-link {
|
|||
background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-section summary:hover {
|
||||
.sidebar-section > summary:hover {
|
||||
background-color: var(--sidebar-hover);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sidebar-section summary::before {
|
||||
.sidebar-section > summary::before {
|
||||
content: "▶";
|
||||
font-size: 0.625rem;
|
||||
transition: transform var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-section[open] summary::before {
|
||||
.sidebar-section[open] > summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Hide default marker */
|
||||
.sidebar-section summary::-webkit-details-marker {
|
||||
.sidebar-section > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -2141,3 +2364,176 @@ h6:hover .copy-link {
|
|||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyboard navigation active state */
|
||||
.search-result-item.search-result-active {
|
||||
background-color: var(--sidebar-active);
|
||||
border-left: 3px solid var(--link-color);
|
||||
}
|
||||
|
||||
/* Search error styling */
|
||||
.search-error {
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--admonition-danger-color);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.search-error p {
|
||||
margin: 0 0 var(--space-3) 0;
|
||||
}
|
||||
|
||||
.search-error button,
|
||||
.search-error a {
|
||||
display: inline-block;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background-color: var(--link-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.search-error button:hover,
|
||||
.search-error a:hover {
|
||||
background-color: var(--link-hover-color);
|
||||
}
|
||||
|
||||
.search-result-item.search-result-active a {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
/* Search result highlighting on target pages */
|
||||
.search-highlight {
|
||||
background-color: rgba(255, 235, 59, 0.4);
|
||||
color: inherit;
|
||||
font-weight: inherit;
|
||||
padding: 2px 0;
|
||||
border-radius: 2px;
|
||||
animation: highlight-fade 3s ease-out forwards;
|
||||
}
|
||||
|
||||
.search-highlight-active {
|
||||
background-color: rgba(255, 193, 7, 0.6);
|
||||
animation: highlight-pulse-active 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes highlight-fade {
|
||||
0% {
|
||||
background-color: rgba(255, 235, 59, 0.6);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(255, 235, 59, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes highlight-pulse-active {
|
||||
0% {
|
||||
background-color: rgba(255, 193, 7, 0.5);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(255, 193, 7, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.search-highlight {
|
||||
background-color: rgba(255, 193, 7, 0.3);
|
||||
animation: highlight-fade-dark 3s ease-out forwards;
|
||||
}
|
||||
|
||||
.search-highlight-active {
|
||||
background-color: rgba(255, 152, 0, 0.5);
|
||||
animation: highlight-pulse-active-dark 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes highlight-fade-dark {
|
||||
0% {
|
||||
background-color: rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(255, 193, 7, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes highlight-pulse-active-dark {
|
||||
0% {
|
||||
background-color: rgba(255, 152, 0, 0.4);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(255, 152, 0, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar directory groups */
|
||||
.sidebar-dir-group {
|
||||
list-style: none;
|
||||
margin-bottom: var(--space-1);
|
||||
|
||||
> details {
|
||||
> summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
list-style: none;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "▶";
|
||||
font-size: 0.625rem;
|
||||
transition: transform var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--sidebar-hover);
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&[open] > summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
> ul {
|
||||
margin: var(--space-1) 0 var(--space-1) var(--space-4);
|
||||
padding-left: var(--space-2);
|
||||
border-left: 2px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-dir-count {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
background-color: var(--sidebar-active);
|
||||
color: var(--text-muted);
|
||||
border-radius: 9999px;
|
||||
padding: 0 var(--space-2);
|
||||
line-height: 1.6;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sidebar-dir-label {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue