OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / lib / nav-ui.js

152 lines · javascript ref:
/**
 * 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }

  // 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>`
}