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