feat(web): shared left-sidebar navigation across all pages

Desktop nav was scattered and inconsistent — the chat header was crammed with
cross-page links and each standalone page had its own ad-hoc, incomplete back-links
(e.g. /hands could only reach Chat). Now a single nav.js (one source of truth, no
build step) injects a left sidebar on desktop (>=769px) with active-page
highlighting across Chat/Session/History/Hands/Mind/Journal/Logs + Settings.

- nav.js: injects sidebar + its own CSS; body gets padding-left on desktop; hidden
  on mobile (each page keeps its bottom bar / back-links there).
- Included on every page (index, session, history, hands, self, journal, logs,
  recap, hand).
- Decluttered the chat header: removed the now-redundant cross-page links (kept the
  chat-specific session selector + inline Live Log toggle).
- Sidebar Settings opens the chat modal, or navigates to /?settings=1 from elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 05:27:55 +00:00
parent 559faaed30
commit f2de7dec61
10 changed files with 88 additions and 5 deletions
+76
View File
@@ -0,0 +1,76 @@
/* Shared app navigation — one source of truth across all pages (no build step).
Injects a left sidebar on desktop (>=769px) with active-page highlighting; stays
out of the way on mobile, where each page keeps its bottom bar / back-links. */
(function () {
const ITEMS = [
{ href: "/", icon: "💬", label: "Chat" },
{ href: "/session", icon: "♠", label: "Session" },
{ href: "/history", icon: "📚", label: "History" },
{ href: "/hands", icon: "🃏", label: "Hands" },
{ href: "/self", icon: "🧠", label: "Mind" },
{ href: "/journal", icon: "📔", label: "Journal" },
{ href: "/logs", icon: "📜", label: "Logs" },
];
const path = location.pathname;
function isActive(href) {
if (href === "/") return path === "/" || path === "";
if (href === "/hands") return path === "/hands" || path.indexOf("/hand") === 0;
if (href === "/history") return path.indexOf("/history") === 0 || path.indexOf("/recap") === 0;
return path === href || path.indexOf(href + "/") === 0;
}
const css = `
#app-nav { display: none; }
@media screen and (min-width: 769px) {
body { padding-left: 212px; }
#app-nav {
position: fixed; left: 0; top: 0; bottom: 0; width: 212px; z-index: 1000;
display: flex; flex-direction: column; gap: 2px; box-sizing: border-box;
padding: 14px 10px; background: #0b0b0b; border-right: 1px solid #2a1d12;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#app-nav .brand {
display: flex; align-items: center; gap: 8px; text-decoration: none;
color: #ff7a00; font-weight: 700; font-size: 1.15rem; letter-spacing: .5px;
padding: 6px 11px 14px;
}
#app-nav .brand .dot { width: 8px; height: 8px; border-radius: 50%;
background: #8fd694; box-shadow: 0 0 8px rgba(143,214,148,.6); }
#app-nav .navitem {
display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
padding: 9px 11px; border-radius: 9px; border: none; background: none;
color: #cfcfcf; text-decoration: none; font-size: .95rem; cursor: pointer;
font-family: inherit; -webkit-tap-highlight-color: transparent;
}
#app-nav .navitem .i { font-size: 1.05rem; width: 20px; text-align: center; filter: grayscale(.3); }
#app-nav .navitem:hover { background: rgba(255,122,0,.08); color: #fff; }
#app-nav .navitem.active { background: rgba(255,122,0,.14); color: #ff7a00; }
#app-nav .navitem.active .i { filter: none; }
#app-nav .spacer { flex: 1; }
}`;
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
const nav = document.createElement("nav");
nav.id = "app-nav";
nav.setAttribute("aria-label", "App navigation");
nav.innerHTML =
'<a class="brand" href="/"><span class="dot"></span> Lyra</a>' +
ITEMS.map(function (it) {
return '<a class="navitem' + (isActive(it.href) ? " active" : "") + '" href="' + it.href + '">' +
'<span class="i">' + it.icon + '</span><span class="l">' + it.label + "</span></a>";
}).join("") +
'<div class="spacer"></div>' +
'<button class="navitem" id="navSettings" type="button"><span class="i">⚙</span><span class="l">Settings</span></button>';
document.body.insertBefore(nav, document.body.firstChild);
// Settings opens the chat-page modal; from other pages, jump to chat and open it.
nav.querySelector("#navSettings").addEventListener("click", function () {
const btn = document.getElementById("settingsBtn");
if (btn) btn.click();
else location.href = "/?settings=1";
});
})();