feat: add responsive sidebar overlay at <960px and reduced-motion support

This commit is contained in:
Brian Krabach
2026-03-27 17:00:07 -07:00
parent 4e7e461077
commit 67da2e9aa2
2 changed files with 132 additions and 2 deletions
+99 -2
View File
@@ -216,7 +216,9 @@ def test_css_sidebar_before_terminal_container():
sidebar_idx = css.index(".session-sidebar") sidebar_idx = css.index(".session-sidebar")
terminal_idx = css.index(".terminal-container") terminal_idx = css.index(".terminal-container")
assert view_body_idx < sidebar_idx, ".session-sidebar must come after .view-body" assert view_body_idx < sidebar_idx, ".session-sidebar must come after .view-body"
assert sidebar_idx < terminal_idx, ".session-sidebar must come before .terminal-container" assert sidebar_idx < terminal_idx, (
".session-sidebar must come before .terminal-container"
)
def test_css_sidebar_header(): def test_css_sidebar_header():
@@ -416,4 +418,99 @@ def test_css_sidebar_item_after_toggle_btn_hover():
assert ".sidebar-item" in css assert ".sidebar-item" in css
toggle_idx = css.index(".sidebar-toggle-btn:hover") toggle_idx = css.index(".sidebar-toggle-btn:hover")
item_idx = css.index(".sidebar-item") item_idx = css.index(".sidebar-item")
assert toggle_idx < item_idx, ".sidebar-item must come after .sidebar-toggle-btn:hover" assert toggle_idx < item_idx, (
".sidebar-item must come after .sidebar-toggle-btn:hover"
)
# ============================================================
# Responsive overlay at <960px and reduced-motion (task-5)
# ============================================================
def _extract_media_block(css: str, query: str) -> str:
"""Extract the inner content of a @media block (balanced-brace aware)."""
idx = css.index(query)
open_brace = css.index("{", idx)
depth = 0
pos = open_brace
while pos < len(css):
if css[pos] == "{":
depth += 1
elif css[pos] == "}":
depth -= 1
if depth == 0:
return css[open_brace + 1 : pos]
pos += 1
raise ValueError(f"Could not find matching close brace for {query}")
def test_css_responsive_overlay_media_query_exists():
"""@media (max-width: 959px) block must exist in the CSS."""
css = read_css()
assert "@media (max-width: 959px)" in css
def test_css_responsive_overlay_at_end():
"""@media (max-width: 959px) must be the last @media block in the file."""
css = read_css()
last_media_idx = css.rfind("@media")
assert "@media (max-width: 959px)" in css[last_media_idx:]
def test_css_responsive_overlay_sidebar_fixed():
""".session-sidebar inside <960px media query must become a fixed overlay."""
css = read_css()
media_block = _extract_media_block(css, "@media (max-width: 959px)")
assert ".session-sidebar {" in media_block
block = _extract_rule_block(media_block, ".session-sidebar {")
assert "position: fixed" in block
assert "left: 0" in block
assert "top: 0" in block
assert "height: 100%" in block
assert "z-index: 200" in block
assert "width: 240px" in block
assert "min-width: 240px" in block
assert "transition: transform 0.25s ease" in block
assert "transform: translateX(0)" in block
assert "box-shadow: 2px 0 16px rgba(0,0,0,0.5)" in block
def test_css_responsive_overlay_sidebar_collapsed():
""".session-sidebar.sidebar--collapsed inside <960px collapses via translateX(-100%)."""
css = read_css()
media_block = _extract_media_block(css, "@media (max-width: 959px)")
assert ".session-sidebar.sidebar--collapsed" in media_block
block = _extract_rule_block(media_block, ".session-sidebar.sidebar--collapsed")
assert "width: 240px" in block
assert "min-width: 240px" in block
assert "transform: translateX(-100%)" in block
def test_css_responsive_overlay_collapse_btn_hidden():
""".sidebar-collapse-btn inside <960px must be display: none."""
css = read_css()
media_block = _extract_media_block(css, "@media (max-width: 959px)")
assert ".sidebar-collapse-btn" in media_block
block = _extract_rule_block(media_block, ".sidebar-collapse-btn")
assert "display: none" in block
def test_css_reduced_motion_sidebar_transition_none():
"""@media (prefers-reduced-motion: reduce) must include .session-sidebar { transition: none; }."""
css = read_css()
media_block = _extract_media_block(css, "@media (prefers-reduced-motion: reduce)")
assert ".session-sidebar" in media_block
block = _extract_rule_block(media_block, ".session-sidebar")
assert "transition: none" in block
def test_css_reduced_motion_sidebar_after_toast():
"""In reduced-motion block, .session-sidebar must come after .toast { animation: none; }."""
css = read_css()
media_block = _extract_media_block(css, "@media (prefers-reduced-motion: reduce)")
toast_idx = media_block.index(".toast")
sidebar_idx = media_block.index(".session-sidebar")
assert toast_idx < sidebar_idx, (
".session-sidebar must come after .toast in reduced-motion block"
)
+33
View File
@@ -903,6 +903,10 @@ body {
.toast { .toast {
animation: none; animation: none;
} }
.session-sidebar {
transition: none;
}
} }
/* ============================================================ /* ============================================================
@@ -920,3 +924,32 @@ body {
width: auto; width: auto;
display: block; display: block;
} }
/* ============================================================
Responsive overlay sidebar at <960px
============================================================ */
@media (max-width: 959px) {
.session-sidebar {
position: fixed;
left: 0;
top: 0;
height: 100%;
z-index: 200;
width: 240px;
min-width: 240px;
transition: transform 0.25s ease;
transform: translateX(0);
box-shadow: 2px 0 16px rgba(0,0,0,0.5);
}
.session-sidebar.sidebar--collapsed {
width: 240px;
min-width: 240px;
transform: translateX(-100%);
}
.sidebar-collapse-btn {
display: none;
}
}