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 --- docs/QUICKSTART.md | 23 ++++-- docs/USERGUIDE.md | 8 +- 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 +- 18 files changed, 205 insertions(+), 100 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index ea75cab..cfb0743 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -48,8 +48,11 @@ Three things, and it is worth knowing which is which before you start: - [ ] **An email address** you can read — account sign-up sends a code to it. - [ ] **About half an hour**, plus however long a first index takes — which depends on how much you are sharing, and does not need watching. -- [ ] **Terminal access** to that machine. Everything below works over SSH; no - graphical session is needed on the node. +- [ ] **Terminal access** to that machine. Everything on the node works over + SSH; no graphical session is needed there. +- [ ] **The desktop application, on any computer with a screen** — the node + machine or another one. Creating a group is done there and only there + (Step 6). You do **not** need a static IP, a domain name, port forwarding, or a router you control. Two residential ISPs have been measured working without any of it. @@ -65,11 +68,11 @@ distribution: |---|---|---| | **`meshbay-common`** — required, install it first | `.deb` | `.rpm` | | **`meshbay-node`** — the daemon that hosts your files | `.deb` | `.rpm` | -| `meshbay-client` — the desktop application, optional | `.deb` | `.rpm` | +| `meshbay-client` — the desktop application | `.deb` | `.rpm` | -You need the first two. The desktop application is worth having if this machine -has a screen, but nothing in this guide requires it: a terminal and a browser -are enough. +The node machine needs the first two. The desktop application goes on whichever +computer you will sit at — this one if it has a screen, another one otherwise: +Step 6 needs it, because a group cannot be created from a browser. Put them in one directory — the next step installs them together. @@ -91,7 +94,8 @@ sudo apt install ./meshbay-common_*.deb ./meshbay-node_*.deb sudo dnf install ./meshbay-common-*.rpm ./meshbay-node-*.rpm ``` -Add `./meshbay-client_*` to that line to install the desktop application too. +Add `./meshbay-client_*` to that line if this is also the computer you will use +the desktop application on. `meshbay-common` carries everything the others need, so there is nothing else to install and no Python environment to set up. **ffmpeg**, required for video @@ -208,7 +212,10 @@ correct. ## Step 6 — Create the group -In the browser, on meshbay.org: **Create group** in the sidebar. +In the **desktop application**, signed in with your account: **Create group** +in the menu under your avatar, top right. It is there once the node key from +Step 4 is linked to your account. A browser does not offer it: creating a group +is a desktop-application feature, by design. - **Name** — anything. It is shown to members as `name@yourusername`. - **Join policy** — leave it on **Invite only**. The alternative, *Open*, lists diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index 42103d6..9d1749f 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -388,9 +388,9 @@ that matters to you. ### Backing up your phone -On the Android application, **Phone → Android Sync** in the side menu backs up -your phone's photos, contacts, calendar, text messages and the files of -folders you choose to a group. +On the Android application, **Android Sync** in the menu under your avatar, +top right, backs up your phone's photos, contacts, calendar, text messages and +the files of folders you choose to a group. Only your **personal profile** is backed up. If your phone has a work profile, nothing in it is sent, and MeshBay installed inside the work profile offers no @@ -503,7 +503,7 @@ notification and on the Android Sync page, and tries again the next day. ### Search -The magnifying glass in the sidebar searches **across every group you are in** +The magnifying glass in the top bar, left of the bell, searches **across every group you are in** at once, with a Files / Videos / Music / Photos view each. A file that two groups both have is **one result**, not two — identity is the 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