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"
+ )