fix: use fixed-position overlay for FAB session input (mobile visibility)

This commit is contained in:
Brian Krabach
2026-03-30 01:45:51 -07:00
parent dbb8b2bd73
commit 9978544dc1
3 changed files with 136 additions and 2 deletions
+47 -1
View File
@@ -1324,6 +1324,51 @@ function showNewSessionInput(btn) {
input.focus(); input.focus();
} }
/**
* Show a fixed-position input overlay for creating a new session from the mobile FAB.
* Unlike showNewSessionInput (which inserts inline into btn.parentNode), this renders
* a fixed-position overlay appended directly to document.body — ensuring it is always
* visible on mobile regardless of body/view overflow:hidden constraints.
*/
function showFabSessionInput() {
const fab = $('new-session-fab');
const overlay = document.createElement('div');
overlay.className = 'fab-input-overlay';
const input = document.createElement('input');
input.type = 'text';
input.className = 'new-session-input';
input.placeholder = 'Session name\u2026';
input.autocomplete = 'off';
input.spellcheck = false;
overlay.appendChild(input);
function cleanup() {
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
if (fab) fab.style.display = '';
}
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
const name = input.value.trim();
cleanup();
if (name) createNewSession(name);
} else if (e.key === 'Escape') {
cleanup();
}
});
input.addEventListener('blur', function() {
setTimeout(cleanup, 150);
});
if (fab) fab.style.display = 'none';
document.body.appendChild(overlay);
input.focus();
}
/** /**
* Create a new tmux session via POST /api/sessions. * Create a new tmux session via POST /api/sessions.
* Shows a toast with the created session name, calls pollSessions() to refresh, * Shows a toast with the created session name, calls pollSessions() to refresh,
@@ -1357,7 +1402,7 @@ function bindStaticEventListeners() {
var sidebarNewSessionBtn = $('sidebar-new-session-btn'); var sidebarNewSessionBtn = $('sidebar-new-session-btn');
if (sidebarNewSessionBtn) on(sidebarNewSessionBtn, 'click', function() { showNewSessionInput(sidebarNewSessionBtn); }); if (sidebarNewSessionBtn) on(sidebarNewSessionBtn, 'click', function() { showNewSessionInput(sidebarNewSessionBtn); });
var newSessionFab = $('new-session-fab'); var newSessionFab = $('new-session-fab');
if (newSessionFab) on(newSessionFab, 'click', function() { showNewSessionInput(newSessionFab); }); if (newSessionFab) on(newSessionFab, 'click', showFabSessionInput);
on($('sidebar-toggle-btn'), 'click', toggleSidebar); on($('sidebar-toggle-btn'), 'click', toggleSidebar);
on($('sidebar-collapse-btn'), 'click', toggleSidebar); on($('sidebar-collapse-btn'), 'click', toggleSidebar);
bindSidebarClickAway(); bindSidebarClickAway();
@@ -1583,6 +1628,7 @@ if (typeof module !== 'undefined' && module.exports) {
patchServerSetting, patchServerSetting,
// Header + button with inline name input // Header + button with inline name input
showNewSessionInput, showNewSessionInput,
showFabSessionInput,
createNewSession, createNewSession,
// Test-only helpers // Test-only helpers
_setCurrentSessions, _setCurrentSessions,
+19 -1
View File
@@ -821,6 +821,10 @@ body {
.session-sidebar { .session-sidebar {
transition: none; transition: none;
} }
.new-session-fab:active {
transform: none;
}
} }
/* ============================================================ /* ============================================================
@@ -1222,13 +1226,27 @@ body {
cursor: pointer; cursor: pointer;
z-index: 100; z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
transition: transform 150ms ease, box-shadow 150ms ease; transition: transform 150ms ease;
} }
.new-session-fab:active { .new-session-fab:active {
transform: scale(0.95); transform: scale(0.95);
} }
.new-session-fab:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Fixed-position input overlay triggered by mobile FAB —
appended to body so position:fixed escapes overflow:hidden on views */
.fab-input-overlay {
position: fixed;
bottom: 80px;
right: 16px;
z-index: 200;
}
/* ============================================================ /* ============================================================
Responsive overlay sidebar at <960px Responsive overlay sidebar at <960px
============================================================ */ ============================================================ */
+70
View File
@@ -1858,3 +1858,73 @@ def test_js_fab_exported() -> None:
assert "showNewSessionInput" in _JS, ( assert "showNewSessionInput" in _JS, (
"showNewSessionInput must exist in app.js (used by FAB click handler)" "showNewSessionInput must exist in app.js (used by FAB click handler)"
) )
# ============================================================
# Mobile FAB — dedicated fixed-position input (quality fix)
# ============================================================
def test_js_show_fab_session_input_exists() -> None:
"""showFabSessionInput function must exist in app.js (dedicated FAB input overlay)."""
assert "function showFabSessionInput" in _JS, (
"showFabSessionInput must be defined in app.js — "
"FAB needs a fixed-position overlay, not inline body insertion"
)
def test_js_show_fab_session_input_appends_to_body() -> None:
"""showFabSessionInput must append the overlay to document.body (fixed positioning)."""
match = re.search(
r"function showFabSessionInput\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
_JS,
re.DOTALL,
)
assert match, "showFabSessionInput function not found"
body = match.group(1)
assert "document.body" in body, (
"showFabSessionInput must append to document.body "
"(fixed-position overlays must be direct body children to escape overflow:hidden)"
)
def test_js_show_fab_session_input_handles_enter() -> None:
"""showFabSessionInput must call createNewSession on Enter key."""
match = re.search(
r"function showFabSessionInput\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
_JS,
re.DOTALL,
)
assert match, "showFabSessionInput function not found"
body = match.group(1)
assert "Enter" in body, "showFabSessionInput must handle Enter key"
assert "createNewSession" in body, (
"showFabSessionInput must call createNewSession on Enter"
)
def test_js_show_fab_session_input_handles_escape() -> None:
"""showFabSessionInput must handle Escape key to cancel."""
match = re.search(
r"function showFabSessionInput\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
_JS,
re.DOTALL,
)
assert match, "showFabSessionInput function not found"
body = match.group(1)
assert "Escape" in body, "showFabSessionInput must handle Escape key"
def test_js_fab_click_calls_show_fab_session_input() -> None:
"""bindStaticEventListeners must bind FAB click to showFabSessionInput (not inline body insert)."""
match = re.search(
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
_JS,
re.DOTALL,
)
assert match, "bindStaticEventListeners function not found"
body = match.group(1)
assert "showFabSessionInput" in body, (
"bindStaticEventListeners must call showFabSessionInput for FAB click — "
"the old showNewSessionInput(fab) inserts into body which is clipped by overflow:hidden"
)