/**
* Navigation UI — shared nav bar for all server-rendered pages
*
* Returns HTML string with embedded styles and search script.
* Following pear-git's lib/nav-ui.js pattern.
*/
/**
* Render the navigation bar
*
* @param {string} pathPrefix - URL path prefix (e.g., '/hypernote')
* @param {string} activePage - Current page identifier for highlighting
* @returns {string} HTML string
*/
export function renderNav (pathPrefix = '', activePage = '') {
const base = pathPrefix || ''
function navLink (href, label, page) {
const active = activePage === page ? ' nav-active' : ''
return `<a href="${base}${href}" class="nav-link${active}">${label}</a>`
}
return `<nav class="hn-nav">
<div class="hn-nav-inner">
<a href="${base}/" class="hn-nav-brand">Hypernote</a>
<div class="hn-nav-search">
<input type="search" id="hn-search-input" placeholder="Search notes..." autocomplete="off">
<div class="hn-search-dropdown" id="hn-search-dropdown" style="display:none"></div>
</div>
<div class="hn-nav-links">
${navLink('/', 'Home', 'home')}
${navLink('/profile', 'Profile', 'profile')}
<a href="${base}/admin" class="nav-link${activePage === 'admin' ? ' nav-active' : ''}" id="hn-nav-admin" style="display:none">Admin</a>
</div>
</div>
</nav>
<style>
.hn-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 9000;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
height: 56px;
}
.hn-nav-inner {
max-width: 1200px; margin: 0 auto; height: 100%;
display: flex; align-items: center; padding: 0 20px; gap: 16px;
}
.hn-nav-brand {
font-weight: 700; font-size: 1.1rem; color: var(--text-primary, #c9d1d9);
text-decoration: none; white-space: nowrap;
}
.hn-nav-brand:hover { color: var(--accent); text-decoration: none; }
.hn-nav-search { position: relative; flex: 1; max-width: 400px; }
.hn-nav-search input {
width: 100%; padding: 6px 12px; font-size: 0.85rem;
background: var(--bg-primary, #0d1117); color: var(--text-primary, #c9d1d9);
border: 1px solid var(--border); border-radius: 6px;
}
.hn-nav-search input:focus { border-color: var(--accent); outline: none; }
.hn-search-dropdown {
position: absolute; top: 100%; left: 0; right: 0;
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: 6px; margin-top: 4px; max-height: 400px; overflow-y: auto; z-index: 9001;
}
.hn-search-result {
display: block; padding: 10px 14px; border-bottom: 1px solid var(--border);
text-decoration: none; color: var(--text-primary, #c9d1d9);
}
.hn-search-result:hover { background: var(--bg-hover); text-decoration: none; }
.hn-search-result:last-child { border-bottom: none; }
.hn-search-result .title { font-weight: 500; font-size: 0.9rem; }
.hn-search-result .context { font-size: 0.8rem; color: var(--text-secondary, #8b949e); }
.hn-search-result .snippet { font-size: 0.8rem; color: var(--text-secondary, #8b949e); margin-top: 2px; }
.hn-nav-links { display: flex; gap: 4px; margin-left: auto; }
.nav-link {
padding: 6px 12px; font-size: 0.85rem; color: var(--text-secondary, #8b949e);
text-decoration: none; border-radius: 6px; white-space: nowrap;
}
.nav-link:hover { color: var(--text-primary, #c9d1d9); background: var(--bg-hover); text-decoration: none; }
.nav-active { color: var(--text-primary, #c9d1d9); border-bottom: 2px solid var(--accent); }
@media (max-width: 640px) {
.hn-nav-search { max-width: 200px; }
.nav-link { padding: 6px 8px; font-size: 0.8rem; }
}
</style>
<script>
(function () {
var basePath = ${JSON.stringify(base)};
var searchInput = document.getElementById('hn-search-input');
var dropdown = document.getElementById('hn-search-dropdown');
var timer = null;
if (searchInput) {
searchInput.addEventListener('input', function () {
clearTimeout(timer);
var q = searchInput.value.trim();
if (!q) { dropdown.style.display = 'none'; return; }
timer = setTimeout(function () { doSearch(q); }, 200);
});
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { dropdown.style.display = 'none'; searchInput.blur(); }
});
}
document.addEventListener('click', function (e) {
if (!e.target.closest('.hn-nav-search')) dropdown.style.display = 'none';
});
async function doSearch (q) {
try {
var res = await window.fetch(basePath + '/api/search?q=' + encodeURIComponent(q));
var data = await res.json();
if (!data.results || data.results.length === 0) {
dropdown.innerHTML = '<div style="padding:12px;color:var(--text-muted)">No results</div>';
dropdown.style.display = 'block';
return;
}
dropdown.innerHTML = data.results.slice(0, 10).map(function (r) {
var href = basePath + '/notebooks/' + encodeURIComponent(r.notebook) + '/notes/' + encodeURIComponent(r.slug || r.id);
var snippet = (r.snippet || '').substring(0, 100);
return '<a class="hn-search-result" href="' + href + '">'
+ '<div class="title">' + esc(r.title || 'Untitled') + '</div>'
+ '<div class="context">' + esc(r.notebook) + '</div>'
+ (snippet ? '<div class="snippet">' + esc(snippet) + '</div>' : '')
+ '</a>';
}).join('');
dropdown.style.display = 'block';
} catch (e) {
dropdown.style.display = 'none';
}
}
function esc (s) { return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
// Show admin link if user has admin permission
if (window.identityReady) {
window.identityReady.then(function () {
window.fetch(basePath + '/api/whoami').then(function (r) { return r.json(); }).then(function (data) {
if (data.authenticated && data.permissions && data.permissions.admin) {
var el = document.getElementById('hn-nav-admin');
if (el) el.style.display = '';
}
}).catch(function () {});
});
}
})();
</script>`
}