aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-12 01:27:00 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-12 01:27:00 +0200
commit3324b4eb701e34f64e859dd90937793ccf7f75c5 (patch)
treef22af245c36b76a1a158b91cc97f98df763ecb0f /packages/meshbay-hub/src/meshbay_hub
parent14911aae21ec4975f7a805b91e3c4867cb41974b (diff)
downloadmeshbay-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/src/meshbay_hub')
-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
12 files changed, 79 insertions, 70 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;