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:
@@ -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();
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<button id="sidebar-collapse-btn" class="sidebar-collapse-btn" aria-label="Collapse session list">‹</button>
|
||||
</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 id="terminal-container" class="terminal-container"></div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
============================================================ */
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user