feat(web): bottom tab bar navigation (M4)

- Mobile bottom tab bar: Chat · Hands · Mind · More. "More" opens the drawer
  for the long tail (Journal, Log, Settings, sessions); hamburger retired.
- Auto-hides while the keyboard is open (body.kb) so the input pins to the
  keyboard; mobile-only (desktop keeps its header nav).
- Removed now-redundant Mind/Hands from the drawer + their listeners.
- Bottom-fill fix: #chat uses 100dvh (the visible viewport) — 100vh/inset:0
  reach into the home-indicator zone iOS won't comfortably show, clipping the
  bar; dvh/svh exclude it. Tab bar is flex:none with a small fixed bottom
  padding (safe-area padding double-counts at dvh height), and the body bg
  matches the bar so any strip below #chat is seamless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 04:36:08 +00:00
parent 5dc3fa17d7
commit 50f460eeb2
2 changed files with 65 additions and 17 deletions
+11 -8
View File
@@ -41,9 +41,7 @@
<h4>Actions</h4> <h4>Actions</h4>
<button id="mobileThinkingStreamBtn">📜 Live Log (inline)</button> <button id="mobileThinkingStreamBtn">📜 Live Log (inline)</button>
<button id="mobileFullLogBtn">⛶ Full Log</button> <button id="mobileFullLogBtn">⛶ Full Log</button>
<button id="mobileMindBtn">🧠 Read Her Mind</button>
<button id="mobileJournalBtn">📔 Journal</button> <button id="mobileJournalBtn">📔 Journal</button>
<button id="mobileHandsBtn">🃏 Hands</button>
<button id="mobileSettingsBtn">⚙ Settings</button> <button id="mobileSettingsBtn">⚙ Settings</button>
<button id="mobileToggleThemeBtn">🌙 Toggle Theme</button> <button id="mobileToggleThemeBtn">🌙 Toggle Theme</button>
<button id="mobileForceReloadBtn">🔄 Force Reload</button> <button id="mobileForceReloadBtn">🔄 Force Reload</button>
@@ -116,6 +114,14 @@
<input id="userInput" type="text" placeholder="Type a message..." autofocus /> <input id="userInput" type="text" placeholder="Type a message..." autofocus />
<button id="sendBtn">Send</button> <button id="sendBtn">Send</button>
</div> </div>
<!-- Bottom tab bar (mobile only; hides while the keyboard is open) -->
<nav id="tabbar" aria-label="Primary navigation">
<a class="tab active" href="/" aria-current="page"><span class="ti">💬</span><span class="tl">Chat</span></a>
<a class="tab" href="/hands"><span class="ti">🃏</span><span class="tl">Hands</span></a>
<a class="tab" href="/self"><span class="ti">🧠</span><span class="tl">Mind</span></a>
<button class="tab" id="moreTab" type="button"><span class="ti"></span><span class="tl">More</span></button>
</nav>
</div> </div>
<!-- Settings Modal (outside chat container) --> <!-- Settings Modal (outside chat container) -->
@@ -523,6 +529,8 @@
// iOS pans the visual viewport when the keyboard opens; follow its top // iOS pans the visual viewport when the keyboard opens; follow its top
// edge so the pinned #chat sits exactly in the visible area. // edge so the pinned #chat sits exactly in the visible area.
root.setProperty("--app-offset", off + "px"); root.setProperty("--app-offset", off + "px");
// Keyboard open ⇒ hide the bottom tab bar so the input pins to the keyboard.
document.body.classList.toggle("kb", (window.innerHeight - h) > 150);
} }
// Re-measure across the keyboard animation: iOS reports a stale (too-short) // Re-measure across the keyboard animation: iOS reports a stale (too-short)
// height mid-animation, so sample a few times until it settles. // height mid-animation, so sample a few times until it settles.
@@ -568,6 +576,7 @@
hamburgerMenu.addEventListener("click", toggleMobileMenu); hamburgerMenu.addEventListener("click", toggleMobileMenu);
mobileMenuOverlay.addEventListener("click", closeMobileMenu); mobileMenuOverlay.addEventListener("click", closeMobileMenu);
document.getElementById("moreTab").addEventListener("click", toggleMobileMenu);
// Sync mobile menu controls with desktop // Sync mobile menu controls with desktop
const mobileMode = document.getElementById("mobileMode"); const mobileMode = document.getElementById("mobileMode");
@@ -1011,15 +1020,9 @@
document.getElementById("mobileFullLogBtn").addEventListener("click", () => { document.getElementById("mobileFullLogBtn").addEventListener("click", () => {
closeMobileMenu(); window.location.href = "/logs"; closeMobileMenu(); window.location.href = "/logs";
}); });
document.getElementById("mobileMindBtn").addEventListener("click", () => {
closeMobileMenu(); window.location.href = "/self";
});
document.getElementById("mobileJournalBtn").addEventListener("click", () => { document.getElementById("mobileJournalBtn").addEventListener("click", () => {
closeMobileMenu(); window.location.href = "/journal"; closeMobileMenu(); window.location.href = "/journal";
}); });
document.getElementById("mobileHandsBtn").addEventListener("click", () => {
closeMobileMenu(); window.location.href = "/hands";
});
// Connect to the global live log on page load. // Connect to the global live log on page load.
connectThinkingStream(); connectThinkingStream();
+54 -9
View File
@@ -677,6 +677,9 @@ select:hover {
/* Wordmark + status dot — shown only in the mobile header (media query below) */ /* Wordmark + status dot — shown only in the mobile header (media query below) */
.brand, .brand-dot { display: none; } .brand, .brand-dot { display: none; }
/* Bottom tab bar — mobile only (shown in the media query) */
#tabbar { display: none; }
/* Hamburger Menu */ /* Hamburger Menu */
.hamburger-menu { .hamburger-menu {
display: none; display: none;
@@ -784,21 +787,26 @@ select:hover {
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
body { body {
padding: 0; padding: 0;
background: var(--bg-elev); /* matches the tab bar so any strip below #chat is seamless */
} }
#chat { #chat {
position: fixed; position: fixed;
top: 0; top: 0; left: 0; right: 0;
left: 0;
right: 0;
width: 100%; width: 100%;
/* Height follows the *visible* viewport (above the keyboard); offsetTop height: 100dvh; /* the *visible* viewport (excludes the home-indicator zone);
shift is applied via JS transform so it tracks iOS's viewport panning. */ overrides the base 95vh. Body bg matches the bar below it. */
height: var(--app-height, 100dvh); background: var(--bg-dark);
transform: translateY(var(--app-offset, 0px));
border-radius: 0; border-radius: 0;
border: none; border: none;
} }
/* Only while the keyboard is open do we follow the *visible* viewport: release
the bottom anchor and size from the top by the measured visible height. */
body.kb #chat {
bottom: auto;
height: var(--app-height, 100dvh);
transform: translateY(var(--app-offset, 0px));
}
/* Show hamburger, hide desktop header controls */ /* Show hamburger, hide desktop header controls */
.hamburger-menu { .hamburger-menu {
@@ -857,14 +865,51 @@ select:hover {
font-size: 0.85rem; font-size: 0.85rem;
} }
/* Input area - bigger touch targets */ /* Input area - bigger touch targets. The tab bar owns the bottom safe-area
inset now (the input is no longer the bottom-most element). */
#input { #input {
padding: 12px; padding: 12px;
padding-bottom: calc(12px + env(safe-area-inset-bottom));
padding-left: calc(12px + env(safe-area-inset-left)); padding-left: calc(12px + env(safe-area-inset-left));
padding-right: calc(12px + env(safe-area-inset-right)); padding-right: calc(12px + env(safe-area-inset-right));
} }
/* Bottom tab bar */
#tabbar {
display: flex;
flex: none; /* never let it be compressed/clipped by the flex column */
border-top: 1px solid var(--border);
background: var(--bg-elev);
padding-bottom: 6px; /* 100dvh already excludes the home-indicator zone */
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
#tabbar .tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
padding: 7px 0 5px;
background: none;
border: none;
border-radius: 0;
color: var(--text-fade);
font-family: var(--font-console);
text-decoration: none;
-webkit-tap-highlight-color: transparent;
}
#tabbar .tab:hover { background: none; }
#tabbar .tab:active { background: var(--accent-soft); }
#tabbar .tab .ti { font-size: 1.3rem; line-height: 1; filter: grayscale(.45); }
#tabbar .tab .tl { font-size: .64rem; letter-spacing: .3px; }
#tabbar .tab.active { color: var(--accent); }
#tabbar .tab.active .ti { filter: none; }
body.kb #tabbar { display: none; } /* keyboard open ⇒ hide so input pins to keyboard */
/* The "More" tab is the menu trigger now — retire the hamburger. */
.hamburger-menu { display: none !important; }
#userInput { #userInput {
font-size: 16px; /* Prevents zoom on iOS */ font-size: 16px; /* Prevents zoom on iOS */
padding: 12px; padding: 12px;