From 3324b4eb701e34f64e859dd90937793ccf7f75c5 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 12 Oct 2026 01:27:00 +0200 Subject: feat: keep the sidebar for groups; search in the nav, the rest in the user menu Search is an icon in the nav, left of the tray button and the bell, and marks itself current on its page. Administration, Node, Create group and Android Sync moved to the user menu, between Settings and the theme, each under the condition it had in the sidebar. The sidebar keeps Public groups and the groups. On a phone, a tablet or the Android application the menu trigger is the avatar alone (max-width 768px or a coarse pointer); the name is the dropdown's first line, one line with an ellipsis. Measured: a 20-character handle put the trigger 159 px off a 320 px screen with search and transfers in the nav, and a 64-character one pushed the dropdown 122 px off the left. Creating a group is a desktop-application feature by design: a hand-typed #/create-group no longer opens the form in a browser, and QUICKSTART step 6 now says to use the desktop application instead of the browser. test_layout_responsive read only the first 768px media block; it reads them all now. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 86 ++++++++++++---------- .../src/meshbay_hub/static/locales/de.js | 3 - .../src/meshbay_hub/static/locales/en.js | 3 - .../src/meshbay_hub/static/locales/es.js | 3 - .../src/meshbay_hub/static/locales/fr.js | 3 - .../src/meshbay_hub/static/locales/it.js | 3 - .../src/meshbay_hub/static/locales/ja.js | 3 - .../src/meshbay_hub/static/locales/nl.js | 3 - .../src/meshbay_hub/static/locales/pl.js | 3 - .../src/meshbay_hub/static/locales/pt-BR.js | 3 - .../src/meshbay_hub/static/locales/zh-CN.js | 3 - .../meshbay-hub/src/meshbay_hub/static/style.css | 33 ++++++++- .../tests/harness/sidebar_scroll_probe.py | 4 +- packages/meshbay-hub/tests/test_layout_measured.py | 83 ++++++++++++++++++++- .../meshbay-hub/tests/test_layout_responsive.py | 33 ++++++--- .../meshbay-hub/tests/test_sidebar_node_section.py | 5 +- 16 files changed, 186 insertions(+), 88 deletions(-) (limited to 'packages/meshbay-hub') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index b45903a..ea4cee6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -86,7 +86,7 @@ const BRAND_M = new URL('./meshbay-m.png', import.meta.url).href; // ── User Menu ──────────────────────────────────────────────────────────────── -function UserMenu({ user, theme, onThemeChange, onLogout }) { +function UserMenu({ user, theme, onThemeChange, onLogout, showNode }) { const [open, setOpen] = useState(false); const [langOpen, setLangOpen] = useState(false); const ref = useRef(null); @@ -101,10 +101,13 @@ function UserMenu({ user, theme, onThemeChange, onLogout }) { }, [open]); const resolved = resolveTheme(theme); + const isStaff = user.role === 'moderator' || user.role === 'admin'; + const go = (path) => { setOpen(false); navigate(path); }; return html`
- + ${/* Here rather than in the sidebar, which is kept for the groups: + these are reached now and then, the groups all the time. */''} + ${isStaff && html` + + `} + ${showNode && html` + + + `} + ${PHONE_SYNC && html` + + `}
+ + 🔔 -
+
""") @@ -206,8 +210,12 @@ GROUPED = textwrap.dedent(""" + + 🔔 -
+
""") @@ -283,3 +291,74 @@ def test_the_waiting_bar_is_as_wide_as_a_progress_bar(grouped): assert bar["width"] > 100, ( f"at {width} px the waiting bar is {bar['width']} px wide") assert bar["height"] >= 3, "the waiting bar has no height" + + +# ── The whole nav on a phone ──────────────────────────────────────────────── +# +# Search moved from the sidebar to the nav, beside the bell. With the user's +# name in the menu trigger, a 20-character handle already put the trigger 79 px +# off a 320 px screen, and 159 px once search and a transfer were there too. +# The fixture is the real markup of `Nav()` and `UserMenu()`, with the icons as +# the 1em boxes `Icon()` draws. + +_ICON = '' +FULL_NAV = textwrap.dedent(f""" + +""") +FULL_NAV_WIDTHS = [320, 360, 412, 521, 768, 1024] + + +@pytest.fixture(scope="module") +def full_nav(tmp_path_factory): + fragment = tmp_path_factory.mktemp("fullnav") / "fragment.html" + fragment.write_text(FULL_NAV, encoding="utf-8") + proc = subprocess.run( + ["python3", str(HARNESS), ",".join(str(w) for w in FULL_NAV_WIDTHS), + str(fragment), ".nav-brand", ".nav-right", ".user-menu-trigger", + ".user-menu-name"], + capture_output=True, text=True, timeout=180) + assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}" + out = json.loads(proc.stdout) + assert "error" not in out, f"no measurement: {out}" + return out + + +@pytest.mark.parametrize("width", FULL_NAV_WIDTHS) +def test_the_user_menu_stays_inside_the_nav(full_nav, width): + """Off the right edge is where nobody can open it — and it holds sign-out.""" + boxes = full_nav[str(width)]["boxes"] + trigger = boxes[".user-menu-trigger"] + assert trigger["right"] <= width - 16, ( + f"at {width} px the user menu ends at {trigger['right']} px, past the " + "nav's own padding") + assert boxes[".nav-right"]["left"] >= boxes[".nav-brand"]["right"], ( + f"at {width} px the nav's icons slide under the wordmark") + + +@pytest.mark.parametrize("width", FULL_NAV_WIDTHS) +def test_the_name_leaves_the_trigger_on_mobile_only(full_nav, width): + """Avatar alone up to the mobile breakpoint; the name beside it on a desktop.""" + name = full_nav[str(width)]["boxes"][".user-menu-name"] + shown = bool(name and name["width"] > 0) + assert shown == (width > 768), ( + f"at {width} px the name in the user menu is " + f"{'shown' if shown else 'hidden'}") diff --git a/packages/meshbay-hub/tests/test_layout_responsive.py b/packages/meshbay-hub/tests/test_layout_responsive.py index bb73557..a44df1a 100644 --- a/packages/meshbay-hub/tests/test_layout_responsive.py +++ b/packages/meshbay-hub/tests/test_layout_responsive.py @@ -50,17 +50,28 @@ def _rule(css: str, selector: str) -> str: @pytest.fixture(scope="module") def mobile(css): - """The body of the max-width: 768px block.""" - i = css.index("@media (max-width: 768px)") - depth, j = 0, css.index("{", i) - for k in range(j, len(css)): - if css[k] == "{": - depth += 1 - elif css[k] == "}": - depth -= 1 - if depth == 0: - return css[j:k] - pytest.fail("the mobile media query is not closed") + """The bodies of every block that applies at 768 px and below, joined. + + Every one, not the first: a second block whose condition also names the + breakpoint (`(max-width: 768px), (pointer: coarse)`) came first in the file + and hid the rules these tests look for. + """ + bodies = [] + for m in re.finditer(r"@media[^{]*\(max-width: 768px\)[^{]*\{", css): + depth, j = 0, m.end() - 1 + for k in range(j, len(css)): + if css[k] == "{": + depth += 1 + elif css[k] == "}": + depth -= 1 + if depth == 0: + bodies.append(css[j:k]) + break + else: + pytest.fail("a mobile media query is not closed") + if not bodies: + pytest.fail("no max-width: 768px media query") + return "\n".join(bodies) def test_a_toolbar_group_can_break(css): diff --git a/packages/meshbay-hub/tests/test_sidebar_node_section.py b/packages/meshbay-hub/tests/test_sidebar_node_section.py index 371a018..57c9e9e 100644 --- a/packages/meshbay-hub/tests/test_sidebar_node_section.py +++ b/packages/meshbay-hub/tests/test_sidebar_node_section.py @@ -1,6 +1,7 @@ """ -The sidebar's Node section follows the account's node link, and must follow it -when it changes -- not only at sign-in. +The Node and Create group entries -- in the sidebar when this was written, in +the user menu since -- follow the account's node link, and must follow it when +it changes -- not only at sign-in. Reported on a real install: after the first click on Create group, the Node section (Node, Create group) was gone from the sidebar until a reload, while -- cgit v1.2.3