From 8ed0e94f809d1350b0db6355a72124e9666af464 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Wed, 15 Apr 2026 17:34:08 -0700 Subject: [PATCH] feat: add view dropdown CSS styles (task-5) Add CSS styles for the header view dropdown component: - .view-dropdown wrapper with relative positioning and flex layout - .view-dropdown__trigger with transparent border, 6px radius, transitions - .view-dropdown__trigger:hover and [aria-expanded=true] states - .view-dropdown__caret with 10px font and muted color - .view-dropdown__menu absolutely positioned below trigger, z-index: 100 - .view-dropdown__item with flex layout, text-overflow ellipsis - .view-dropdown__item:hover/:focus-visible with bg-surface - .view-dropdown__item--active with accent color and checkmark pseudo-element - .view-dropdown__shortcut right-aligned with monospace font - .view-dropdown__separator as 1px height divider - .view-dropdown__action and :hover states - .view-dropdown__new-input with accent border - .view-dropdown__count with dim color Add 3 TDD tests: test_view_dropdown_trigger_styled, test_view_dropdown_menu_styled, test_view_dropdown_item_styled --- muxplex/frontend/style.css | 136 +++++++++++++++++++++++++++++ muxplex/tests/test_frontend_css.py | 36 ++++++++ 2 files changed, 172 insertions(+) diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index 4f0cae1..f160545 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -1616,6 +1616,142 @@ body { z-index: 200; } +/* ============================================================ + View dropdown — header view selector + ============================================================ */ + +.view-dropdown { + position: relative; + display: flex; + align-items: center; + margin-left: 12px; +} + +.view-dropdown__trigger { + display: flex; + align-items: center; + gap: 6px; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + font-size: 13px; + font-family: var(--font-ui); + color: var(--text); + cursor: pointer; + padding: 4px 10px; + transition: border-color 0.15s, background 0.15s; +} + +.view-dropdown__trigger:hover, +.view-dropdown__trigger[aria-expanded="true"] { + border-color: var(--border); + background: var(--bg-surface); +} + +.view-dropdown__caret { + font-size: 10px; + color: var(--text-muted); +} + +.view-dropdown__menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + min-width: 200px; + max-width: 280px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + z-index: 100; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + overflow: hidden; +} + +.view-dropdown__item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: transparent; + border: none; + width: 100%; + font-size: 13px; + font-family: var(--font-ui); + color: var(--text); + cursor: pointer; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.view-dropdown__item:hover, +.view-dropdown__item:focus-visible { + background: var(--bg-surface); + outline: none; +} + +.view-dropdown__item--active { + color: var(--accent); +} + +.view-dropdown__item--active::before { + content: "\2713"; + flex-shrink: 0; +} + +.view-dropdown__shortcut { + margin-left: auto; + font-size: 11px; + color: var(--text-dim); + font-family: var(--font-mono); +} + +.view-dropdown__separator { + height: 1px; + margin: 4px 0; + background: var(--border-subtle); +} + +.view-dropdown__action { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + color: var(--text-muted); + font-size: 13px; + font-family: var(--font-ui); + background: transparent; + border: none; + width: 100%; + cursor: pointer; + text-align: left; +} + +.view-dropdown__action:hover { + background: var(--bg-surface); + color: var(--text); +} + +.view-dropdown__new-input { + width: calc(100% - 24px); + margin: 12px; + border: 1px solid var(--accent); + border-radius: 4px; + background: var(--bg); + color: var(--text); + font-size: 13px; + font-family: var(--font-ui); + padding: 4px 8px; + outline: none; + box-sizing: border-box; +} + +.view-dropdown__count { + font-size: 11px; + color: var(--text-dim); +} + /* ============================================================ Device badge, group header, filter bar, sidebar device header ============================================================ */ diff --git a/muxplex/tests/test_frontend_css.py b/muxplex/tests/test_frontend_css.py index 81c4bf5..915d4e8 100644 --- a/muxplex/tests/test_frontend_css.py +++ b/muxplex/tests/test_frontend_css.py @@ -2136,3 +2136,39 @@ def test_css_sidebar_item_default_left_border() -> None: ".sidebar-item must not use transparent for default border-left — " "it makes the left edge invisible on dark backgrounds" ) + + +# ============================================================ +# Header view dropdown styles (task-5) +# ============================================================ + + +def test_view_dropdown_trigger_styled() -> None: + """.view-dropdown__trigger must exist in CSS.""" + css = read_css() + assert ".view-dropdown__trigger" in css, ( + "Missing .view-dropdown__trigger CSS rule in style.css" + ) + + +def test_view_dropdown_menu_styled() -> None: + """.view-dropdown__menu must exist in CSS and be absolutely positioned below trigger.""" + css = read_css() + assert ".view-dropdown__menu" in css, ( + "Missing .view-dropdown__menu CSS rule in style.css" + ) + block = _extract_rule_block(css, ".view-dropdown__menu {") + assert "position: absolute" in block, ( + ".view-dropdown__menu must use position: absolute" + ) + assert "z-index: 100" in block, ( + ".view-dropdown__menu must have z-index: 100" + ) + + +def test_view_dropdown_item_styled() -> None: + """.view-dropdown__item must exist in CSS.""" + css = read_css() + assert ".view-dropdown__item" in css, ( + "Missing .view-dropdown__item CSS rule in style.css" + )