aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js86
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css33
-rw-r--r--packages/meshbay-hub/tests/harness/sidebar_scroll_probe.py4
-rw-r--r--packages/meshbay-hub/tests/test_layout_measured.py83
-rw-r--r--packages/meshbay-hub/tests/test_layout_responsive.py33
-rw-r--r--packages/meshbay-hub/tests/test_sidebar_node_section.py5
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="#/">&#128276;</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="#/">&#128276;</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