mirror of
https://github.com/infinition/Bjorn.git
synced 2026-02-05 03:31:02 +00:00
fixes
This commit is contained in:
450
index.html
450
index.html
@@ -8,6 +8,32 @@
|
||||
<title id="site-title">BJORN // WIKI NODE</title>
|
||||
<meta name="description" id="meta-description" content="Official Documentation and Wiki for BJORN Cyber Viking">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" id="og-url" content="">
|
||||
<meta property="og:title" id="og-title" content="BJORN // WIKI NODE">
|
||||
<meta property="og:description" id="og-desc" content="Official Documentation and Wiki for BJORN Cyber Viking">
|
||||
<meta property="og:image" id="og-image" content="assets/bjorn.png">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" id="tw-url" content="">
|
||||
<meta property="twitter:title" id="tw-title" content="BJORN // WIKI NODE">
|
||||
<meta property="twitter:description" id="tw-desc" content="Official Documentation and Wiki for BJORN Cyber Viking">
|
||||
<meta property="twitter:image" id="tw-image" content="assets/bjorn.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/bjorn.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/bjorn.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/bjorn.png">
|
||||
<link rel="mask-icon" href="assets/bjorn.png" color="#22c55e">
|
||||
<meta name="apple-mobile-web-app-title" content="BjornWiki">
|
||||
<meta name="application-name" content="BjornWiki">
|
||||
<meta name="msapplication-TileColor" content="#0B0C0E">
|
||||
<meta name="theme-color" content="#0B0C0E">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<!-- Configuration -->
|
||||
<script src="config.js"></script>
|
||||
|
||||
@@ -68,34 +94,128 @@
|
||||
background: var(--accent-green);
|
||||
}
|
||||
|
||||
/* Utility Classes using vars */
|
||||
/* --- GLASSMORPHISM & UI --- */
|
||||
.bg-hack-sidebar {
|
||||
background-color: var(--bg-sidebar);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.bg-hack-bg {
|
||||
background-color: var(--bg-body);
|
||||
.sidebar-mobile {
|
||||
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.border-hack-border {
|
||||
/* --- SEARCH MODAL --- */
|
||||
#search-modal {
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
#search-modal.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
#search-modal:not(.hidden) {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background-color: var(--accent-dim);
|
||||
border-color: var(--border-color);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.text-hack-heading {
|
||||
color: var(--text-heading);
|
||||
.search-result-item.active {
|
||||
background-color: var(--accent-dim);
|
||||
border-color: var(--accent-green);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.text-hack-green {
|
||||
color: var(--accent-green);
|
||||
/* --- MICRO-INTERACTIONS --- */
|
||||
.nav-link,
|
||||
.section-header,
|
||||
.copy-btn,
|
||||
.toc-link,
|
||||
.search-result-item,
|
||||
#menu-btn,
|
||||
#theme-toggle-desktop,
|
||||
#theme-toggle-mobile,
|
||||
.badge-sm,
|
||||
.pagination-card,
|
||||
#toc-btn-mobile,
|
||||
#toc-btn-desktop {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-hack-greenDim {
|
||||
.nav-link::after,
|
||||
.section-header::after,
|
||||
.copy-btn::after,
|
||||
.search-result-item::after,
|
||||
#menu-btn::after,
|
||||
#theme-toggle-desktop::after,
|
||||
#theme-toggle-mobile::after,
|
||||
.badge-sm::after,
|
||||
.pagination-card::after,
|
||||
#toc-btn-mobile::after,
|
||||
#toc-btn-desktop::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), rgba(34, 197, 94, 0.2) 0%, transparent 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-link:hover::after,
|
||||
.section-header:hover::after,
|
||||
.copy-btn:hover::after,
|
||||
.search-result-item:hover::after,
|
||||
#menu-btn:hover::after,
|
||||
#theme-toggle-desktop:hover::after,
|
||||
#theme-toggle-mobile:hover::after,
|
||||
.badge-sm:hover::after,
|
||||
.pagination-card:hover::after,
|
||||
#toc-btn-mobile:hover::after,
|
||||
#toc-btn-desktop:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#page-nav button {
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
#page-nav button:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--accent-dim);
|
||||
}
|
||||
|
||||
/* Sidebar Animations */
|
||||
.sidebar-mobile {
|
||||
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
/* --- SKELETON SCREENS --- */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--bg-sidebar) 25%, var(--border-color) 50%, var(--bg-sidebar) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- MARKDOWN STYLES --- */
|
||||
@@ -682,23 +802,27 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="theme-toggle-mobile"
|
||||
class="text-gray-300 dark:text-gray-200 hover:text-hack-green p-2 transition-colors"
|
||||
title="Switch Theme">
|
||||
title="Switch Theme" aria-label="Switch Theme">
|
||||
<i data-lucide="palette" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<button id="menu-btn"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded bg-hack-bg border border-hack-border text-hack-heading active:bg-hack-greenDim transition-colors">
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded bg-hack-bg border border-hack-border text-hack-heading active:bg-hack-greenDim transition-colors"
|
||||
aria-label="Open Menu">
|
||||
<span id="label-menu" class="text-xs font-mono font-bold uppercase tracking-wider">Menu</span>
|
||||
<i data-lucide="menu" class="w-5 h-5 text-hack-green"></i>
|
||||
</button>
|
||||
<button id="toc-btn-mobile" class="text-gray-400 hover:text-hack-green p-2 transition-colors"
|
||||
aria-label="Toggle Table of Contents">
|
||||
<i data-lucide="hash" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div id="overlay"
|
||||
class="fixed inset-0 bg-black/80 backdrop-blur-[2px] z-[65] hidden transition-opacity opacity-0 md:hidden">
|
||||
<div id="overlay" class="fixed inset-0 bg-black/80 backdrop-blur-[2px] z-[65] hidden transition-opacity opacity-0">
|
||||
</div>
|
||||
|
||||
<!-- Mobile Sidebar (Left - Menu) -->
|
||||
@@ -706,7 +830,8 @@
|
||||
class="sidebar-mobile fixed top-0 bottom-0 left-0 z-[70] w-[280px] bg-hack-sidebar border-r border-hack-border transform -translate-x-full md:translate-x-0 md:static md:h-full flex flex-col shadow-2xl md:shadow-none transition-transform duration-300">
|
||||
|
||||
<div class="p-5 border-b border-hack-border relative flex-none">
|
||||
<button id="close-sidebar-btn" class="md:hidden absolute top-4 right-4 text-gray-400 hover:text-white">
|
||||
<button id="close-sidebar-btn" class="md:hidden absolute top-4 right-4 text-gray-400 hover:text-white"
|
||||
aria-label="Close Menu">
|
||||
<i data-lucide="x" class="w-6 h-6"></i>
|
||||
</button>
|
||||
|
||||
@@ -727,15 +852,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 relative group">
|
||||
<div class="mt-4 relative group cursor-pointer" onclick="openSearch()">
|
||||
<i data-lucide="search" class="absolute left-2.5 top-2.5 w-4 h-4 text-gray-500"></i>
|
||||
<input type="text" id="search-input" placeholder="Search (Ctrl+K)..."
|
||||
class="w-full border rounded py-1.5 pl-8 pr-8 text-base md:text-sm shadow-sm transition-colors appearance-none">
|
||||
<button id="search-clear"
|
||||
class="absolute right-0 top-0 bottom-0 px-2 flex items-center justify-center text-hack-green hover:text-white transition-all z-20 cursor-pointer opacity-0 pointer-events-none"
|
||||
title="Clear search (Esc)">
|
||||
<i data-lucide="x" class="w-3 h-3"></i>
|
||||
</button>
|
||||
<div
|
||||
class="w-full border border-hack-border rounded py-1.5 pl-8 pr-3 text-xs text-gray-500 bg-hack-bg/50 flex justify-between items-center">
|
||||
<span>Search...</span>
|
||||
<span class="text-[10px] opacity-50 border border-hack-border px-1 rounded">Ctrl+K</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -790,12 +913,12 @@
|
||||
|
||||
<!-- Mobile Sidebar (Right - TOC) -->
|
||||
<aside id="mobile-toc-sidebar"
|
||||
class="sidebar-mobile fixed top-0 bottom-0 right-0 z-[70] w-[280px] bg-hack-sidebar border-l border-hack-border transform translate-x-full md:hidden flex flex-col shadow-2xl transition-transform duration-300">
|
||||
class="sidebar-mobile fixed top-0 bottom-0 right-0 z-[70] w-[280px] bg-hack-sidebar border-l border-hack-border transform translate-x-full flex flex-col shadow-2xl transition-transform duration-300">
|
||||
<div class="p-4 border-b border-hack-border flex justify-between items-center bg-hack-bg/50">
|
||||
<div id="label-mobile-toc"
|
||||
class="text-[10px] font-bold text-hack-green uppercase tracking-widest px-3 py-1 bg-hack-greenDim border border-hack-border rounded-full font-mono truncate max-w-[200px]">
|
||||
</div>
|
||||
<button id="close-toc-btn" class="text-gray-400 hover:text-white">
|
||||
<button id="close-toc-btn" class="text-gray-400 hover:text-white" aria-label="Close Table of Contents">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -823,9 +946,15 @@
|
||||
|
||||
<button id="theme-toggle-desktop"
|
||||
class="text-gray-500 hover:text-hack-green transition-colors hidden md:block bg-hack-sidebar border border-hack-border rounded p-1.5"
|
||||
title="Switch Theme">
|
||||
title="Switch Theme" aria-label="Switch Theme">
|
||||
<i data-lucide="palette" class="w-4 h-4"></i>
|
||||
</button>
|
||||
|
||||
<button id="toc-btn-desktop"
|
||||
class="text-gray-500 hover:text-hack-green transition-colors hidden md:block xl:hidden bg-hack-sidebar border border-hack-border rounded p-1.5"
|
||||
title="Table of Contents" aria-label="Toggle Table of Contents">
|
||||
<i data-lucide="hash" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -859,11 +988,44 @@
|
||||
|
||||
<button id="scroll-top-btn"
|
||||
class="absolute bottom-6 right-6 p-3 rounded-full bg-hack-greenDim border border-hack-border text-hack-green hover:bg-hack-green hover:text-white shadow-lg hidden z-30"
|
||||
title="Back to Top">
|
||||
title="Back to Top" aria-label="Back to Top">
|
||||
<i data-lucide="arrow-up" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<div id="search-modal-overlay"
|
||||
class="fixed inset-0 z-[150] bg-black/60 backdrop-blur-md hidden transition-opacity duration-300 opacity-0"
|
||||
onclick="closeSearch()">
|
||||
<div class="flex items-start justify-center pt-[10vh] px-4 h-full">
|
||||
<div id="search-modal"
|
||||
class="bg-hack-sidebar border border-hack-border w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[70vh] hidden"
|
||||
onclick="event.stopPropagation()">
|
||||
<div class="p-4 border-b border-hack-border flex items-center gap-3 bg-hack-bg/30">
|
||||
<i data-lucide="search" class="w-5 h-5 text-hack-green"></i>
|
||||
<input type="text" id="modal-search-input" placeholder="Search documentation..."
|
||||
class="flex-1 bg-transparent border-none outline-none text-lg text-hack-heading placeholder-gray-500"
|
||||
aria-label="Search documentation" role="searchbox">
|
||||
<kbd
|
||||
class="hidden md:block px-2 py-1 rounded bg-hack-bg border border-hack-border text-[10px] text-gray-500 font-mono">ESC</kbd>
|
||||
</div>
|
||||
<div id="modal-search-results" class="flex-1 overflow-y-auto p-2 space-y-1 min-h-[100px]">
|
||||
<div class="text-center py-10 text-gray-500 text-sm">Type to start searching...</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-3 border-t border-hack-border bg-hack-bg/20 flex items-center justify-between text-[10px] text-gray-500 font-mono uppercase tracking-widest">
|
||||
<div class="flex gap-4">
|
||||
<span class="flex items-center gap-1"><i data-lucide="arrow-down-up" class="w-3 h-3"></i>
|
||||
Navigate</span>
|
||||
<span class="flex items-center gap-1"><i data-lucide="corner-down-left" class="w-3 h-3"></i>
|
||||
Select</span>
|
||||
</div>
|
||||
<div id="search-count" class="text-hack-green"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- 1. CONFIG & STATE ---
|
||||
const renderer = new marked.Renderer();
|
||||
@@ -914,13 +1076,14 @@
|
||||
document.getElementById('sidebar-project-name').innerText = CONFIG.projectName;
|
||||
|
||||
// UI Strings
|
||||
document.getElementById('search-input').placeholder = CONFIG.ui.searchPlaceholder;
|
||||
const modalSearchInput = document.getElementById('modal-search-input');
|
||||
if (modalSearchInput) modalSearchInput.placeholder = CONFIG.ui.searchPlaceholder;
|
||||
|
||||
document.getElementById('label-changelog').innerText = CONFIG.ui.changelogTitle;
|
||||
document.getElementById('label-initializing').innerText = CONFIG.ui.initializingText;
|
||||
document.getElementById('label-join-us').innerText = CONFIG.ui.joinUsTitle;
|
||||
document.getElementById('label-on-this-page').innerText = CONFIG.ui.onThisPageTitle;
|
||||
document.getElementById('label-mobile-toc').innerText = CONFIG.ui.onThisPageMobile || CONFIG.ui.onThisPageTitle;
|
||||
document.getElementById('search-results-msg').innerText = CONFIG.ui.noResultsText;
|
||||
document.getElementById('label-menu').innerText = CONFIG.ui.menuText || "Menu";
|
||||
|
||||
const versionLoading = document.querySelector('.version-loading');
|
||||
@@ -932,9 +1095,9 @@
|
||||
changelogBtn.style.display = CONFIG.features.showChangelog ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
const searchContainer = document.getElementById('search-input').parentElement;
|
||||
if (searchContainer) {
|
||||
searchContainer.style.display = CONFIG.features.showSearch ? 'block' : 'none';
|
||||
const searchTrigger = document.querySelector('[onclick="openSearch()"]');
|
||||
if (searchTrigger) {
|
||||
searchTrigger.style.display = CONFIG.features.showSearch ? 'block' : 'none';
|
||||
}
|
||||
|
||||
const socialSection = document.getElementById('social-section');
|
||||
@@ -1336,7 +1499,21 @@
|
||||
|
||||
// Only show loader if not in RAM cache
|
||||
if (!STATE.contentCache[`${folder}/${filename}`]) {
|
||||
viewer.innerHTML = `<div class="animate-pulse space-y-4 pt-10"><div class="h-8 bg-gray-800 rounded w-1/3 mb-6"></div><div class="h-4 bg-gray-800 rounded w-full"></div><div class="h-4 bg-gray-800 rounded w-5/6"></div><div class="h-4 bg-gray-800 rounded w-4/6"></div></div>`;
|
||||
viewer.innerHTML = `
|
||||
<div class="space-y-6 pt-4">
|
||||
<div class="skeleton h-10 w-2/3 mb-8"></div>
|
||||
<div class="space-y-3">
|
||||
<div class="skeleton h-4 w-full"></div>
|
||||
<div class="skeleton h-4 w-11/12"></div>
|
||||
<div class="skeleton h-4 w-full"></div>
|
||||
<div class="skeleton h-4 w-4/5"></div>
|
||||
</div>
|
||||
<div class="skeleton h-48 w-full mt-10"></div>
|
||||
<div class="space-y-3 mt-10">
|
||||
<div class="skeleton h-4 w-full"></div>
|
||||
<div class="skeleton h-4 w-5/6"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
pageNav.innerHTML = '';
|
||||
document.getElementById('reading-time').innerText = '';
|
||||
@@ -1400,6 +1577,14 @@
|
||||
const cleanHTML = DOMPurify.sanitize(marked.parse(text));
|
||||
viewer.innerHTML = cleanHTML;
|
||||
|
||||
// Update SEO Tags
|
||||
const fullTitle = `${title} | ${CONFIG.projectName}`;
|
||||
document.title = fullTitle;
|
||||
document.getElementById('og-title').content = fullTitle;
|
||||
document.getElementById('tw-title').content = fullTitle;
|
||||
document.getElementById('og-url').content = window.location.href;
|
||||
document.getElementById('tw-url').content = window.location.href;
|
||||
|
||||
const wordCount = text.replace(/[#*`]/g, '').split(/\s+/).length;
|
||||
document.getElementById('reading-time').textContent = `${CONFIG.ui.readingTimePrefix}${Math.ceil(wordCount / 200)} ${CONFIG.ui.readingTimeSuffix}`;
|
||||
fetchLastUpdated(folder, filename);
|
||||
@@ -1539,6 +1724,7 @@
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'copy-btn';
|
||||
btn.textContent = 'Copy';
|
||||
btn.setAttribute('aria-label', 'Copy code to clipboard');
|
||||
btn.onclick = () => {
|
||||
navigator.clipboard.writeText(pre.querySelector('code').innerText);
|
||||
btn.textContent = 'Copied!';
|
||||
@@ -1579,7 +1765,7 @@
|
||||
if (idx > 0) {
|
||||
const prev = flatList[idx - 1];
|
||||
const btn = document.createElement('button');
|
||||
btn.className = "text-left p-4 rounded border border-hack-border hover:border-hack-green group transition-colors bg-hack-sidebar/50 hover:bg-hack-sidebar w-full flex flex-col items-start";
|
||||
btn.className = "pagination-card text-left p-4 rounded border border-hack-border hover:border-hack-green group transition-colors bg-hack-sidebar/50 hover:bg-hack-sidebar w-full flex flex-col items-start";
|
||||
btn.innerHTML = `<span class="text-[10px] text-gray-500 uppercase tracking-widest mb-1 group-hover:text-hack-green">Previous</span><span class="font-bold text-sm truncate text-hack-heading w-full">« ${prev.title}</span>`;
|
||||
btn.onclick = () => loadContent(prev.folder, prev.title, prev.file);
|
||||
navContainer.appendChild(btn);
|
||||
@@ -1588,7 +1774,7 @@
|
||||
if (idx < flatList.length - 1) {
|
||||
const next = flatList[idx + 1];
|
||||
const btn = document.createElement('button');
|
||||
btn.className = "text-right p-4 rounded border border-hack-border hover:border-hack-green group transition-colors bg-hack-sidebar/50 hover:bg-hack-sidebar w-full flex flex-col items-end";
|
||||
btn.className = "pagination-card text-right p-4 rounded border border-hack-border hover:border-hack-green group transition-colors bg-hack-sidebar/50 hover:bg-hack-sidebar w-full flex flex-col items-end";
|
||||
btn.innerHTML = `<span class="text-[10px] text-gray-500 uppercase tracking-widest mb-1 group-hover:text-hack-green">Next</span><span class="font-bold text-sm truncate text-hack-heading w-full">${next.title} »</span>`;
|
||||
btn.onclick = () => loadContent(next.folder, next.title, next.file);
|
||||
navContainer.appendChild(btn);
|
||||
@@ -1804,55 +1990,136 @@
|
||||
|
||||
renderRecursive(STATE.wikiData, container);
|
||||
|
||||
if (!hasContent && searchResults) noResults.classList.remove('hidden');
|
||||
else noResults.classList.add('hidden');
|
||||
if (!hasContent && searchResults) {
|
||||
if (noResults) noResults.classList.remove('hidden');
|
||||
} else {
|
||||
if (noResults) noResults.classList.add('hidden');
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// --- 7. SEARCH ---
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchClear = document.getElementById('search-clear');
|
||||
let debounceTimeout;
|
||||
// --- 7. SEARCH (MODAL VERSION) ---
|
||||
const searchModalOverlay = document.getElementById('search-modal-overlay');
|
||||
const searchModal = document.getElementById('search-modal');
|
||||
const modalSearchInput = document.getElementById('modal-search-input');
|
||||
const modalSearchResults = document.getElementById('modal-search-results');
|
||||
const searchCount = document.getElementById('search-count');
|
||||
let selectedResultIndex = -1;
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(debounceTimeout);
|
||||
debounceTimeout = setTimeout(() => {
|
||||
const q = e.target.value.toLowerCase();
|
||||
if (!q) {
|
||||
renderSidebar();
|
||||
searchClear.classList.add('opacity-0', 'pointer-events-none');
|
||||
return;
|
||||
}
|
||||
searchClear.classList.remove('opacity-0', 'pointer-events-none');
|
||||
function openSearch() {
|
||||
searchModalOverlay.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
searchModalOverlay.classList.remove('opacity-0');
|
||||
searchModal.classList.remove('hidden');
|
||||
modalSearchInput.focus();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
const results = {};
|
||||
if (STATE.searchIndex.length > 0) {
|
||||
STATE.searchIndex.forEach(item => {
|
||||
if (item.titleLower.includes(q) || item.content.includes(q)) {
|
||||
if (!results[item.folder]) results[item.folder] = {};
|
||||
results[item.folder][item.title] = item.filename;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for (const [folder, files] of Object.entries(STATE.wikiData)) {
|
||||
for (const [title, file] of Object.entries(files)) {
|
||||
if (title.toLowerCase().includes(q)) {
|
||||
if (!results[folder]) results[folder] = {};
|
||||
results[folder][title] = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
renderSidebar(results);
|
||||
function closeSearch() {
|
||||
searchModalOverlay.classList.add('opacity-0');
|
||||
searchModal.classList.add('hidden');
|
||||
setTimeout(() => {
|
||||
searchModalOverlay.classList.add('hidden');
|
||||
modalSearchInput.value = '';
|
||||
modalSearchResults.innerHTML = '<div class="text-center py-10 text-gray-500 text-sm">Type to start searching...</div>';
|
||||
searchCount.innerText = '';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
modalSearchInput.addEventListener('input', (e) => {
|
||||
const q = e.target.value.toLowerCase().trim();
|
||||
if (!q) {
|
||||
modalSearchResults.innerHTML = '<div class="text-center py-10 text-gray-500 text-sm">Type to start searching...</div>';
|
||||
searchCount.innerText = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
STATE.searchIndex.forEach(item => {
|
||||
const titleMatch = item.titleLower.indexOf(q);
|
||||
const contentMatch = item.content.indexOf(q);
|
||||
|
||||
if (titleMatch !== -1 || contentMatch !== -1) {
|
||||
// Calculate a simple score: title match is better
|
||||
const score = (titleMatch !== -1 ? 100 : 0) + (contentMatch !== -1 ? 10 : 0);
|
||||
|
||||
// Extract snippet
|
||||
let snippet = "";
|
||||
if (contentMatch !== -1) {
|
||||
const start = Math.max(0, contentMatch - 40);
|
||||
const end = Math.min(item.content.length, contentMatch + 80);
|
||||
snippet = item.content.substring(start, end).replace(new RegExp(q, 'gi'), (m) => `<mark class="bg-hack-green/30 text-hack-green rounded px-0.5">${m}</mark>`);
|
||||
if (start > 0) snippet = "..." + snippet;
|
||||
if (end < item.content.length) snippet = snippet + "...";
|
||||
}
|
||||
|
||||
results.push({ ...item, score, snippet });
|
||||
}
|
||||
});
|
||||
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
renderSearchResults(results.slice(0, 10)); // Top 10
|
||||
});
|
||||
|
||||
searchClear.onclick = () => {
|
||||
searchInput.value = '';
|
||||
searchInput.dispatchEvent(new Event('input'));
|
||||
searchInput.focus();
|
||||
};
|
||||
function renderSearchResults(results) {
|
||||
if (results.length === 0) {
|
||||
modalSearchResults.innerHTML = '<div class="text-center py-10 text-red-400/60 text-sm italic">No results found for your query.</div>';
|
||||
searchCount.innerText = '0 results';
|
||||
return;
|
||||
}
|
||||
|
||||
searchCount.innerText = `${results.length} results`;
|
||||
modalSearchResults.innerHTML = results.map((res, i) => `
|
||||
<div class="search-result-item p-3 rounded-xl cursor-pointer flex flex-col gap-1" onclick="selectSearchResult(${i})" data-index="${i}">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-hack-heading font-bold text-sm">${res.title}</span>
|
||||
<span class="text-[10px] text-gray-500 font-mono opacity-50 uppercase">${res.folder.replace(/_/g, ' ')}</span>
|
||||
</div>
|
||||
${res.snippet ? `<div class="text-xs text-gray-400 line-clamp-2 font-sans leading-relaxed">${res.snippet}</div>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
selectedResultIndex = 0;
|
||||
updateSelectedResult();
|
||||
|
||||
// Store results for keyboard nav
|
||||
window.currentSearchResults = results;
|
||||
}
|
||||
|
||||
function updateSelectedResult() {
|
||||
const items = modalSearchResults.querySelectorAll('.search-result-item');
|
||||
items.forEach((item, i) => {
|
||||
if (i === selectedResultIndex) {
|
||||
item.classList.add('active');
|
||||
item.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectSearchResult(index) {
|
||||
const res = window.currentSearchResults[index];
|
||||
if (res) {
|
||||
loadContent(res.folder, res.title, res.filename);
|
||||
closeSearch();
|
||||
}
|
||||
}
|
||||
|
||||
modalSearchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
selectedResultIndex = (selectedResultIndex + 1) % (window.currentSearchResults?.length || 1);
|
||||
updateSelectedResult();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
selectedResultIndex = (selectedResultIndex - 1 + (window.currentSearchResults?.length || 1)) % (window.currentSearchResults?.length || 1);
|
||||
updateSelectedResult();
|
||||
} else if (e.key === 'Enter') {
|
||||
if (selectedResultIndex !== -1) selectSearchResult(selectedResultIndex);
|
||||
}
|
||||
});
|
||||
|
||||
// --- 8. CHANGELOG ---
|
||||
async function toggleVersionsPage(btn, pushHistory = true) {
|
||||
@@ -1887,6 +2154,7 @@
|
||||
renderSidebar();
|
||||
|
||||
if (pushHistory) window.history.pushState({ page: 'versions' }, "", "?page=changelog");
|
||||
if (window.innerWidth < 768) closeMenu();
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases`);
|
||||
@@ -1982,6 +2250,8 @@
|
||||
document.getElementById('menu-btn').onclick = openMenu;
|
||||
document.getElementById('close-sidebar-btn').onclick = closeMenu;
|
||||
document.getElementById('close-toc-btn').onclick = closeTOC;
|
||||
document.getElementById('toc-btn-mobile').onclick = openTOC;
|
||||
document.getElementById('toc-btn-desktop').onclick = openTOC;
|
||||
|
||||
// Unified overlay click closes whichever is open
|
||||
overlay.onclick = () => {
|
||||
@@ -1991,9 +2261,12 @@
|
||||
|
||||
// Events
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); searchInput.focus(); }
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if (document.activeElement === searchInput) searchInput.blur();
|
||||
if (!searchModalOverlay.classList.contains('hidden')) closeSearch();
|
||||
else if (lightbox.classList.contains('active')) closeLightbox();
|
||||
else {
|
||||
closeMenu();
|
||||
@@ -2127,6 +2400,23 @@
|
||||
fetchLatestVersion();
|
||||
initWiki();
|
||||
lucide.createIcons();
|
||||
|
||||
// Register Service Worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('./sw.js').catch(() => { });
|
||||
}
|
||||
|
||||
// Mouse tracking for glow effects
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
const target = e.target.closest('.nav-link, .section-header, .copy-btn, .search-result-item, #menu-btn, #theme-toggle-desktop, #theme-toggle-mobile, .badge-sm, .pagination-card, #toc-btn-mobile, #toc-btn-desktop');
|
||||
if (target) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
target.style.setProperty('--x', `${x}%`);
|
||||
target.style.setProperty('--y', `${y}%`);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
35
manifest.json
Normal file
35
manifest.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "BJORN WIKI",
|
||||
"short_name": "BjornWiki",
|
||||
"description": "Official Documentation and Wiki for BJORN Cyber Viking",
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#0B0C0E",
|
||||
"theme_color": "#22c55e",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/bjorn.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "assets/bjorn.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Search Wiki",
|
||||
"url": "./index.html?search=true",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/bjorn.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
69
sw.js
Normal file
69
sw.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const CACHE_NAME = 'bjorn-wiki-v2';
|
||||
const STATIC_ASSETS = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'./config.js',
|
||||
'./manifest.json',
|
||||
'./assets/bjorn.png',
|
||||
'https://cdn.tailwindcss.com',
|
||||
'https://unpkg.com/lucide@latest',
|
||||
'https://cdn.jsdelivr.net/npm/marked/marked.min.js',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js',
|
||||
'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600&display=swap'
|
||||
];
|
||||
|
||||
// Install Event - Cache Static Assets
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
console.log('[SW] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS);
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Activate Event - Clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
console.log('[SW] Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
// Fetch Event - Stale-While-Revalidate Strategy
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// Skip non-GET requests
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
// Strategy: Stale-While-Revalidate for most assets
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.match(event.request).then((response) => {
|
||||
const fetchPromise = fetch(event.request).then((networkResponse) => {
|
||||
// Cache the new response
|
||||
if (networkResponse.ok) {
|
||||
cache.put(event.request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
}).catch(() => {
|
||||
// If network fails, we already returned the cached response if it exists
|
||||
});
|
||||
|
||||
return response || fetchPromise;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user