Add ChronoSeal product website
This commit is contained in:
1 parent
6c11495892
commit
1e038901b3
33 files changed
+6352
No files matched your search
+184
@@ -0,0 +1,184 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Website — app.js
|
||||
Core runtime: header scroll, mobile nav, scroll animations,
|
||||
code copy, back-to-top, keyboard shortcuts
|
||||
============================================================ */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* ---- Header scroll state ---- */
|
||||
var header = document.querySelector('.navbar');
|
||||
if (header) {
|
||||
var onScroll = function () {
|
||||
header.classList.toggle('scrolled', window.scrollY > 20);
|
||||
};
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
onScroll();
|
||||
}
|
||||
|
||||
/* ---- Enhanced Mobile nav toggle ---- */
|
||||
var toggle = document.querySelector('.nav-toggle');
|
||||
var navLinks = document.querySelector('.nav-links');
|
||||
if (toggle && navLinks) {
|
||||
var menuIcon = toggle.querySelector('i');
|
||||
toggle.addEventListener('click', function () {
|
||||
var isActive = navLinks.classList.contains('active');
|
||||
if (isActive) {
|
||||
navLinks.classList.remove('active');
|
||||
if (menuIcon) {
|
||||
menuIcon.classList.remove('fa-times');
|
||||
menuIcon.classList.add('fa-bars');
|
||||
}
|
||||
} else {
|
||||
navLinks.classList.add('active');
|
||||
if (menuIcon) {
|
||||
menuIcon.classList.remove('fa-bars');
|
||||
menuIcon.classList.add('fa-times');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Close nav on link click
|
||||
navLinks.querySelectorAll('a').forEach(function (a) {
|
||||
a.addEventListener('click', function () {
|
||||
navLinks.classList.remove('active');
|
||||
if (menuIcon) {
|
||||
menuIcon.classList.remove('fa-times');
|
||||
menuIcon.classList.add('fa-bars');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Doc sidebar mobile toggle ---- */
|
||||
var sidebarToggle = document.querySelector('.doc-sidebar-toggle');
|
||||
var sidebar = document.querySelector('.doc-sidebar');
|
||||
if (sidebarToggle && sidebar) {
|
||||
sidebarToggle.addEventListener('click', function () {
|
||||
sidebar.classList.toggle('open');
|
||||
});
|
||||
// Close sidebar on link click (mobile)
|
||||
sidebar.querySelectorAll('.doc-nav-item').forEach(function (a) {
|
||||
a.addEventListener('click', function () {
|
||||
if (window.innerWidth <= 900) sidebar.classList.remove('open');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Intersection Observer for Scroll Animations ---- */
|
||||
if ('IntersectionObserver' in window) {
|
||||
var observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' };
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('in-view');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all animated elements
|
||||
document.querySelectorAll('.animate, .card, .stat-card, .step').forEach(function (el) {
|
||||
if (!el.classList.contains('animate')) {
|
||||
el.classList.add('animate');
|
||||
}
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Code block copy buttons ---- */
|
||||
document.querySelectorAll('.code-copy-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var pre = btn.closest('.code-block').querySelector('pre');
|
||||
if (!pre) return;
|
||||
var text = pre.textContent;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
|
||||
});
|
||||
} else {
|
||||
// Fallback
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.cssText = 'position:fixed;left:-999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ---- Back to top ---- */
|
||||
var btt = document.querySelector('.back-to-top');
|
||||
if (btt) {
|
||||
window.addEventListener('scroll', function () {
|
||||
btt.classList.toggle('visible', window.scrollY > 400);
|
||||
}, { passive: true });
|
||||
btt.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Keyboard shortcut: / or Ctrl+K for search ---- */
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === '/' && !isInput(e.target)) {
|
||||
e.preventDefault();
|
||||
if (typeof ChronoSearch !== 'undefined') ChronoSearch.open();
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
if (typeof ChronoSearch !== 'undefined') ChronoSearch.open();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if (typeof ChronoSearch !== 'undefined') ChronoSearch.close();
|
||||
}
|
||||
});
|
||||
|
||||
function isInput(el) {
|
||||
var tag = el.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
||||
}
|
||||
|
||||
/* ---- Nav dropdown (desktop hover + mobile tap) ---- */
|
||||
document.querySelectorAll('.nav-dropdown').forEach(function (dd) {
|
||||
var trigger = dd.querySelector('.nav-link');
|
||||
if (!trigger) return;
|
||||
trigger.addEventListener('click', function (e) {
|
||||
if (window.innerWidth <= 768) {
|
||||
e.preventDefault();
|
||||
dd.classList.toggle('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ---- Active nav link highlight ---- */
|
||||
var currentPage = window.location.pathname.split('/').pop() || 'index.html';
|
||||
document.querySelectorAll('.nav-links a, .doc-nav-item').forEach(function (link) {
|
||||
var href = link.getAttribute('href');
|
||||
if (!href) return;
|
||||
var linkPage = href.split('/').pop().split('#')[0] || 'index.html';
|
||||
if (linkPage === currentPage) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
/* ---- Smooth anchor scroll ---- */
|
||||
document.querySelectorAll('a[href^="#"]').forEach(function (a) {
|
||||
a.addEventListener('click', function (e) {
|
||||
var id = a.getAttribute('href').substring(1);
|
||||
var target = document.getElementById(id);
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
history.pushState(null, '', '#' + id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,160 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Website — diagrams.js
|
||||
Generates SVG architecture & protocol flow diagrams inline
|
||||
============================================================ */
|
||||
|
||||
var ChronoDiagrams = (function () {
|
||||
'use strict';
|
||||
|
||||
function drawArchitecture(containerId) {
|
||||
var el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
|
||||
var svg = '<svg viewBox="0 0 760 420" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;max-width:760px">';
|
||||
|
||||
/* Background */
|
||||
svg += '<rect width="760" height="420" rx="12" fill="#131620"/>';
|
||||
|
||||
/* Browser box */
|
||||
svg += '<rect x="40" y="30" width="280" height="170" rx="8" fill="#1e2230" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<text x="180" y="55" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">BROWSER</text>';
|
||||
|
||||
svg += '<rect x="60" y="70" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="115" y="93" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">WASM Runtime</text>';
|
||||
|
||||
svg += '<rect x="190" y="70" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="245" y="93" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">heartbeat.js</text>';
|
||||
|
||||
svg += '<rect x="60" y="120" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="115" y="143" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Ed25519 Keys</text>';
|
||||
|
||||
svg += '<rect x="190" y="120" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="245" y="143" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Gene Engine</text>';
|
||||
|
||||
svg += '<text x="180" y="185" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">Entropy · Signing · VM · Mutation</text>';
|
||||
|
||||
/* Arrow: Browser → Server */
|
||||
svg += '<line x1="320" y1="115" x2="430" y2="115" stroke="#7b7e85" stroke-width="1.5" stroke-dasharray="6 3"/>';
|
||||
svg += '<polygon points="428,110 438,115 428,120" fill="#7b7e85"/>';
|
||||
svg += '<text x="375" y="105" fill="#6b6f7a" font-size="8" text-anchor="middle" font-family="Inter,sans-serif">POST /hb</text>';
|
||||
svg += '<text x="375" y="135" fill="#6b6f7a" font-size="8" text-anchor="middle" font-family="Inter,sans-serif">POST /init</text>';
|
||||
|
||||
/* Server box */
|
||||
svg += '<rect x="440" y="30" width="280" height="170" rx="8" fill="#1e2230" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<text x="580" y="55" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">SERVER (chronoseal)</text>';
|
||||
|
||||
svg += '<rect x="460" y="70" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="515" y="93" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Axum Router</text>';
|
||||
|
||||
svg += '<rect x="590" y="70" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="645" y="93" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Rate Limiter</text>';
|
||||
|
||||
svg += '<rect x="460" y="120" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="515" y="143" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Verifier</text>';
|
||||
|
||||
svg += '<rect x="590" y="120" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="645" y="143" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Gene Engine</text>';
|
||||
|
||||
svg += '<text x="580" y="185" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">Verify · Advance · Trust · CAS</text>';
|
||||
|
||||
/* Shared box (bottom center) */
|
||||
svg += '<rect x="230" y="240" width="300" height="60" rx="8" fill="#1e2230" stroke="#b0b2b8" stroke-width="1" stroke-dasharray="4 3"/>';
|
||||
svg += '<text x="380" y="266" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">shared crate</text>';
|
||||
svg += '<text x="380" y="284" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">Protocol · Hashing · Gene · VM · Constants</text>';
|
||||
|
||||
/* Arrows to shared */
|
||||
svg += '<line x1="180" y1="200" x2="310" y2="244" stroke="#7b7e85" stroke-width="1" stroke-dasharray="4 3"/>';
|
||||
svg += '<line x1="580" y1="200" x2="450" y2="244" stroke="#7b7e85" stroke-width="1" stroke-dasharray="4 3"/>';
|
||||
|
||||
/* Storage box */
|
||||
svg += '<rect x="440" y="340" width="280" height="55" rx="8" fill="#1e2230" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<text x="580" y="365" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">STORAGE</text>';
|
||||
svg += '<text x="580" y="382" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">SQLite In-Memory · SQLite Disk · Valkey</text>';
|
||||
|
||||
/* Server → Storage arrow */
|
||||
svg += '<line x1="580" y1="200" x2="580" y2="340" stroke="#7b7e85" stroke-width="1.5" stroke-dasharray="6 3"/>';
|
||||
svg += '<polygon points="575,338 580,348 585,338" fill="#7b7e85"/>';
|
||||
|
||||
/* CLI box */
|
||||
svg += '<rect x="40" y="340" width="170" height="55" rx="8" fill="#1e2230" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<text x="125" y="365" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">CLI</text>';
|
||||
svg += '<text x="125" y="382" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">status · health · metrics · stats</text>';
|
||||
|
||||
svg += '</svg>';
|
||||
el.innerHTML = svg;
|
||||
}
|
||||
|
||||
function drawProtocolFlow(containerId) {
|
||||
var el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
|
||||
var svg = '<svg viewBox="0 0 600 520" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;max-width:600px">';
|
||||
svg += '<rect width="600" height="520" rx="12" fill="#131620"/>';
|
||||
|
||||
/* Columns */
|
||||
svg += '<text x="150" y="30" fill="#b0b2b8" font-size="12" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">Browser</text>';
|
||||
svg += '<text x="450" y="30" fill="#b0b2b8" font-size="12" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">Server</text>';
|
||||
|
||||
/* Lifelines */
|
||||
svg += '<line x1="150" y1="44" x2="150" y2="500" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<line x1="450" y1="44" x2="450" y2="500" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
|
||||
var y = 70;
|
||||
var arrows = [
|
||||
{ from: 150, to: 450, label: 'Generate Ed25519 keypair', note: '', dir: 'self' },
|
||||
{ from: 150, to: 450, label: 'POST /init { public_key }', note: '', dir: 'right' },
|
||||
{ from: 450, to: 150, label: '{ session_id, salt, opcodes, gene_size, mutation_order }', note: '', dir: 'left' },
|
||||
{ from: 150, to: 450, label: 'Execute VM program', note: '', dir: 'self' },
|
||||
{ from: 150, to: 450, label: 'Collect entropy + sign payload', note: '', dir: 'self' },
|
||||
{ from: 150, to: 450, label: 'Preview gene commitment', note: '', dir: 'self' },
|
||||
{ from: 150, to: 450, label: 'POST /hb { session_id, hash, signature, gene_commitment, … }', note: '', dir: 'right' },
|
||||
{ from: 450, to: 150, label: 'Verify chain + signature + gene + trust', note: '', dir: 'selfr' },
|
||||
{ from: 450, to: 150, label: '{ next_salt, next_mutation_step, next_mutation_order }', note: 'accepted', dir: 'left' },
|
||||
{ from: 450, to: 150, label: '{ status: "ok" }', note: 'rejected (silent)', dir: 'left' },
|
||||
];
|
||||
|
||||
for (var i = 0; i < arrows.length; i++) {
|
||||
var a = arrows[i];
|
||||
if (a.dir === 'right') {
|
||||
svg += '<line x1="155" y1="' + y + '" x2="445" y2="' + y + '" stroke="#7b7e85" stroke-width="1.2"/>';
|
||||
svg += '<polygon points="443,' + (y - 4) + ' 450,' + y + ' 443,' + (y + 4) + '" fill="#7b7e85"/>';
|
||||
svg += '<text x="300" y="' + (y - 8) + '" fill="#e1e1e4" font-size="8.5" text-anchor="middle" font-family="Inter,sans-serif">' + a.label + '</text>';
|
||||
} else if (a.dir === 'left') {
|
||||
svg += '<line x1="445" y1="' + y + '" x2="155" y2="' + y + '" stroke="#7b7e85" stroke-width="1.2"/>';
|
||||
svg += '<polygon points="157,' + (y - 4) + ' 150,' + y + ' 157,' + (y + 4) + '" fill="#7b7e85"/>';
|
||||
svg += '<text x="300" y="' + (y - 8) + '" fill="#e1e1e4" font-size="8.5" text-anchor="middle" font-family="Inter,sans-serif">' + a.label + '</text>';
|
||||
if (a.note) {
|
||||
svg += '<text x="300" y="' + (y + 14) + '" fill="#6b6f7a" font-size="8" text-anchor="middle" font-style="italic" font-family="Inter,sans-serif">' + a.note + '</text>';
|
||||
y += 8;
|
||||
}
|
||||
} else if (a.dir === 'self') {
|
||||
svg += '<rect x="60" y="' + (y - 12) + '" width="180" height="22" rx="4" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="150" y="' + (y + 3) + '" fill="#9a9da6" font-size="8.5" text-anchor="middle" font-family="Inter,sans-serif">' + a.label + '</text>';
|
||||
} else if (a.dir === 'selfr') {
|
||||
svg += '<rect x="360" y="' + (y - 12) + '" width="180" height="22" rx="4" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="450" y="' + (y + 3) + '" fill="#9a9da6" font-size="8.5" text-anchor="middle" font-family="Inter,sans-serif">' + a.label + '</text>';
|
||||
}
|
||||
y += 44;
|
||||
}
|
||||
|
||||
svg += '</svg>';
|
||||
el.innerHTML = svg;
|
||||
}
|
||||
|
||||
/* ---- Auto-init ---- */
|
||||
function init() {
|
||||
drawArchitecture('diagram-architecture');
|
||||
drawProtocolFlow('diagram-protocol');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return {
|
||||
drawArchitecture: drawArchitecture,
|
||||
drawProtocolFlow: drawProtocolFlow
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,41 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Website — pwa.js
|
||||
Progressive Web App: service worker registration + install
|
||||
============================================================ */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/sw.js').then(function (reg) {
|
||||
/* Update found — silent refresh */
|
||||
reg.addEventListener('updatefound', function () {
|
||||
var worker = reg.installing;
|
||||
if (!worker) return;
|
||||
worker.addEventListener('statechange', function () {
|
||||
if (worker.state === 'activated' && navigator.serviceWorker.controller) {
|
||||
/* New version available — user can refresh */
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch(function () {
|
||||
/* SW registration failed — app still works */
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Installable PWA banner (A2HS) ---- */
|
||||
var deferredPrompt = null;
|
||||
|
||||
window.addEventListener('beforeinstallprompt', function (e) {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
/* Could show a custom install banner here */
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', function () {
|
||||
deferredPrompt = null;
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,144 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Website — search.js
|
||||
Client-side full-text search across all pages
|
||||
============================================================ */
|
||||
|
||||
var ChronoSearch = (function () {
|
||||
'use strict';
|
||||
|
||||
/* ---- Search index ---- */
|
||||
var pages = [
|
||||
{ title: 'Home', url: 'index.html', desc: 'ChronoSeal overview, features, and quick start', keywords: 'home overview features quick start install' },
|
||||
{ title: 'Architecture', url: 'architecture.html', desc: 'System architecture, crate layout, and data flow', keywords: 'architecture crates workspace wasm shared server' },
|
||||
{ title: 'Protocol', url: 'protocol.html', desc: 'Heartbeat protocol, hash chain, and mutation engine', keywords: 'protocol heartbeat hash chain blake3 ed25519 mutation gene' },
|
||||
{ title: 'API Reference', url: 'api.html', desc: 'HTTP endpoints: /init, /hb, /health, /metrics, /stats', keywords: 'api http post init heartbeat health metrics stats json' },
|
||||
{ title: 'Deployment', url: 'deployment.html', desc: 'Installation, systemd, Docker, nginx reverse proxy', keywords: 'deploy install systemd docker nginx proxy production' },
|
||||
{ title: 'Threat Model', url: 'threat-model.html', desc: 'Security threat model and defense boundaries', keywords: 'threat model security attack replay automation defense' },
|
||||
{ title: 'Performance', url: 'performance.html', desc: 'Performance tuning, benchmarks, and optimization', keywords: 'performance tuning benchmark latency throughput' },
|
||||
{ title: 'Philosophy', url: 'philosophy.html', desc: 'Design philosophy and engineering priorities', keywords: 'philosophy design unix privacy operator control' },
|
||||
{ title: 'Privacy Policy', url: 'privacy.html', desc: 'Data handling, storage, and privacy commitments', keywords: 'privacy policy data collection storage session' },
|
||||
{ title: 'Security', url: 'security.html', desc: 'Security assumptions, hardening, and response headers', keywords: 'security assumptions headers hardening csp' },
|
||||
{ title: 'Operations', url: 'operations.html', desc: 'Monitoring, health checks, metrics, and logging', keywords: 'operations health status metrics stats logging monitor' },
|
||||
{ title: 'Testing', url: 'testing.html', desc: 'Test suite, fuzzing, and validation coverage', keywords: 'testing tests cargo fuzz validation fingerprint' },
|
||||
{ title: 'Comparison', url: 'comparison.html', desc: 'ChronoSeal vs commercial anti-bot solutions', keywords: 'comparison cloudflare akamai recaptcha datadome kasada' },
|
||||
];
|
||||
|
||||
var overlay = null;
|
||||
var input = null;
|
||||
var results = null;
|
||||
|
||||
function init() {
|
||||
overlay = document.querySelector('.search-overlay');
|
||||
input = document.querySelector('.search-input');
|
||||
results = document.querySelector('.search-results');
|
||||
if (!overlay || !input || !results) return;
|
||||
|
||||
input.addEventListener('input', debounce(onInput, 150));
|
||||
overlay.addEventListener('click', function (e) {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
|
||||
// Keyboard nav inside results
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
navigateResults(e.key === 'ArrowDown' ? 1 : -1);
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
var sel = results.querySelector('.search-result.selected');
|
||||
if (sel) { window.location.href = sel.getAttribute('href'); close(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!overlay) init();
|
||||
if (!overlay) return;
|
||||
overlay.classList.add('open');
|
||||
input.value = '';
|
||||
results.innerHTML = renderHints();
|
||||
setTimeout(function () { input.focus(); }, 100);
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (overlay) overlay.classList.remove('open');
|
||||
}
|
||||
|
||||
function onInput() {
|
||||
var q = input.value.trim().toLowerCase();
|
||||
if (!q) { results.innerHTML = renderHints(); return; }
|
||||
|
||||
var matches = [];
|
||||
for (var i = 0; i < pages.length; i++) {
|
||||
var p = pages[i];
|
||||
var hay = (p.title + ' ' + p.desc + ' ' + p.keywords).toLowerCase();
|
||||
if (hay.indexOf(q) !== -1) {
|
||||
matches.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
results.innerHTML = '<div class="search-empty">No results for “' + escHtml(q) + '”</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
for (var j = 0; j < matches.length; j++) {
|
||||
var m = matches[j];
|
||||
html += '<a class="search-result' + (j === 0 ? ' selected' : '') + '" href="' + m.url + '">';
|
||||
html += '<div class="search-result-title">' + highlightMatch(m.title, q) + '</div>';
|
||||
html += '<div class="search-result-desc">' + highlightMatch(m.desc, q) + '</div>';
|
||||
html += '</a>';
|
||||
}
|
||||
results.innerHTML = html;
|
||||
}
|
||||
|
||||
function navigateResults(dir) {
|
||||
var items = results.querySelectorAll('.search-result');
|
||||
if (!items.length) return;
|
||||
var idx = -1;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].classList.contains('selected')) { idx = i; break; }
|
||||
}
|
||||
if (idx >= 0) items[idx].classList.remove('selected');
|
||||
idx = Math.max(0, Math.min(items.length - 1, idx + dir));
|
||||
items[idx].classList.add('selected');
|
||||
items[idx].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
function renderHints() {
|
||||
return '<div class="search-empty">Type to search documentation…<br><span style="font-size:.78rem;color:var(--text-muted)">↑↓ Navigate · ↵ Open · Esc Close</span></div>';
|
||||
}
|
||||
|
||||
function highlightMatch(text, q) {
|
||||
var idx = text.toLowerCase().indexOf(q);
|
||||
if (idx === -1) return escHtml(text);
|
||||
return escHtml(text.substring(0, idx)) +
|
||||
'<mark style="background:rgba(176,178,184,.2);color:var(--text-primary);border-radius:2px;padding:0 2px">' +
|
||||
escHtml(text.substring(idx, idx + q.length)) + '</mark>' +
|
||||
escHtml(text.substring(idx + q.length));
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(s));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
var t;
|
||||
return function () {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(fn, ms);
|
||||
};
|
||||
}
|
||||
|
||||
/* ---- Auto-init ---- */
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return { open: open, close: close };
|
||||
})();
|
||||
Reference in new issue
Block a user