diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 78151b6..302b419 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -1346,6 +1346,8 @@ function bindStaticEventListeners() { on($('back-btn'), 'click', closeSession); var newSessionBtn = $('new-session-btn'); if (newSessionBtn) on(newSessionBtn, 'click', function() { showNewSessionInput(newSessionBtn); }); + var sidebarNewSessionBtn = $('sidebar-new-session-btn'); + if (sidebarNewSessionBtn) on(sidebarNewSessionBtn, 'click', function() { showNewSessionInput(sidebarNewSessionBtn); }); on($('sidebar-toggle-btn'), 'click', toggleSidebar); on($('sidebar-collapse-btn'), 'click', toggleSidebar); bindSidebarClickAway(); diff --git a/muxplex/frontend/index.html b/muxplex/frontend/index.html index 9b28013..f5cf808 100644 --- a/muxplex/frontend/index.html +++ b/muxplex/frontend/index.html @@ -45,6 +45,9 @@ +
diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index 75cb162..99ec2cc 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -1174,6 +1174,33 @@ body { color: var(--text-dim); } +/* ============================================================ + Sidebar sticky footer (+ New button) + ============================================================ */ + +.sidebar-footer { + padding: 8px; + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; +} + +.sidebar-new-btn { + width: 100%; + background: none; + border: 1px dashed var(--border); + border-radius: 4px; + color: var(--text-dim); + font-size: 12px; + font-family: var(--font-ui); + padding: 8px; + cursor: pointer; +} + +.sidebar-new-btn:hover { + color: var(--text-muted); + border-color: var(--text-muted); +} + /* ============================================================ Responsive overlay sidebar at <960px ============================================================ */ diff --git a/muxplex/tests/test_frontend_css.py b/muxplex/tests/test_frontend_css.py index 05f34ee..96984ca 100644 --- a/muxplex/tests/test_frontend_css.py +++ b/muxplex/tests/test_frontend_css.py @@ -1390,3 +1390,101 @@ def test_css_new_session_input_placeholder_color() -> None: assert "color" in body and ("text-dim" in body or "--text-dim" in body), ( ".new-session-input::placeholder must have color: var(--text-dim)" ) + + +# ============================================================ +# Sidebar footer (task-5-sidebar-new-footer) +# ============================================================ + + +def test_css_sidebar_footer_rule_exists() -> None: + """.sidebar-footer rule must exist in style.css.""" + css = read_css() + assert ".sidebar-footer" in css, "Missing .sidebar-footer rule in style.css" + + +def test_css_sidebar_footer_padding() -> None: + """.sidebar-footer must have padding: 8px.""" + import re + css = read_css() + match = re.search( + r"\.sidebar-footer\s*\{([^}]*)\}", + css, + re.DOTALL, + ) + assert match, ".sidebar-footer rule not found" + body = match.group(1) + assert "padding" in body, ".sidebar-footer must have padding property" + assert "8px" in body, ".sidebar-footer padding must include 8px" + + +def test_css_sidebar_footer_border_top() -> None: + """.sidebar-footer must have border-top.""" + import re + css = read_css() + match = re.search( + r"\.sidebar-footer\s*\{([^}]*)\}", + css, + re.DOTALL, + ) + assert match, ".sidebar-footer rule not found" + body = match.group(1) + assert "border-top" in body, ".sidebar-footer must have border-top property" + + +def test_css_sidebar_footer_flex_shrink_0() -> None: + """.sidebar-footer must have flex-shrink: 0.""" + import re + css = read_css() + match = re.search( + r"\.sidebar-footer\s*\{([^}]*)\}", + css, + re.DOTALL, + ) + assert match, ".sidebar-footer rule not found" + body = match.group(1) + assert "flex-shrink" in body, ".sidebar-footer must have flex-shrink property" + assert "0" in body, ".sidebar-footer flex-shrink must be 0" + + +def test_css_sidebar_new_btn_rule_exists() -> None: + """.sidebar-new-btn rule must exist in style.css.""" + css = read_css() + assert ".sidebar-new-btn" in css, "Missing .sidebar-new-btn rule in style.css" + + +def test_css_sidebar_new_btn_width_100() -> None: + """.sidebar-new-btn must have width: 100%.""" + import re + css = read_css() + match = re.search( + r"\.sidebar-new-btn\s*\{([^}]*)\}", + css, + re.DOTALL, + ) + assert match, ".sidebar-new-btn rule not found" + body = match.group(1) + assert "width" in body, ".sidebar-new-btn must have width property" + assert "100%" in body, ".sidebar-new-btn width must be 100%" + + +def test_css_sidebar_new_btn_dashed_border() -> None: + """.sidebar-new-btn must have a dashed border.""" + import re + css = read_css() + match = re.search( + r"\.sidebar-new-btn\s*\{([^}]*)\}", + css, + re.DOTALL, + ) + assert match, ".sidebar-new-btn rule not found" + body = match.group(1) + assert "dashed" in body, ".sidebar-new-btn must have border: 1px dashed" + + +def test_css_sidebar_new_btn_hover_exists() -> None: + """.sidebar-new-btn:hover rule must exist.""" + css = read_css() + assert ".sidebar-new-btn:hover" in css, ( + "Missing .sidebar-new-btn:hover rule in style.css" + ) diff --git a/muxplex/tests/test_frontend_html.py b/muxplex/tests/test_frontend_html.py index fdf5f22..66917c9 100644 --- a/muxplex/tests/test_frontend_html.py +++ b/muxplex/tests/test_frontend_html.py @@ -860,3 +860,92 @@ def test_html_new_session_panel_has_reset_button() -> None: assert "settings-action-btn" in classes, ( f"#setting-template-reset must have class 'settings-action-btn', has: {classes}" ) + + +# ============================================================ +# Sidebar sticky footer (task-5-sidebar-new-footer) +# ============================================================ + + +def test_html_sidebar_footer_exists() -> None: + """#session-sidebar must contain a div.sidebar-footer after #sidebar-list.""" + soup = _SOUP + sidebar = soup.find(id="session-sidebar") + assert sidebar is not None, "Missing #session-sidebar" + footer = sidebar.find("div", class_="sidebar-footer") + assert footer is not None, "Missing div.sidebar-footer inside #session-sidebar" + + +def test_html_sidebar_footer_after_sidebar_list() -> None: + """div.sidebar-footer must appear after #sidebar-list inside #session-sidebar.""" + soup = _SOUP + sidebar = soup.find(id="session-sidebar") + assert sidebar is not None, "Missing #session-sidebar" + children = [el for el in sidebar.children if isinstance(el, Tag)] + child_ids_and_classes = [] + for el in children: + cid = el.get("id") + cls = el.get("class") or [] + child_ids_and_classes.append((cid, cls)) + + # Find sidebar-list and sidebar-footer positions + list_idx = None + footer_idx = None + for i, (cid, cls) in enumerate(child_ids_and_classes): + if cid == "sidebar-list": + list_idx = i + if "sidebar-footer" in cls: + footer_idx = i + + assert list_idx is not None, "#sidebar-list must be in #session-sidebar children" + assert footer_idx is not None, "div.sidebar-footer must be in #session-sidebar children" + assert list_idx < footer_idx, ( + f"div.sidebar-footer must appear after #sidebar-list, " + f"got list_idx={list_idx}, footer_idx={footer_idx}" + ) + + +def test_html_sidebar_new_session_btn_exists() -> None: + """#sidebar-new-session-btn must exist inside div.sidebar-footer.""" + soup = _SOUP + sidebar = soup.find(id="session-sidebar") + assert sidebar is not None, "Missing #session-sidebar" + footer = sidebar.find("div", class_="sidebar-footer") + assert footer is not None, "Missing div.sidebar-footer inside #session-sidebar" + btn = footer.find(id="sidebar-new-session-btn") + assert btn is not None, "Missing #sidebar-new-session-btn inside div.sidebar-footer" + + +def test_html_sidebar_new_session_btn_class() -> None: + """#sidebar-new-session-btn must have class sidebar-new-btn.""" + soup = _SOUP + btn = soup.find(id="sidebar-new-session-btn") + assert btn is not None, "Missing #sidebar-new-session-btn" + classes = btn.get("class") or [] + assert "sidebar-new-btn" in classes, ( + f"#sidebar-new-session-btn must have class 'sidebar-new-btn', has: {classes}" + ) + + +def test_html_sidebar_new_session_btn_text() -> None: + """#sidebar-new-session-btn must have text '+ New'.""" + soup = _SOUP + btn = soup.find(id="sidebar-new-session-btn") + assert btn is not None, "Missing #sidebar-new-session-btn" + text = btn.get_text(strip=True) + assert text == "+ New", ( + f"#sidebar-new-session-btn text must be '+ New', got: {text!r}" + ) + + +def test_html_sidebar_structure_complete() -> None: + """#session-sidebar must have sidebar-header, sidebar-list, and sidebar-footer in order.""" + soup = _SOUP + sidebar = soup.find(id="session-sidebar") + assert sidebar is not None, "Missing #session-sidebar" + header = sidebar.find(class_="sidebar-header") + list_ = sidebar.find(id="sidebar-list") + footer = sidebar.find(class_="sidebar-footer") + assert header is not None, "Missing .sidebar-header in #session-sidebar" + assert list_ is not None, "Missing #sidebar-list in #session-sidebar" + assert footer is not None, "Missing .sidebar-footer in #session-sidebar" diff --git a/muxplex/tests/test_frontend_js.py b/muxplex/tests/test_frontend_js.py index ebb27d1..5552d41 100644 --- a/muxplex/tests/test_frontend_js.py +++ b/muxplex/tests/test_frontend_js.py @@ -1776,3 +1776,25 @@ def test_exports_create_new_session() -> None: assert "createNewSession" in exports, ( "module.exports must export createNewSession" ) + + +# ============================================================ +# Sidebar + New sticky footer (task-5-sidebar-new-footer) +# ============================================================ + + +def test_bind_sidebar_new_session_btn_in_bind_static_event_listeners() -> None: + """bindStaticEventListeners must bind #sidebar-new-session-btn click to showNewSessionInput.""" + match = re.search( + r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}", + _JS, + re.DOTALL, + ) + assert match, "bindStaticEventListeners function not found" + body = match.group(1) + assert "sidebar-new-session-btn" in body, ( + "bindStaticEventListeners must reference 'sidebar-new-session-btn'" + ) + assert "showNewSessionInput" in body, ( + "bindStaticEventListeners must call showNewSessionInput for the sidebar new-session button" + )