This commit is contained in:
Fabien POLLY
2026-01-23 11:51:48 +01:00
parent b94dacc563
commit 686263ca6a
3 changed files with 474 additions and 80 deletions

View File

@@ -8,6 +8,32 @@
<title id="site-title">BJORN // WIKI NODE</title> <title id="site-title">BJORN // WIKI NODE</title>
<meta name="description" id="meta-description" content="Official Documentation and Wiki for BJORN Cyber Viking"> <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 --> <!-- Configuration -->
<script src="config.js"></script> <script src="config.js"></script>
@@ -68,34 +94,128 @@
background: var(--accent-green); background: var(--accent-green);
} }
/* Utility Classes using vars */ /* --- GLASSMORPHISM & UI --- */
.bg-hack-sidebar { .bg-hack-sidebar {
background-color: var(--bg-sidebar); background-color: var(--bg-sidebar);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
} }
.bg-hack-bg { .sidebar-mobile {
background-color: var(--bg-body); 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); border-color: var(--border-color);
transform: translateX(4px);
} }
.text-hack-heading { .search-result-item.active {
color: var(--text-heading); background-color: var(--accent-dim);
border-color: var(--accent-green);
transform: translateX(4px);
} }
.text-hack-green { /* --- MICRO-INTERACTIONS --- */
color: var(--accent-green); .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); background-color: var(--accent-dim);
} }
/* Sidebar Animations */ /* --- SKELETON SCREENS --- */
.sidebar-mobile { .skeleton {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); 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 --- */ /* --- MARKDOWN STYLES --- */
@@ -682,23 +802,27 @@
</span> </span>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-2">
<button id="theme-toggle-mobile" <button id="theme-toggle-mobile"
class="text-gray-300 dark:text-gray-200 hover:text-hack-green p-2 transition-colors" 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> <i data-lucide="palette" class="w-5 h-5"></i>
</button> </button>
<button id="menu-btn" <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> <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> <i data-lucide="menu" class="w-5 h-5 text-hack-green"></i>
</button> </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> </div>
</header> </header>
<!-- Overlay --> <!-- Overlay -->
<div id="overlay" <div id="overlay" class="fixed inset-0 bg-black/80 backdrop-blur-[2px] z-[65] hidden transition-opacity opacity-0">
class="fixed inset-0 bg-black/80 backdrop-blur-[2px] z-[65] hidden transition-opacity opacity-0 md:hidden">
</div> </div>
<!-- Mobile Sidebar (Left - Menu) --> <!-- 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"> 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"> <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> <i data-lucide="x" class="w-6 h-6"></i>
</button> </button>
@@ -727,15 +852,13 @@
</div> </div>
</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> <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)..." <div
class="w-full border rounded py-1.5 pl-8 pr-8 text-base md:text-sm shadow-sm transition-colors appearance-none"> 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">
<button id="search-clear" <span>Search...</span>
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" <span class="text-[10px] opacity-50 border border-hack-border px-1 rounded">Ctrl+K</span>
title="Clear search (Esc)"> </div>
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div> </div>
</div> </div>
@@ -790,12 +913,12 @@
<!-- Mobile Sidebar (Right - TOC) --> <!-- Mobile Sidebar (Right - TOC) -->
<aside id="mobile-toc-sidebar" <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 class="p-4 border-b border-hack-border flex justify-between items-center bg-hack-bg/50">
<div id="label-mobile-toc" <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]"> 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> </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> <i data-lucide="x" class="w-5 h-5"></i>
</button> </button>
</div> </div>
@@ -823,9 +946,15 @@
<button id="theme-toggle-desktop" <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" 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> <i data-lucide="palette" class="w-4 h-4"></i>
</button> </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> </div>
</div> </div>
@@ -859,11 +988,44 @@
<button id="scroll-top-btn" <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" 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> <i data-lucide="arrow-up" class="w-5 h-5"></i>
</button> </button>
</div> </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> <script>
// --- 1. CONFIG & STATE --- // --- 1. CONFIG & STATE ---
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
@@ -914,13 +1076,14 @@
document.getElementById('sidebar-project-name').innerText = CONFIG.projectName; document.getElementById('sidebar-project-name').innerText = CONFIG.projectName;
// UI Strings // 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-changelog').innerText = CONFIG.ui.changelogTitle;
document.getElementById('label-initializing').innerText = CONFIG.ui.initializingText; document.getElementById('label-initializing').innerText = CONFIG.ui.initializingText;
document.getElementById('label-join-us').innerText = CONFIG.ui.joinUsTitle; document.getElementById('label-join-us').innerText = CONFIG.ui.joinUsTitle;
document.getElementById('label-on-this-page').innerText = CONFIG.ui.onThisPageTitle; document.getElementById('label-on-this-page').innerText = CONFIG.ui.onThisPageTitle;
document.getElementById('label-mobile-toc').innerText = CONFIG.ui.onThisPageMobile || 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"; document.getElementById('label-menu').innerText = CONFIG.ui.menuText || "Menu";
const versionLoading = document.querySelector('.version-loading'); const versionLoading = document.querySelector('.version-loading');
@@ -932,9 +1095,9 @@
changelogBtn.style.display = CONFIG.features.showChangelog ? 'flex' : 'none'; changelogBtn.style.display = CONFIG.features.showChangelog ? 'flex' : 'none';
} }
const searchContainer = document.getElementById('search-input').parentElement; const searchTrigger = document.querySelector('[onclick="openSearch()"]');
if (searchContainer) { if (searchTrigger) {
searchContainer.style.display = CONFIG.features.showSearch ? 'block' : 'none'; searchTrigger.style.display = CONFIG.features.showSearch ? 'block' : 'none';
} }
const socialSection = document.getElementById('social-section'); const socialSection = document.getElementById('social-section');
@@ -1336,7 +1499,21 @@
// Only show loader if not in RAM cache // Only show loader if not in RAM cache
if (!STATE.contentCache[`${folder}/${filename}`]) { 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 = ''; pageNav.innerHTML = '';
document.getElementById('reading-time').innerText = ''; document.getElementById('reading-time').innerText = '';
@@ -1400,6 +1577,14 @@
const cleanHTML = DOMPurify.sanitize(marked.parse(text)); const cleanHTML = DOMPurify.sanitize(marked.parse(text));
viewer.innerHTML = cleanHTML; 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; const wordCount = text.replace(/[#*`]/g, '').split(/\s+/).length;
document.getElementById('reading-time').textContent = `${CONFIG.ui.readingTimePrefix}${Math.ceil(wordCount / 200)} ${CONFIG.ui.readingTimeSuffix}`; document.getElementById('reading-time').textContent = `${CONFIG.ui.readingTimePrefix}${Math.ceil(wordCount / 200)} ${CONFIG.ui.readingTimeSuffix}`;
fetchLastUpdated(folder, filename); fetchLastUpdated(folder, filename);
@@ -1539,6 +1724,7 @@
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.className = 'copy-btn'; btn.className = 'copy-btn';
btn.textContent = 'Copy'; btn.textContent = 'Copy';
btn.setAttribute('aria-label', 'Copy code to clipboard');
btn.onclick = () => { btn.onclick = () => {
navigator.clipboard.writeText(pre.querySelector('code').innerText); navigator.clipboard.writeText(pre.querySelector('code').innerText);
btn.textContent = 'Copied!'; btn.textContent = 'Copied!';
@@ -1579,7 +1765,7 @@
if (idx > 0) { if (idx > 0) {
const prev = flatList[idx - 1]; const prev = flatList[idx - 1];
const btn = document.createElement('button'); 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.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); btn.onclick = () => loadContent(prev.folder, prev.title, prev.file);
navContainer.appendChild(btn); navContainer.appendChild(btn);
@@ -1588,7 +1774,7 @@
if (idx < flatList.length - 1) { if (idx < flatList.length - 1) {
const next = flatList[idx + 1]; const next = flatList[idx + 1];
const btn = document.createElement('button'); 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.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); btn.onclick = () => loadContent(next.folder, next.title, next.file);
navContainer.appendChild(btn); navContainer.appendChild(btn);
@@ -1804,55 +1990,136 @@
renderRecursive(STATE.wikiData, container); renderRecursive(STATE.wikiData, container);
if (!hasContent && searchResults) noResults.classList.remove('hidden'); if (!hasContent && searchResults) {
else noResults.classList.add('hidden'); if (noResults) noResults.classList.remove('hidden');
} else {
if (noResults) noResults.classList.add('hidden');
}
lucide.createIcons(); lucide.createIcons();
} }
// --- 7. SEARCH --- // --- 7. SEARCH (MODAL VERSION) ---
const searchInput = document.getElementById('search-input'); const searchModalOverlay = document.getElementById('search-modal-overlay');
const searchClear = document.getElementById('search-clear'); const searchModal = document.getElementById('search-modal');
let debounceTimeout; 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) => { function openSearch() {
clearTimeout(debounceTimeout); searchModalOverlay.classList.remove('hidden');
debounceTimeout = setTimeout(() => { setTimeout(() => {
const q = e.target.value.toLowerCase(); searchModalOverlay.classList.remove('opacity-0');
if (!q) { searchModal.classList.remove('hidden');
renderSidebar(); modalSearchInput.focus();
searchClear.classList.add('opacity-0', 'pointer-events-none'); }, 10);
return; }
}
searchClear.classList.remove('opacity-0', 'pointer-events-none');
const results = {}; function closeSearch() {
if (STATE.searchIndex.length > 0) { searchModalOverlay.classList.add('opacity-0');
STATE.searchIndex.forEach(item => { searchModal.classList.add('hidden');
if (item.titleLower.includes(q) || item.content.includes(q)) { setTimeout(() => {
if (!results[item.folder]) results[item.folder] = {}; searchModalOverlay.classList.add('hidden');
results[item.folder][item.title] = item.filename; modalSearchInput.value = '';
} modalSearchResults.innerHTML = '<div class="text-center py-10 text-gray-500 text-sm">Type to start searching...</div>';
}); searchCount.innerText = '';
} 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);
}, 300); }, 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 = () => { function renderSearchResults(results) {
searchInput.value = ''; if (results.length === 0) {
searchInput.dispatchEvent(new Event('input')); modalSearchResults.innerHTML = '<div class="text-center py-10 text-red-400/60 text-sm italic">No results found for your query.</div>';
searchInput.focus(); 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 --- // --- 8. CHANGELOG ---
async function toggleVersionsPage(btn, pushHistory = true) { async function toggleVersionsPage(btn, pushHistory = true) {
@@ -1887,6 +2154,7 @@
renderSidebar(); renderSidebar();
if (pushHistory) window.history.pushState({ page: 'versions' }, "", "?page=changelog"); if (pushHistory) window.history.pushState({ page: 'versions' }, "", "?page=changelog");
if (window.innerWidth < 768) closeMenu();
try { try {
const res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases`); const res = await fetch(`https://api.github.com/repos/${STATE.repo}/releases`);
@@ -1982,6 +2250,8 @@
document.getElementById('menu-btn').onclick = openMenu; document.getElementById('menu-btn').onclick = openMenu;
document.getElementById('close-sidebar-btn').onclick = closeMenu; document.getElementById('close-sidebar-btn').onclick = closeMenu;
document.getElementById('close-toc-btn').onclick = closeTOC; 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 // Unified overlay click closes whichever is open
overlay.onclick = () => { overlay.onclick = () => {
@@ -1991,9 +2261,12 @@
// Events // Events
document.addEventListener('keydown', (e) => { 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 (e.key === 'Escape') {
if (document.activeElement === searchInput) searchInput.blur(); if (!searchModalOverlay.classList.contains('hidden')) closeSearch();
else if (lightbox.classList.contains('active')) closeLightbox(); else if (lightbox.classList.contains('active')) closeLightbox();
else { else {
closeMenu(); closeMenu();
@@ -2127,6 +2400,23 @@
fetchLatestVersion(); fetchLatestVersion();
initWiki(); initWiki();
lucide.createIcons(); 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> </script>
</body> </body>

35
manifest.json Normal file
View 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
View 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;
});
})
);
});