From 90c3c6a01f46c9b29c5d92702d7383f32e8951d7 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 19 Aug 2026 16:36:29 +0200 Subject: feat(ui): 11-point UI overhaul — tabs, transfers, settings, uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hub/UI: - Icon-only group tabs (chat, files, settings) with per-group default tab - Transfer widget: filename becomes a clickable link to open completed downloads - Pulse animation on transfer icon (pale→dark green) while active - Download button feedback in FilePreview (spinner, auto-reset) - Group mute toggle persists across navigation - Login page autofocus, chat refocus after send - Theme toggle closes menu, status badge and duplicate connecting removed - Create-folder restricted to operators, download-path note removed - User preferences API (CRUD) with Alembic migration - Profile: email display/edit via PATCH /v1/users/me - Settings: "Defaults" section for default tab selector - All 10 locale files updated Node: - upload_dir in node.toml: separate filesystem path for uploads - Root.direct flag: uploads land at root path, no subdirectory - CLI --upload-dir flag on `group add` - Admin UI accepts upload_dir Client (Electron): - shell.openPath bridge for opening completed downloads - platform.js passes open callback from native save Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 291 +++++++++++++++++---- .../src/meshbay_hub/static/locales/de.js | 19 +- .../src/meshbay_hub/static/locales/en.js | 17 +- .../src/meshbay_hub/static/locales/es.js | 19 +- .../src/meshbay_hub/static/locales/fr.js | 20 +- .../src/meshbay_hub/static/locales/it.js | 19 +- .../src/meshbay_hub/static/locales/ja.js | 20 +- .../src/meshbay_hub/static/locales/nl.js | 19 +- .../src/meshbay_hub/static/locales/pl.js | 19 +- .../src/meshbay_hub/static/locales/pt-BR.js | 19 +- .../src/meshbay_hub/static/locales/zh-CN.js | 17 +- .../meshbay-hub/src/meshbay_hub/static/platform.js | 1 + .../meshbay-hub/src/meshbay_hub/static/style.css | 47 ++-- 13 files changed, 386 insertions(+), 141 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 2e3c422..816c0ab 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -398,6 +398,11 @@ const ICON_PATHS = { check: ['M4.5 12.5l5 5 10-11'], chevron: ['M6 9.5l6 6 6-6'], close: ['M6 6l12 12M18 6L6 18'], + chat: ['M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'], + folder: ['M3.5 6.6a1 1 0 0 1 1-1h4.2l2 2.4h7.8a1 1 0 0 1 1 1v9.4a1 1 0 0 1-1 1h-14a1 1 0 0 1-1-1z'], + 'bell-off': ['M18 9a6 6 0 0 0-12 0c0 6-2.5 7.5-2.5 7.5h17S18 15 18 9', + 'M10.3 20a2 2 0 0 0 3.4 0', + 'M4 4l16 16'], }; // The M of the wordmark is a picture; the rest is text. Resolved from this @@ -472,6 +477,7 @@ function UserMenu({ user, theme, onThemeChange, onLogout }) { + `} ${error && html`
${error}
`} ${needsDevice && html` @@ -1969,15 +2006,18 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, chat still need the node and say so. */ group && html`
+ onClick=${() => setTab('chat')} title=${t('group.tab_chat')}> + <${Icon} name="chat" cls="tab-icon" /> + onClick=${() => setTab('files')} title=${t('group.tab_files')}> + <${Icon} name="folder" cls="tab-icon" /> + onClick=${() => setTab('settings')} title=${t('group.tab_settings')}> + <${Icon} name="gear" cls="tab-icon" />
${tab === 'files' && status !== 'connected' && html` -

${' '}${t('status.connecting')}

+

${' '}${t('status.connecting_short')}

`} ${tab === 'files' && status === 'connected' && html` @@ -2114,7 +2154,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, }} /> `} ${tab === 'chat' && status !== 'connected' && html` -

${' '}${t('status.connecting')}

+

${' '}${t('status.connecting_short')}

`} ${tab === 'settings' && html` @@ -2134,8 +2174,8 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, ${' '}${t('group.offline_hint')}

`} - ${(status === 'discovering' || status === 'connecting' || status === 'fetching') && html` -

${statusLabel}

+ ${(status === 'discovering' || status === 'connecting' || status === 'fetching') && !group && html` +

${' '}${t('status.connecting_short')}

`} ${previewEntry && html` <${FilePreview} @@ -2167,6 +2207,7 @@ function FilePreview({ entry, transportRef, gekRef, onClose, onDownload }) { const [progress, setProgress] = useState(0); const [content, setContent] = useState(null); const [error, setError] = useState(''); + const [downloading, setDownloading] = useState(false); const blobUrlRef = useRef(null); useEffect(() => { @@ -2239,9 +2280,18 @@ function FilePreview({ entry, transportRef, gekRef, onClose, onDownload }) {
${entry.name} (${formatSize(entry.size)}) ${onDownload && html` - + `} @@ -2297,7 +2347,8 @@ function _b64ToU8(b64) { */ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, isNodeAdmin, userId, operatorPaired, connected, - memberUpload, onMemberUpload, onPaired, onLeft }) { + memberUpload, onMemberUpload, + onPaired, onLeft }) { const [members, setMembers] = useState([]); const [adminId, setAdminId] = useState(''); const [loading, setLoading] = useState(true); @@ -2833,6 +2884,7 @@ function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, const [attaching, setAttaching] = useState(false); const listRef = useRef(null); const panelRef = useRef(null); + const inputRef = useRef(null); const loadedRef = useRef(false); // Set just before older messages are prepended; read once, after the DOM has // them but before the browser paints. @@ -2998,6 +3050,7 @@ function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, setInput(text); } finally { setSending(false); + setTimeout(() => { if (inputRef.current) inputRef.current.focus(); }); } }, [input, username, jumpToBottom]); @@ -3126,7 +3179,7 @@ function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, `} -