feat: add sidebar + New sticky footer with inline input

- Add div.sidebar-footer with button#sidebar-new-session-btn.sidebar-new-btn
  ('+ New') to #session-sidebar in index.html, positioned after #sidebar-list
- Add CSS for .sidebar-footer (padding: 8px, border-top, flex-shrink: 0)
- Add CSS for .sidebar-new-btn (width: 100%, dashed border, dim text, hover
  effects with muted text and border color)
- Bind #sidebar-new-session-btn click to showNewSessionInput() in
  bindStaticEventListeners()
- Add 15 tests covering HTML structure, CSS rules, and JS binding

🤖 Co-authored-by: Amplifier <amplifier@example.com>
This commit is contained in:
Brian Krabach
2026-03-30 01:23:37 -07:00
parent 84fb7826d7
commit d76042560a
6 changed files with 241 additions and 0 deletions
+2
View File
@@ -1346,6 +1346,8 @@ function bindStaticEventListeners() {
on($('back-btn'), 'click', closeSession); on($('back-btn'), 'click', closeSession);
var newSessionBtn = $('new-session-btn'); var newSessionBtn = $('new-session-btn');
if (newSessionBtn) on(newSessionBtn, 'click', function() { showNewSessionInput(newSessionBtn); }); 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-toggle-btn'), 'click', toggleSidebar);
on($('sidebar-collapse-btn'), 'click', toggleSidebar); on($('sidebar-collapse-btn'), 'click', toggleSidebar);
bindSidebarClickAway(); bindSidebarClickAway();
+3
View File
@@ -45,6 +45,9 @@
<button id="sidebar-collapse-btn" class="sidebar-collapse-btn" aria-label="Collapse session list">&#8249;</button> <button id="sidebar-collapse-btn" class="sidebar-collapse-btn" aria-label="Collapse session list">&#8249;</button>
</div> </div>
<div id="sidebar-list" class="sidebar-list"></div> <div id="sidebar-list" class="sidebar-list"></div>
<div class="sidebar-footer">
<button id="sidebar-new-session-btn" class="sidebar-new-btn">+ New</button>
</div>
</div> </div>
<div id="terminal-container" class="terminal-container"></div> <div id="terminal-container" class="terminal-container"></div>
</div> </div>
+27
View File
@@ -1174,6 +1174,33 @@ body {
color: var(--text-dim); 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 Responsive overlay sidebar at <960px
============================================================ */ ============================================================ */
+98
View File
@@ -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), ( assert "color" in body and ("text-dim" in body or "--text-dim" in body), (
".new-session-input::placeholder must have color: var(--text-dim)" ".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"
)
+89
View File
@@ -860,3 +860,92 @@ def test_html_new_session_panel_has_reset_button() -> None:
assert "settings-action-btn" in classes, ( assert "settings-action-btn" in classes, (
f"#setting-template-reset must have class 'settings-action-btn', has: {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"
+22
View File
@@ -1776,3 +1776,25 @@ def test_exports_create_new_session() -> None:
assert "createNewSession" in exports, ( assert "createNewSession" in exports, (
"module.exports must export createNewSession" "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"
)