diff options
Diffstat (limited to 'packages')
16 files changed, 186 insertions, 88 deletions
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` <div class="user-menu-wrap" ref=${ref}> - <button class="user-menu-trigger" onClick=${() => setOpen(o => !o)}> + <button class="user-menu-trigger" aria-label=${user.username} + aria-expanded=${open ? 'true' : 'false'} onClick=${() => setOpen(o => !o)}> <span class="user-menu-avatar">${user.username[0].toUpperCase()}</span> <span class="user-menu-name">${user.username}</span> <${Icon} name="chevron" cls="user-menu-caret ${open ? 'flip' : ''}" /> @@ -114,7 +117,7 @@ function UserMenu({ user, theme, onThemeChange, onLogout }) { <div class="user-menu-header"> <span class="user-menu-avatar lg">${user.username[0].toUpperCase()}</span> <div> - <div class="user-menu-uname">${user.username}</div> + <div class="user-menu-uname" title=${user.username}>${user.username}</div> <div class="user-menu-role">${user.role || 'user'}</div> </div> </div> @@ -137,6 +140,26 @@ function UserMenu({ user, theme, onThemeChange, onLogout }) { <button class="user-menu-item" onClick=${() => { setOpen(false); navigate('/settings'); }}> <${Icon} name="gear" cls="umi-icon" /> ${t('usermenu.settings')} </button> + ${/* 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` + <button class="user-menu-item" onClick=${() => go('/admin')}> + <${Icon} name="shield" cls="umi-icon" /> ${t('admin.title')} + </button> + `} + ${showNode && html` + <button class="user-menu-item" onClick=${() => go('/node')}> + <${Icon} name="server" cls="umi-icon" /> ${t('node.title')} + </button> + <button class="user-menu-item" onClick=${() => go('/create-group')}> + <${Icon} name="plus" cls="umi-icon" /> ${t('sidebar.create_group')} + </button> + `} + ${PHONE_SYNC && html` + <button class="user-menu-item" onClick=${() => go('/android-sync')}> + <${Icon} name="phone" cls="umi-icon" /> ${t('androidsync.title')} + </button> + `} <button class="user-menu-item" onClick=${() => { onThemeChange(resolved === 'dark' ? 'light' : 'dark'); setOpen(false); @@ -395,7 +418,7 @@ function formatEta(seconds) { // ── Nav ────────────────────────────────────────────────────────────────────── function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount, - hubUnset }) { + hubUnset, route, showNode }) { return html` <nav class="nav"> <div class="nav-left"> @@ -409,6 +432,13 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount, </div> <div class="nav-right"> ${user && html`<${TransferWidget} />`} + ${user && html` + <a class="nav-notif nav-search" href="#/search" + aria-current=${route === '/search' ? 'page' : null} + title=${t('sidebar.search')} aria-label=${t('sidebar.search')}> + <${Icon} name="search" /> + </a> + `} ${platform.capabilities.tray && html` <button class="nav-tray" onClick=${() => platform.minimizeToTray(trayLabels())} title=${t('nav.minimize_tray')} aria-label=${t('nav.minimize_tray')}> @@ -422,7 +452,7 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount, </a> `} ${user ? html` - <${UserMenu} user=${user} theme=${theme} + <${UserMenu} user=${user} theme=${theme} showNode=${showNode} onThemeChange=${onThemeChange} onLogout=${onLogout} /> ` : hubUnset ? null : html` <a class="nav-btn" href="#/login">${t('nav.login')}</a> @@ -434,40 +464,17 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount, // ── Sidebar ────────────────────────────────────────────────────────────────── -function Sidebar({ groups, presence, indexProgressPct, route, menuOpen, role, showNode, +// Groups, and little else: search is in the nav; administration, the node, +// group creation and Android Sync in the user menu. +function Sidebar({ groups, presence, indexProgressPct, route, menuOpen, allowPublicGroups = true }) { - const isStaff = role === 'moderator' || role === 'admin'; return html` <aside class="sidebar ${menuOpen ? 'open' : ''}"> - ${isStaff && html` + ${allowPublicGroups && html` <div class="sidebar-section"> - <div class="sidebar-heading">${t('sidebar.admin')}</div> - <a class="sidebar-item ${route === '/admin' ? 'active' : ''}" - href="#/admin"><${Icon} name="shield" /> ${t('admin.title')}</a> - </div> - `} - <div class="sidebar-section"> - <div class="sidebar-heading">${t('sidebar.discover')}</div> - ${allowPublicGroups && html` + <div class="sidebar-heading">${t('sidebar.discover')}</div> <a class="sidebar-item ${route === '/explore' ? 'active' : ''}" - href="#/explore"><${Icon} name="globe" /> ${t('sidebar.public_groups')}</a>`} - <a class="sidebar-item ${route === '/search' ? 'active' : ''}" - href="#/search"><${Icon} name="search" /> ${t('sidebar.search')}</a> - </div> - ${platform.capabilities.nodeAdmin && showNode && html` - <div class="sidebar-section"> - <div class="sidebar-heading">${t('sidebar.node')}</div> - <a class="sidebar-item ${route === '/node' ? 'active' : ''}" - href="#/node"><${Icon} name="server" /> ${t('node.title')}</a> - <a class="sidebar-item ${route === '/create-group' ? 'active' : ''}" - href="#/create-group"><${Icon} name="plus" /> ${t('sidebar.create_group')}</a> - </div> - `} - ${PHONE_SYNC && html` - <div class="sidebar-section"> - <div class="sidebar-heading">${t('sidebar.phone')}</div> - <a class="sidebar-item ${route === '/android-sync' ? 'active' : ''}" - href="#/android-sync"><${Icon} name="phone" /> ${t('androidsync.title')}</a> + href="#/explore"><${Icon} name="globe" /> ${t('sidebar.public_groups')}</a> </div> `} <div class="sidebar-section"> @@ -1434,7 +1441,9 @@ function App() { page = html`<${ExplorePage} token=${user.token} myGroupIds=${groups.map(g => g.id)} allowPublicGroups=${allowPublicGroups} />`; - } else if (route === '/create-group') { + } else if (route === '/create-group' && platform.capabilities.nodeAdmin) { + // The desktop application only, by design: a browser is offered no way to + // create a group, and a hand-typed address does not open one either. page = html`<${LazyCreateGroupPage} token=${user.token} username=${user.username} allowPublicGroups=${allowPublicGroups} onNodeLinked=${refreshNodeKey} @@ -1500,7 +1509,8 @@ function App() { onThemeChange=${changeTheme} onLogout=${authCtx.logout} onMenuToggle=${() => setMenuOpen(o => !o)} - unreadCount=${unreadCount} hubUnset=${needsHub} /> + unreadCount=${unreadCount} hubUnset=${needsHub} route=${groupRoute} + showNode=${platform.capabilities.nodeAdmin && (hasNodeKey || localNode)} /> <div class="layout"> ${user && html`<${Sidebar} groups=${groups} @@ -1508,9 +1518,7 @@ function App() { indexProgressPct=${indexProgressPct} route=${groupRoute} menuOpen=${menuOpen} - role=${user.role} - allowPublicGroups=${allowPublicGroups} - showNode=${hasNodeKey || localNode} />`} + allowPublicGroups=${allowPublicGroups} />`} ${menuOpen && html`<div class="overlay visible" onClick=${() => setMenuOpen(false)} />`} <main class="main"> diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 264cd5b..afa1ba0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -556,7 +556,6 @@ export default { // Sidebar 'sidebar.create_group': 'Gruppe erstellen', 'sidebar.settings': 'Einstellungen', - 'sidebar.admin': 'Verwaltung', // User menu 'usermenu.language': 'Sprache', @@ -927,9 +926,7 @@ export default { 'lazy.reload': 'Seite neu laden', // Sidebar - 'sidebar.phone': 'Telefon', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index a2d7699..b4346b9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -544,7 +544,6 @@ export default { // Sidebar 'sidebar.create_group': 'Create group', 'sidebar.settings': 'Settings', - 'sidebar.admin': 'Admin', // User menu 'usermenu.language': 'Language', @@ -1028,9 +1027,7 @@ export default { 'usermenu.profile': 'Profile', // Node management (D5) - 'sidebar.phone': 'Phone', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', 'node.title': 'Node', 'node.service_checking': 'Checking node service…', 'node.service_not_installed': 'Not installed as a service', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index e7d313e..9bb26e1 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -551,7 +551,6 @@ export default { // Sidebar 'sidebar.create_group': 'Crear un grupo', 'sidebar.settings': 'Ajustes', - 'sidebar.admin': 'Administración', // User menu 'usermenu.language': 'Idioma', @@ -921,9 +920,7 @@ export default { 'lazy.reload': 'Recargar la página', // Sidebar - 'sidebar.phone': 'Teléfono', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index e1994dd..9f32a19 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -554,7 +554,6 @@ export default { // Sidebar 'sidebar.create_group': 'Créer un groupe', 'sidebar.settings': 'Paramètres', - 'sidebar.admin': 'Administration', // User menu 'usermenu.language': 'Langue', @@ -924,9 +923,7 @@ export default { 'lazy.reload': 'Recharger la page', // Sidebar - 'sidebar.phone': 'Téléphone', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index eb96a49..5578028 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -554,7 +554,6 @@ export default { // Sidebar 'sidebar.create_group': 'Crea un gruppo', 'sidebar.settings': 'Impostazioni', - 'sidebar.admin': 'Amministrazione', // User menu 'usermenu.language': 'Lingua', @@ -923,9 +922,7 @@ export default { 'lazy.reload': 'Ricarica la pagina', // Sidebar - 'sidebar.phone': 'Telefono', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 2a4584a..b5e091c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -548,7 +548,6 @@ export default { // Sidebar 'sidebar.create_group': 'グループを作成', 'sidebar.settings': '設定', - 'sidebar.admin': '管理', // User menu 'usermenu.language': '言語', @@ -911,9 +910,7 @@ export default { 'lazy.reload': 'ページを再読み込み', // Sidebar - 'sidebar.phone': 'スマートフォン', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index ce4acbe..86e01a2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -555,7 +555,6 @@ export default { // Sidebar 'sidebar.create_group': 'Groep aanmaken', 'sidebar.settings': 'Instellingen', - 'sidebar.admin': 'Beheer', // User menu 'usermenu.language': 'Taal', @@ -925,9 +924,7 @@ export default { 'lazy.reload': 'Pagina opnieuw laden', // Sidebar - 'sidebar.phone': 'Telefoon', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 72ace3a..090be3f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -569,7 +569,6 @@ export default { // Sidebar 'sidebar.create_group': 'Utwórz grupę', 'sidebar.settings': 'Ustawienia', - 'sidebar.admin': 'Administracja', // User menu 'usermenu.language': 'Język', @@ -945,9 +944,7 @@ export default { 'lazy.reload': 'Wczytaj stronę ponownie', // Sidebar (supplementary) - 'sidebar.phone': 'Telefon', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index ad46dc2..bfac600 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -553,7 +553,6 @@ export default { // Sidebar 'sidebar.create_group': 'Criar um grupo', 'sidebar.settings': 'Configurações', - 'sidebar.admin': 'Administração', // User menu 'usermenu.language': 'Idioma', @@ -922,9 +921,7 @@ export default { 'lazy.reload': 'Recarregar a página', // Sidebar - 'sidebar.phone': 'Celular', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index ccaf059..367efa0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -542,7 +542,6 @@ export default { // Sidebar 'sidebar.create_group': '创建群组', 'sidebar.settings': '设置', - 'sidebar.admin': '管理', // User menu 'usermenu.language': '语言', @@ -899,9 +898,7 @@ export default { 'lazy.reload': '重新加载页面', // Sidebar - 'sidebar.phone': '手机', 'androidsync.title': 'Android Sync', - 'sidebar.node': 'Node', // Node admin 'node.title': 'Node', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index f6cb57c..b5996d8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -328,6 +328,7 @@ a:hover { text-decoration: underline; } border-radius: 10px; box-shadow: var(--shadow-lg); min-width: 220px; + max-width: calc(100vw - 32px); z-index: 120; /* The language submenu expands to ten entries inside this dropdown, which overruns a short viewport. Clip horizontally as before, scroll vertically @@ -342,7 +343,14 @@ a:hover { text-decoration: underline; } display: flex; align-items: center; gap: 10px; padding: 12px 14px 8px; } -.user-menu-uname { font-weight: 600; font-size: 0.9em; color: var(--text); } +/* One line, whatever the name. Usernames go to 64 characters, and unclipped one + stretched the dropdown 122 px off the left of a phone — where, on a phone, + this line is now the only place the name is shown. */ +.user-menu-header > div { min-width: 0; } +.user-menu-uname { + font-weight: 600; font-size: 0.9em; color: var(--text); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} .user-menu-role { font-size: 0.75em; color: var(--text-dim); text-transform: capitalize; } .user-menu-divider { height: 1px; background: var(--border); margin: 4px 0; } @@ -2914,6 +2922,20 @@ a.transfer-name { /* ── Responsive ───────────────────────────────────────────────────────────── */ +/* On a phone or a tablet — the Android application included, in either + orientation — the user menu is the avatar alone; the name is the dropdown's + first line. The nav there holds the hamburger, the wordmark, transfers, + search, the bell and the menu: with the name in the trigger, a 20-character + handle put the menu 159 px off a 320 px screen. `pointer: coarse` is what + catches a phone held sideways, which is wider than the breakpoint. */ +@media (max-width: 768px), (pointer: coarse) { + .user-menu-trigger { padding: 3px; } + .user-menu-name, .user-menu-caret { display: none; } +} +@media (max-width: 520px) { + .nav-right { gap: 6px; } +} + @media (max-width: 768px) { .nav-hamburger { display: flex; } @@ -3123,6 +3145,15 @@ a.transfer-name { font-size: 1.25rem; margin-right: 4px; } +/* Search left the sidebar for the nav, and took its "you are here" with it. */ +.nav-search { + display: inline-flex; + align-items: center; + padding: 4px; + border-radius: 6px; +} +.nav-search:hover, +.nav-search[aria-current="page"] { background: rgba(255, 255, 255, 0.1); } .notif-badge { position: absolute; top: -6px; diff --git a/packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py b/packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py index 0324367..c70688b 100644 --- a/packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py +++ b/packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py @@ -41,9 +41,7 @@ FRAGMENT = f""" <div class="layout"> <aside class="sidebar open"> <div class="sidebar-section"><div class="sidebar-heading">Discover</div> - <a class="sidebar-item" href="#">Search</a></div> - <div class="sidebar-section"><div class="sidebar-heading">Node</div> - <a class="sidebar-item" href="#">Node</a><a class="sidebar-item" href="#">Create</a></div> + <a class="sidebar-item" href="#">Public groups</a></div> <div class="sidebar-section"><div class="sidebar-heading">My groups</div>{GROUPS}</div> <a class="sidebar-legal" href="#">Legal</a> </aside> diff --git a/packages/meshbay-hub/tests/test_layout_measured.py b/packages/meshbay-hub/tests/test_layout_measured.py index 57d325a..298a754 100644 --- a/packages/meshbay-hub/tests/test_layout_measured.py +++ b/packages/meshbay-hub/tests/test_layout_measured.py @@ -62,8 +62,12 @@ NAV = textwrap.dedent(""" </div> </div> </div> + <a class="nav-notif nav-search" href="#/search"> + <svg class="icon" viewBox="0 0 24 24"></svg></a> <a class="nav-notif" href="#/">🔔</a> - <div class="user-menu"><button class="nav-btn">someone</button></div> + <div class="user-menu-wrap"><button class="user-menu-trigger"> + <span class="user-menu-avatar">S</span><span class="user-menu-name">someone</span> + </button></div> </div> </nav> """) @@ -206,8 +210,12 @@ GROUPED = textwrap.dedent(""" </div> </div> </div> + <a class="nav-notif nav-search" href="#/search"> + <svg class="icon" viewBox="0 0 24 24"></svg></a> <a class="nav-notif" href="#/">🔔</a> - <div class="user-menu"><button class="nav-btn">someone</button></div> + <div class="user-menu-wrap"><button class="user-menu-trigger"> + <span class="user-menu-avatar">S</span><span class="user-menu-name">someone</span> + </button></div> </div> </nav> """) @@ -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 = '<svg class="icon" viewBox="0 0 24 24"></svg>' +FULL_NAV = textwrap.dedent(f""" + <nav class="nav"> + <div class="nav-left"> + <button class="nav-hamburger">{_ICON}</button> + <a class="nav-brand" href="#/"><img class="nav-brand-m" alt="M">eshBay</a> + </div> + <div class="nav-right"> + <div class="transfer-wrap"> + <button class="nav-notif transfer-btn">{_ICON}<span class="notif-badge">2</span></button> + </div> + <a class="nav-notif nav-search" href="#/search">{_ICON}</a> + <a class="nav-notif" href="#/">{_ICON}<span class="notif-badge">3</span></a> + <div class="user-menu-wrap"> + <button class="user-menu-trigger"> + <span class="user-menu-avatar">A</span> + <span class="user-menu-name">a_rather_long_handle</span> + <svg class="icon user-menu-caret" viewBox="0 0 24 24"></svg> + </button> + </div> + </div> + </nav> +""") +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 |