mirror of
https://github.com/NotAShelf/nvf.git
synced 2026-02-04 10:55:50 +00:00
deploy: ab629c1995
This commit is contained in:
parent
c8e237c3a8
commit
3435dce176
13 changed files with 4554 additions and 5571 deletions
160
assets/main.js
160
assets/main.js
|
|
@ -1,8 +1,8 @@
|
|||
// Polyfill for requestIdleCallback for Safari and unsupported browsers
|
||||
if (typeof window.requestIdleCallback === "undefined") {
|
||||
window.requestIdleCallback = function (cb) {
|
||||
var start = Date.now();
|
||||
var idlePeriod = 50;
|
||||
const start = Date.now();
|
||||
const idlePeriod = 50;
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
|
|
@ -85,6 +85,127 @@ 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",
|
||||
);
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionId = section.dataset.section;
|
||||
if (!sectionId) return;
|
||||
|
||||
const storageKey = `sidebar-section-${sectionId}`;
|
||||
const savedState = localStorage.getItem(storageKey);
|
||||
|
||||
// Restore saved state (default is open)
|
||||
if (savedState === "closed") {
|
||||
section.removeAttribute("open");
|
||||
}
|
||||
|
||||
// Save state on toggle and sync between desktop/mobile
|
||||
section.addEventListener("toggle", () => {
|
||||
localStorage.setItem(storageKey, section.open ? "open" : "closed");
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize scroll spy
|
||||
function initScrollSpy() {
|
||||
const pageToc = document.querySelector(".page-toc");
|
||||
if (!pageToc) return;
|
||||
|
||||
const tocLinks = pageToc.querySelectorAll(".page-toc-list a");
|
||||
const content = document.querySelector(".content");
|
||||
if (!tocLinks.length || !content) return;
|
||||
|
||||
const headings = Array.from(
|
||||
content.querySelectorAll("h1[id], h2[id], h3[id]"),
|
||||
);
|
||||
|
||||
if (!headings.length) return;
|
||||
|
||||
// Build a map of heading IDs to TOC links for quick lookup
|
||||
const linkMap = new Map();
|
||||
tocLinks.forEach((link) => {
|
||||
const href = link.getAttribute("href");
|
||||
if (href && href.startsWith("#")) {
|
||||
linkMap.set(href.slice(1), link);
|
||||
}
|
||||
});
|
||||
|
||||
let activeLink = null;
|
||||
|
||||
// Update active link based on scroll position
|
||||
function updateActiveLink() {
|
||||
const threshold = 120; // threshold from the top of the viewport
|
||||
|
||||
let currentHeading = null;
|
||||
|
||||
// Find the last heading that is at or above the threshold
|
||||
for (const heading of headings) {
|
||||
const rect = heading.getBoundingClientRect();
|
||||
if (rect.top <= threshold) {
|
||||
currentHeading = heading;
|
||||
}
|
||||
}
|
||||
|
||||
// If no heading is above threshold, use first heading if it's in view
|
||||
if (!currentHeading && headings.length > 0) {
|
||||
const firstRect = headings[0].getBoundingClientRect();
|
||||
if (firstRect.top < window.innerHeight) {
|
||||
currentHeading = headings[0];
|
||||
}
|
||||
}
|
||||
|
||||
const newLink = currentHeading ? linkMap.get(currentHeading.id) : null;
|
||||
|
||||
if (newLink !== activeLink) {
|
||||
if (activeLink) {
|
||||
activeLink.classList.remove("active");
|
||||
}
|
||||
if (newLink) {
|
||||
newLink.classList.add("active");
|
||||
}
|
||||
activeLink = newLink;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll event handler
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
updateActiveLink();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
// Also update on hash change (direct link navigation)
|
||||
window.addEventListener("hashchange", () => {
|
||||
requestAnimationFrame(updateActiveLink);
|
||||
});
|
||||
|
||||
// Set initial active state after a small delay to ensure
|
||||
// browser has completed any hash-based scrolling
|
||||
setTimeout(updateActiveLink, 100);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Apply sidebar state immediately before DOM rendering
|
||||
if (localStorage.getItem("sidebar-collapsed") === "true") {
|
||||
|
|
@ -96,6 +217,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
createMobileElements();
|
||||
}
|
||||
|
||||
// Initialize collapsible sidebar sections
|
||||
// after mobile elements are created
|
||||
initCollapsibleSections();
|
||||
|
||||
// Initialize scroll spy for page TOC
|
||||
initScrollSpy();
|
||||
|
||||
// Desktop Sidebar Toggle
|
||||
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
||||
|
||||
|
|
@ -111,8 +239,9 @@ 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");
|
||||
const isCollapsed = document.documentElement.classList.contains(
|
||||
"sidebar-collapsed",
|
||||
);
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed);
|
||||
});
|
||||
}
|
||||
|
|
@ -144,7 +273,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
}
|
||||
|
||||
// Make the entire heading clickable
|
||||
heading.addEventListener("click", function (e) {
|
||||
heading.addEventListener("click", function () {
|
||||
const id = this.id;
|
||||
history.pushState(null, null, "#" + id);
|
||||
|
||||
|
|
@ -257,19 +386,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
".mobile-sidebar-container",
|
||||
);
|
||||
const mobileSidebarFab = document.querySelector(".mobile-sidebar-fab");
|
||||
const mobileSidebarContent = document.querySelector(
|
||||
".mobile-sidebar-content",
|
||||
);
|
||||
const mobileSidebarHandle = document.querySelector(".mobile-sidebar-handle");
|
||||
const desktopSidebar = document.querySelector(".sidebar");
|
||||
|
||||
// Always set up FAB if it exists
|
||||
if (mobileSidebarFab && mobileSidebarContainer) {
|
||||
// Populate content if desktop sidebar exists
|
||||
if (desktopSidebar && mobileSidebarContent) {
|
||||
mobileSidebarContent.innerHTML = desktopSidebar.innerHTML;
|
||||
}
|
||||
|
||||
const openMobileSidebar = () => {
|
||||
mobileSidebarContainer.classList.add("active");
|
||||
mobileSidebarFab.setAttribute("aria-expanded", "true");
|
||||
|
|
@ -379,8 +499,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Options filter functionality
|
||||
const optionsFilter = document.getElementById("options-filter");
|
||||
if (optionsFilter) {
|
||||
|
|
@ -404,8 +522,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"));
|
||||
|
|
@ -483,7 +601,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
// 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";
|
||||
|
|
@ -530,8 +649,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
isDescMatch = !isTitleMatch && data.description.includes(term);
|
||||
} else {
|
||||
isTitleMatch = searchTerms.every((term) => data.name.includes(term));
|
||||
isDescMatch =
|
||||
!isTitleMatch &&
|
||||
isDescMatch = !isTitleMatch &&
|
||||
searchTerms.every((term) => data.description.includes(term));
|
||||
}
|
||||
if (isTitleMatch) {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,151 @@
|
|||
self.onmessage = function(e) {
|
||||
const isWordBoundary = (char) =>
|
||||
/[A-Z]/.test(char) || /[-_\/.]/.test(char) || /\s/.test(char);
|
||||
|
||||
const isCaseTransition = (prev, curr) => {
|
||||
const prevIsUpper = prev.toLowerCase() !== prev;
|
||||
const currIsUpper = curr.toLowerCase() !== curr;
|
||||
return (
|
||||
prevIsUpper && currIsUpper && prev.toLowerCase() !== curr.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
const findBestSubsequenceMatch = (query, target) => {
|
||||
const n = query.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0 || m === 0) return null;
|
||||
|
||||
const positions = [];
|
||||
|
||||
const memo = new Map();
|
||||
const key = (qIdx, tIdx, gap) => `${qIdx}:${tIdx}:${gap}`;
|
||||
|
||||
const findBest = (qIdx, tIdx, currentGap) => {
|
||||
if (qIdx === n) {
|
||||
return { done: true, positions: [...positions], gap: currentGap };
|
||||
}
|
||||
|
||||
const memoKey = key(qIdx, tIdx, currentGap);
|
||||
if (memo.has(memoKey)) {
|
||||
return memo.get(memoKey);
|
||||
}
|
||||
|
||||
let bestResult = null;
|
||||
|
||||
for (let i = tIdx; i < m; i++) {
|
||||
if (target[i] === query[qIdx]) {
|
||||
positions.push(i);
|
||||
const gap = qIdx === 0 ? 0 : i - positions[positions.length - 2] - 1;
|
||||
const newGap = currentGap + gap;
|
||||
|
||||
if (newGap > m) {
|
||||
positions.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = findBest(qIdx + 1, i + 1, newGap);
|
||||
positions.pop();
|
||||
|
||||
if (result && (!bestResult || result.gap < bestResult.gap)) {
|
||||
bestResult = result;
|
||||
if (result.gap === 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memo.set(memoKey, bestResult);
|
||||
return bestResult;
|
||||
};
|
||||
|
||||
const result = findBest(0, 0, 0);
|
||||
if (!result) return null;
|
||||
|
||||
const consecutive = (() => {
|
||||
let c = 1;
|
||||
for (let i = 1; i < result.positions.length; i++) {
|
||||
if (result.positions[i] === result.positions[i - 1] + 1) {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
})();
|
||||
|
||||
return {
|
||||
positions: result.positions,
|
||||
consecutive,
|
||||
score: calculateMatchScore(query, target, result.positions, consecutive),
|
||||
};
|
||||
};
|
||||
|
||||
const calculateMatchScore = (query, target, positions, consecutive) => {
|
||||
const n = positions.length;
|
||||
const m = target.length;
|
||||
|
||||
if (n === 0) return 0;
|
||||
|
||||
let score = 1.0;
|
||||
|
||||
const startBonus = (m - positions[0]) / m;
|
||||
score += startBonus * 0.5;
|
||||
|
||||
let gapPenalty = 0;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const gap = positions[i] - positions[i - 1] - 1;
|
||||
if (gap > 0) {
|
||||
gapPenalty += Math.min(gap / m, 1.0) * 0.3;
|
||||
}
|
||||
}
|
||||
score -= gapPenalty;
|
||||
|
||||
const consecutiveBonus = consecutive / n;
|
||||
score += consecutiveBonus * 0.3;
|
||||
|
||||
let boundaryBonus = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const char = target[positions[i]];
|
||||
if (i === 0 || isWordBoundary(char)) {
|
||||
boundaryBonus += 0.05;
|
||||
}
|
||||
if (i > 0) {
|
||||
const prevChar = target[positions[i - 1]];
|
||||
if (isCaseTransition(prevChar, char)) {
|
||||
boundaryBonus += 0.03;
|
||||
}
|
||||
}
|
||||
}
|
||||
score = Math.min(1.0, score + boundaryBonus);
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
const fuzzyMatch = (query, target) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const lowerTarget = target.toLowerCase();
|
||||
|
||||
if (lowerQuery.length === 0) return null;
|
||||
if (lowerTarget.length === 0) return null;
|
||||
|
||||
if (lowerTarget === lowerQuery) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
if (lowerTarget.includes(lowerQuery)) {
|
||||
const ratio = lowerQuery.length / lowerTarget.length;
|
||||
return 0.8 + ratio * 0.2;
|
||||
}
|
||||
|
||||
const match = findBestSubsequenceMatch(lowerQuery, lowerTarget);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.min(1.0, match.score);
|
||||
};
|
||||
|
||||
self.onmessage = function (e) {
|
||||
const { messageId, type, data } = e.data;
|
||||
|
||||
const respond = (type, data) => {
|
||||
|
|
@ -6,59 +153,146 @@ self.onmessage = function(e) {
|
|||
};
|
||||
|
||||
const respondError = (error) => {
|
||||
self.postMessage({ messageId, type: 'error', error: error.message || String(error) });
|
||||
self.postMessage({
|
||||
messageId,
|
||||
type: "error",
|
||||
error: error.message || String(error),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if (type === 'tokenize') {
|
||||
const tokens = (typeof data === 'string' ? data : '')
|
||||
.toLowerCase()
|
||||
.match(/\b[a-zA-Z0-9_-]+\b/g) || []
|
||||
.filter(word => word.length > 2);
|
||||
|
||||
if (type === "tokenize") {
|
||||
const text = typeof data === "string" ? data : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const tokens = words.filter((word) => word.length > 2);
|
||||
const uniqueTokens = Array.from(new Set(tokens));
|
||||
respond('tokens', uniqueTokens);
|
||||
}
|
||||
respond("tokens", uniqueTokens);
|
||||
} else if (type === "search") {
|
||||
const { query, limit = 10 } = data;
|
||||
|
||||
if (type === 'search') {
|
||||
const { documents, query, limit } = data;
|
||||
const searchTerms = (typeof query === 'string' ? query : '')
|
||||
.toLowerCase()
|
||||
.match(/\b[a-zA-Z0-9_-]+\b/g) || []
|
||||
.filter(word => word.length > 2);
|
||||
if (!query || typeof query !== "string") {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const docScores = new Map();
|
||||
|
||||
// Pre-compute lower-case terms once
|
||||
const lowerSearchTerms = searchTerms.map(term => term.toLowerCase());
|
||||
const rawQuery = query.toLowerCase();
|
||||
const text = typeof query === "string" ? query : "";
|
||||
const words = text.toLowerCase().match(/\b[a-zA-Z0-9_-]+\b/g) || [];
|
||||
const searchTerms = words.filter((word) => word.length > 2);
|
||||
|
||||
let documents = [];
|
||||
if (typeof data.documents === "string") {
|
||||
documents = JSON.parse(data.documents);
|
||||
} else if (Array.isArray(data.documents)) {
|
||||
documents = data.documents;
|
||||
} else if (typeof data.transferables === "string") {
|
||||
documents = JSON.parse(data.transferables);
|
||||
}
|
||||
|
||||
if (!Array.isArray(documents) || documents.length === 0) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const useFuzzySearch = rawQuery.length >= 3;
|
||||
|
||||
if (searchTerms.length === 0 && rawQuery.length < 3) {
|
||||
respond("results", []);
|
||||
return;
|
||||
}
|
||||
|
||||
const pageMatches = new Map();
|
||||
|
||||
// Pre-compute lower-case strings for each document
|
||||
const processedDocs = documents.map((doc, docId) => ({
|
||||
docId,
|
||||
title: doc.title,
|
||||
content: doc.content,
|
||||
lowerTitle: doc.title.toLowerCase(),
|
||||
lowerContent: doc.content.toLowerCase()
|
||||
}));
|
||||
const processedDocs = documents.map((doc, docId) => {
|
||||
const title = typeof doc.title === "string" ? doc.title : "";
|
||||
const content = typeof doc.content === "string" ? doc.content : "";
|
||||
|
||||
lowerSearchTerms.forEach(lowerTerm => {
|
||||
processedDocs.forEach(({ docId, title, content, lowerTitle, lowerContent }) => {
|
||||
if (lowerTitle.includes(lowerTerm) || lowerContent.includes(lowerTerm)) {
|
||||
const score = lowerTitle === lowerTerm ? 30 :
|
||||
lowerTitle.includes(lowerTerm) ? 10 : 2;
|
||||
docScores.set(docId, (docScores.get(docId) || 0) + score);
|
||||
return {
|
||||
docId,
|
||||
doc,
|
||||
lowerTitle: title.toLowerCase(),
|
||||
lowerContent: content.toLowerCase(),
|
||||
};
|
||||
});
|
||||
|
||||
// First pass: Score pages with fuzzy matching
|
||||
processedDocs.forEach(({ docId, doc, lowerTitle, lowerContent }) => {
|
||||
let match = pageMatches.get(docId);
|
||||
if (!match) {
|
||||
match = { doc, pageScore: 0, matchingAnchors: [] };
|
||||
pageMatches.set(docId, match);
|
||||
}
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyTitleScore = fuzzyMatch(rawQuery, lowerTitle);
|
||||
if (fuzzyTitleScore !== null) {
|
||||
match.pageScore += fuzzyTitleScore * 100;
|
||||
}
|
||||
|
||||
const fuzzyContentScore = fuzzyMatch(rawQuery, lowerContent);
|
||||
if (fuzzyContentScore !== null) {
|
||||
match.pageScore += fuzzyContentScore * 30;
|
||||
}
|
||||
}
|
||||
|
||||
// Token-based exact matching
|
||||
searchTerms.forEach((term) => {
|
||||
if (lowerTitle.includes(term)) {
|
||||
match.pageScore += lowerTitle === term ? 20 : 10;
|
||||
}
|
||||
if (lowerContent.includes(term)) {
|
||||
match.pageScore += 2;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = Array.from(docScores.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, limit)
|
||||
.map(([docId, score]) => ({ ...documents[docId], score }));
|
||||
// Second pass: Find matching anchors
|
||||
pageMatches.forEach((match) => {
|
||||
const doc = match.doc;
|
||||
if (
|
||||
!doc.anchors ||
|
||||
!Array.isArray(doc.anchors) ||
|
||||
doc.anchors.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
respond('results', results);
|
||||
doc.anchors.forEach((anchor) => {
|
||||
if (!anchor || !anchor.text) return;
|
||||
|
||||
const anchorText = anchor.text.toLowerCase();
|
||||
let anchorMatches = false;
|
||||
|
||||
if (useFuzzySearch) {
|
||||
const fuzzyScore = fuzzyMatch(rawQuery, anchorText);
|
||||
if (fuzzyScore !== null && fuzzyScore >= 0.4) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anchorMatches) {
|
||||
searchTerms.forEach((term) => {
|
||||
if (anchorText.includes(term)) {
|
||||
anchorMatches = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (anchorMatches) {
|
||||
match.matchingAnchors.push(anchor);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = Array.from(pageMatches.values())
|
||||
.filter((m) => m.pageScore > 5)
|
||||
.sort((a, b) => b.pageScore - a.pageScore)
|
||||
.slice(0, limit);
|
||||
|
||||
respond("results", results);
|
||||
}
|
||||
} catch (error) {
|
||||
respondError(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
1235
assets/search.js
1235
assets/search.js
File diff suppressed because it is too large
Load diff
202
assets/style.css
202
assets/style.css
|
|
@ -203,6 +203,11 @@
|
|||
U+FFFD;
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Core Layout */
|
||||
body {
|
||||
font-family:
|
||||
|
|
@ -771,7 +776,9 @@ img {
|
|||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
#search-input:focus {
|
||||
/* Expand input when focused OR when results are visible */
|
||||
.search-container:focus-within #search-input,
|
||||
.search-container.has-results #search-input {
|
||||
outline: none;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
width: 300px;
|
||||
|
|
@ -1925,7 +1932,6 @@ h6:hover .copy-link {
|
|||
|
||||
.mobile-sidebar-container {
|
||||
display: flex;
|
||||
/* Use flex for container */
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
@ -1943,3 +1949,195 @@ h6:hover .copy-link {
|
|||
margin-bottom: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Search result styles */
|
||||
.search-result-page {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-result-anchor {
|
||||
padding-left: calc(var(--space-4) + var(--space-6));
|
||||
font-size: 0.9em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-result-anchor::before {
|
||||
content: "↳";
|
||||
position: absolute;
|
||||
left: var(--space-4);
|
||||
color: var(--text-muted);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.search-result-anchor a {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-result-anchor:hover::before {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
/* Search page specific styling */
|
||||
.search-page-results .search-result-page {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.search-page-results .search-result-anchor {
|
||||
padding-left: calc(var(--space-6) + var(--space-6));
|
||||
}
|
||||
|
||||
.search-page-results .search-result-anchor::before {
|
||||
left: var(--space-6);
|
||||
}
|
||||
|
||||
/* Mobile search anchor styling */
|
||||
.mobile-search-results .search-result-anchor {
|
||||
padding-left: calc(var(--space-4) + var(--space-4));
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.mobile-search-results .search-result-anchor::before {
|
||||
left: var(--space-3);
|
||||
}
|
||||
|
||||
/* Sidebar Sections */
|
||||
.sidebar-section {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.sidebar-section summary {
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
border-radius: var(--radius-md);
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-section summary:hover {
|
||||
background-color: var(--sidebar-hover);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sidebar-section summary::before {
|
||||
content: "▶";
|
||||
font-size: 0.625rem;
|
||||
transition: transform var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-section[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Hide default marker */
|
||||
.sidebar-section summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Animated content wrapper using CSS Grid */
|
||||
.sidebar-section-content {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
overflow: hidden;
|
||||
transition: grid-template-rows var(--transition);
|
||||
}
|
||||
|
||||
.sidebar-section[open] .sidebar-section-content {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.sidebar-section-content > * {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Right-side Page Table of Contents */
|
||||
.page-toc {
|
||||
position: fixed;
|
||||
top: calc(var(--header-height) + var(--space-4));
|
||||
max-height: calc(100vh - var(--header-height) - var(--space-8));
|
||||
width: 220px;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-4);
|
||||
font-size: 0.8125rem;
|
||||
display: none;
|
||||
|
||||
/* Position to the right of centered content
|
||||
Content is max 800px, centered. Sidebar is 300px fixed left.
|
||||
Calculate: sidebar (300px) + half remaining + half content (400px) + gap
|
||||
*/
|
||||
right: var(--space-4);
|
||||
}
|
||||
|
||||
.page-toc-nav h3 {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 var(--space-3) 0;
|
||||
padding-left: var(--space-3);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-toc-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-left: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.page-toc-list ul {
|
||||
list-style: none;
|
||||
padding-left: var(--space-3);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-toc-list li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-toc-list a {
|
||||
display: block;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
border-color var(--transition-fast);
|
||||
border-left: 2px solid transparent;
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.page-toc-list a:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.page-toc-list a.active {
|
||||
color: var(--link-color);
|
||||
border-left-color: var(--link-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Show page-toc only on wide screens */
|
||||
@media (min-width: 1400px) {
|
||||
.page-toc {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide page-toc on mobile */
|
||||
@media (max-width: 800px) {
|
||||
.page-toc {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue