feat(ui): mobile mesh tabs, AIUI-style audio player, cloud grid + map fixes

UI (this session):
- Global audio player now scales the whole interface into the space above it
  on desktop (sidebar + main) and docks directly above the tab bar on mobile;
  it stays visible while navigating.
- Mesh mobile redesign: floating Chat / BTC / Dead Man / AI / Map tab strip
  with a single fixed, internally-scrolling pane (page no longer scrolls);
  tabs hide while a conversation is open; floating back button; collapsible
  Device panel (starts collapsed); keyboard-aware conversation sizing via
  VisualViewport so the chat sits just above the keyboard.
- Cloud file grid: uniform 4/3 card heights (folders + images match).
- Swipe left/right switches tabs on the Apps and Web5 screens.
- Map tool fills its pane (no bottom gap); fix skewed Share Location toggle
  on mobile (global min-height rule was deforming the switch).
- Trim redundant helper copy from the mesh AI tab.

Also bundles pre-existing in-progress work that was already in the tree:
mesh listener/session + wallet + container + bitcoin-status backend changes,
docker UI updates, and assorted other UI tweaks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-19 09:52:26 -04:00
co-authored by Claude Opus 4.8
parent c4855526fe
commit 1bce694ebb
37 changed files with 1260 additions and 208 deletions
+68
View File
@@ -70,6 +70,8 @@
tabindex="-1"
@pointerenter="activateMainScroll"
@wheel.capture="activateMainScroll"
@touchstart.passive="onContentTouchStart"
@touchend.passive="onContentTouchEnd"
>
<div data-controller-main-entry class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
<!-- Controller zone entry point - no switcher -->
@@ -253,6 +255,72 @@ function activateMainScroll() {
}
}
// ── Swipe left/right to move between the mobile top tabs ──────────────────
// Apps screen: My Apps ⇄ App Store ⇄ Services
// Web5 screen: Web5 ⇄ Cloud ⇄ Network ⇄ Mesh
// Only active while the matching tab strip is actually showing (mobile).
type TabTarget = { key: string; to: { path: string; query: Record<string, string> } }
const APPS_TABS: TabTarget[] = [
{ key: 'myapps', to: { path: '/dashboard/apps', query: {} } },
{ key: 'store', to: { path: '/dashboard/discover', query: {} } },
{ key: 'services', to: { path: '/dashboard/apps', query: { tab: 'services' } } },
]
const NET_TABS: TabTarget[] = [
{ key: 'web5', to: { path: '/dashboard/web5', query: {} } },
{ key: 'cloud', to: { path: '/dashboard/cloud', query: {} } },
{ key: 'server', to: { path: '/dashboard/server', query: {} } },
{ key: 'mesh', to: { path: '/dashboard/mesh', query: {} } },
]
function activeAppsKey(): string {
if (route.query.tab === 'services' || route.query.tab === 'websites') return 'services'
if (route.path.includes('/marketplace') || route.path.includes('/discover')) return 'store'
if (route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps')) return 'myapps'
return ''
}
function activeNetKey(): string {
const p = route.path
if (p.startsWith('/dashboard/web5')) return 'web5'
if (p.startsWith('/dashboard/cloud')) return 'cloud'
if (p.startsWith('/dashboard/server')) return 'server'
if (p.startsWith('/dashboard/mesh')) return 'mesh'
return ''
}
let touchStartX = 0
let touchStartY = 0
let touchStartTime = 0
function onContentTouchStart(e: TouchEvent) {
const t = e.touches[0]
if (!t) return
touchStartX = t.clientX
touchStartY = t.clientY
touchStartTime = e.timeStamp
}
function onContentTouchEnd(e: TouchEvent) {
const t = e.changedTouches[0]
if (!t) return
const dx = t.clientX - touchStartX
const dy = t.clientY - touchStartY
const dt = e.timeStamp - touchStartTime
// Clear horizontal flick: far enough, mostly sideways, and quick.
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.8 || dt > 600) return
const nav = mobileNavRef.value
if (!nav) return
let tabs: TabTarget[] | null = null
let activeKey = ''
if (nav.showAppsTabs) { tabs = APPS_TABS; activeKey = activeAppsKey() }
else if (nav.showNetworkTabs) { tabs = NET_TABS; activeKey = activeNetKey() }
if (!tabs) return
const idx = tabs.findIndex(tb => tb.key === activeKey)
if (idx < 0) return
const next = idx + (dx < 0 ? 1 : -1) // swipe left → next tab, right → previous
if (next < 0 || next >= tabs.length) return
router.push(tabs[next].to).catch(() => {})
}
watch(() => route.path, (newPath) => {
const isAppDetails = isDetailRoute(newPath)
const wasAppDetails = showAltBackground.value