diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 4cad1de..a10db36 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -1622,6 +1622,81 @@ function closeFlyoutMenu() { _flyoutRemoteId = null; } +/** + * Open a bottom action sheet for the flyout menu (mobile). + * Same actions as the desktop flyout, but renders as a full-width bottom sheet. + */ +function _openFlyoutSheet() { + var viewType = _activeView; + if (viewType !== 'all' && viewType !== 'hidden') viewType = 'user'; + + var items = FLYOUT_MENU_MAP[viewType] || FLYOUT_MENU_MAP['all']; + + var html = '
'; + html += '
'; + html += ''; + + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item.separator) { + html += '
'; + continue; + } + + var label = item.label; + if (label.indexOf('{viewName}') !== -1) { + var displayName = _activeView; + if (displayName.length > 20) displayName = displayName.substring(0, 20) + '\u2026'; + label = label.replace('{viewName}', escapeHtml(displayName)); + } + + var cls = 'flyout-sheet__item'; + if (item.className && item.className.indexOf('danger') !== -1) cls += ' flyout-sheet__item--danger'; + + html += ''; + } + + html += '
'; + + var sheet = document.createElement('div'); + sheet.className = 'flyout-sheet'; + sheet.setAttribute('role', 'dialog'); + sheet.setAttribute('aria-modal', 'true'); + sheet.innerHTML = html; + document.body.appendChild(sheet); + + // Backdrop closes + var backdrop = sheet.querySelector('.flyout-sheet__backdrop'); + if (backdrop) { + backdrop.addEventListener('click', closeFlyoutMenu); + } + + // Delegated action handler + var panel = sheet.querySelector('.flyout-sheet__panel'); + if (panel) { + panel.addEventListener('click', function(e) { + var btn = e.target.closest('[data-action]'); + if (!btn) return; + + var action = btn.dataset.action; + if (action === 'add-to-view' || action === 'unhide-add-to-view') { + // On mobile, close sheet and open Add Sessions panel + closeFlyoutMenu(); + openAddSessionsPanel(); + } else if (action === 'kill') { + // Simple confirm on mobile (inline doesn't work well in sheets) + closeFlyoutMenu(); + killSession(_flyoutSessionName, _flyoutRemoteId); + } else { + // Dispatch directly + _handleFlyoutClick(e); + } + }); + } +} + /** * Click-outside handler for the flyout menu. * @param {MouseEvent} e diff --git a/muxplex/tests/test_frontend_js.py b/muxplex/tests/test_frontend_js.py index f5dedbd..51fdb28 100644 --- a/muxplex/tests/test_frontend_js.py +++ b/muxplex/tests/test_frontend_js.py @@ -3924,3 +3924,21 @@ def test_add_sessions_shows_device_name() -> None: assert "deviceName" in fn_body or "device" in fn_body, ( "renderAddSessionsList must show device name for disambiguation" ) + + +# ─── Task 10: Mobile variants ──────────────────────────────────────────────── + + +def test_open_flyout_sheet_function_exists() -> None: + """app.js must define a _openFlyoutSheet function for mobile.""" + assert "function _openFlyoutSheet" in _JS, ( + "app.js must contain a _openFlyoutSheet function for mobile bottom sheet" + ) + + +def test_open_flyout_menu_checks_mobile() -> None: + """openFlyoutMenu must check isMobile() to decide between flyout and sheet.""" + fn_body = _JS.split("function openFlyoutMenu")[1].split("\nfunction ")[0] + assert "isMobile" in fn_body, ( + "openFlyoutMenu must check isMobile() to branch between flyout and sheet" + )