diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-12 01:27:00 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-12 01:27:00 +0200 |
| commit | 3324b4eb701e34f64e859dd90937793ccf7f75c5 (patch) | |
| tree | f22af245c36b76a1a158b91cc97f98df763ecb0f /packages/meshbay-hub | |
| parent | 14911aae21ec4975f7a805b91e3c4867cb41974b (diff) | |
| download | meshbay-3324b4eb701e34f64e859dd90937793ccf7f75c5.tar.gz | |
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
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 |