feat: mobile UI overhaul — iPhone-style app grid, icon-only tab bar, fullscreen app sessions
- Add AppIconGrid: 4-column swipeable icon grid with page dots for My Apps on mobile - Tab bar: remove text labels, square icon-only buttons (w-14 h-14), doubled padding - Hide tab bar and top context tabs when app session is open - App session header hidden on mobile, replaced with floating glass close button - App sessions now render fullscreen on mobile without nav chrome Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
afda9897f1
commit
76585656a7
@ -1926,6 +1926,129 @@ html:has(body.video-background-active)::before {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== iPhone-style App Icon Grid (mobile) ===== */
|
||||||
|
|
||||||
|
.app-icon-grid-wrap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-pages {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.app-icon-pages::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-page {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20px 12px;
|
||||||
|
padding: 8px 4px 16px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
.app-icon-item:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-frame {
|
||||||
|
position: relative;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status dot — top-right of icon */
|
||||||
|
.app-icon-status {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
.app-icon-status-running {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
.app-icon-status-error {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
.app-icon-status-transition {
|
||||||
|
background: #f59e0b;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-installing {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-label {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 72px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page indicator dots */
|
||||||
|
.app-icon-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.app-icon-dot-active {
|
||||||
|
background: rgba(247, 147, 26, 0.9);
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== End App Icon Grid ===== */
|
||||||
|
|
||||||
/* Monitoring dashboard */
|
/* Monitoring dashboard */
|
||||||
.monitoring-stat-card {
|
.monitoring-stat-card {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
@ -2120,6 +2243,29 @@ html:has(body.video-background-active)::before {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.discover-hero-layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-hero-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discover-hero-face {
|
||||||
|
display: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.discover-hero-face {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.discover-hero-accent {
|
.discover-hero-accent {
|
||||||
background: linear-gradient(90deg, #fb923c, #f59e0b, #fb923c);
|
background: linear-gradient(90deg, #fb923c, #f59e0b, #fb923c);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
@ -2175,9 +2321,12 @@ html:has(body.video-background-active)::before {
|
|||||||
|
|
||||||
.discover-principle-card {
|
.discover-principle-card {
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 1rem;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.65);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
transition: border-color 0.3s ease, transform 0.3s ease;
|
transition: border-color 0.3s ease, transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2187,8 +2336,9 @@ html:has(body.video-background-active)::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.discover-manifesto {
|
.discover-manifesto {
|
||||||
border-color: rgba(251, 146, 60, 0.1);
|
background-color: rgba(0, 0, 0, 0.65);
|
||||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.7) 0%, rgba(251, 146, 60, 0.03) 100%);
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fleet-matrix-table {
|
.fleet-matrix-table {
|
||||||
|
|||||||
@ -37,6 +37,17 @@
|
|||||||
@refresh="refresh"
|
@refresh="refresh"
|
||||||
@open-new-tab-and-back="openNewTabAndBack"
|
@open-new-tab-and-back="openNewTabAndBack"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Mobile: floating glass close button -->
|
||||||
|
<button
|
||||||
|
class="md:hidden app-session-mobile-close"
|
||||||
|
aria-label="Close"
|
||||||
|
@click="closeSession"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NostrIdentityPicker
|
<NostrIdentityPicker
|
||||||
@ -460,4 +471,30 @@ onBeforeUnmount(() => {
|
|||||||
.content-fade-leave-to {
|
.content-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile floating glass close button */
|
||||||
|
.app-session-mobile-close {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(24px + env(safe-area-inset-bottom, 0px));
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 2500;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
transition: background 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.app-session-mobile-close:active {
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
transform: translateX(-50%) scale(0.9);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -91,8 +91,16 @@
|
|||||||
<p class="text-white/70">{{ t('apps.noResults', { query: searchQuery }) }}</p>
|
<p class="text-white/70">{{ t('apps.noResults', { query: searchQuery }) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Apps Grid -->
|
<!-- Mobile: iPhone-style icon grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-6">
|
<div class="md:hidden">
|
||||||
|
<AppIconGrid
|
||||||
|
:apps="filteredPackageEntries as [string, PackageDataEntry][]"
|
||||||
|
@go-to-app="goToApp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop: Card grid -->
|
||||||
|
<div class="hidden md:grid grid-cols-2 lg:grid-cols-3 gap-4 pb-6">
|
||||||
<AppCard
|
<AppCard
|
||||||
v-for="([id, pkg], index) in filteredPackageEntries"
|
v-for="([id, pkg], index) in filteredPackageEntries"
|
||||||
:key="id"
|
:key="id"
|
||||||
@ -147,6 +155,7 @@ import { useServerStore } from '@/stores/server'
|
|||||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||||
import type { PackageDataEntry } from '@/types/api'
|
import type { PackageDataEntry } from '@/types/api'
|
||||||
import AppCard from './apps/AppCard.vue'
|
import AppCard from './apps/AppCard.vue'
|
||||||
|
import AppIconGrid from './apps/AppIconGrid.vue'
|
||||||
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
|
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
|
||||||
import { useAppsActions } from './apps/useAppsActions'
|
import { useAppsActions } from './apps/useAppsActions'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sticky top-0 z-10 flex items-center gap-3 border-b border-white/10 px-4 py-3 bg-black/60 backdrop-blur-md md:bg-transparent md:backdrop-blur-none">
|
<div class="sticky top-0 z-10 hidden md:flex items-center gap-3 border-b border-white/10 px-4 py-3 bg-black/60 backdrop-blur-md md:bg-transparent md:backdrop-blur-none">
|
||||||
<!-- Back / Forward navigation -->
|
<!-- Back / Forward navigation -->
|
||||||
<div class="flex items-center gap-0.5">
|
<div class="flex items-center gap-0.5">
|
||||||
<button class="app-session-btn" aria-label="Back" title="Go back" @click="$emit('goBack')">
|
<button class="app-session-btn" aria-label="Back" title="Go back" @click="$emit('goBack')">
|
||||||
|
|||||||
142
neode-ui/src/views/apps/AppIconGrid.vue
Normal file
142
neode-ui/src/views/apps/AppIconGrid.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-icon-grid-wrap">
|
||||||
|
<!-- Swipeable pages -->
|
||||||
|
<div
|
||||||
|
ref="scrollContainer"
|
||||||
|
class="app-icon-pages"
|
||||||
|
@scroll="onScroll"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(page, pageIndex) in pages"
|
||||||
|
:key="pageIndex"
|
||||||
|
class="app-icon-page"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="([id, pkg]) in page"
|
||||||
|
:key="id"
|
||||||
|
class="app-icon-item"
|
||||||
|
role="button"
|
||||||
|
:tabindex="0"
|
||||||
|
:aria-label="getTitle(id, pkg)"
|
||||||
|
@click="handleTap(id, pkg)"
|
||||||
|
@keydown.enter="handleTap(id, pkg)"
|
||||||
|
>
|
||||||
|
<!-- Icon with status indicator -->
|
||||||
|
<div class="app-icon-frame">
|
||||||
|
<img
|
||||||
|
:src="getIcon(id, pkg)"
|
||||||
|
:alt="getTitle(id, pkg)"
|
||||||
|
class="app-icon-img"
|
||||||
|
@error="handleImageError"
|
||||||
|
/>
|
||||||
|
<!-- Status dot -->
|
||||||
|
<span
|
||||||
|
v-if="pkg.state === 'running'"
|
||||||
|
class="app-icon-status app-icon-status-running"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-else-if="pkg.state === 'exited'"
|
||||||
|
class="app-icon-status app-icon-status-error"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-else-if="pkg.state === 'starting' || pkg.state === 'stopping' || pkg.state === 'installing'"
|
||||||
|
class="app-icon-status app-icon-status-transition"
|
||||||
|
></span>
|
||||||
|
<!-- Installing overlay -->
|
||||||
|
<div
|
||||||
|
v-if="serverStore.isInstalling(id)"
|
||||||
|
class="app-icon-installing"
|
||||||
|
>
|
||||||
|
<svg class="animate-spin h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Label -->
|
||||||
|
<span class="app-icon-label">{{ getTitle(id, pkg) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page dots -->
|
||||||
|
<div v-if="pages.length > 1" class="app-icon-dots">
|
||||||
|
<button
|
||||||
|
v-for="(_, i) in pages"
|
||||||
|
:key="i"
|
||||||
|
class="app-icon-dot"
|
||||||
|
:class="{ 'app-icon-dot-active': i === activePage }"
|
||||||
|
:aria-label="`Page ${i + 1}`"
|
||||||
|
@click="scrollToPage(i)"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useServerStore } from '@/stores/server'
|
||||||
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||||
|
import type { PackageDataEntry } from '@/types/api'
|
||||||
|
import { canLaunch, handleImageError } from './appsConfig'
|
||||||
|
import { getCuratedAppList } from '../discover/curatedApps'
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 16 // 4 columns x 4 rows
|
||||||
|
|
||||||
|
const serverStore = useServerStore()
|
||||||
|
const appLauncher = useAppLauncherStore()
|
||||||
|
|
||||||
|
const curatedMap = new Map(getCuratedAppList().map(a => [a.id, a]))
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
apps: [string, PackageDataEntry][]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
goToApp: [id: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
|
const activePage = ref(0)
|
||||||
|
|
||||||
|
const pages = computed(() => {
|
||||||
|
const result: [string, PackageDataEntry][][] = []
|
||||||
|
for (let i = 0; i < props.apps.length; i += ITEMS_PER_PAGE) {
|
||||||
|
result.push(props.apps.slice(i, i + ITEMS_PER_PAGE))
|
||||||
|
}
|
||||||
|
return result.length ? result : [[]]
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTitle(id: string, pkg: PackageDataEntry): string {
|
||||||
|
const t = pkg.manifest?.title
|
||||||
|
if (t && t !== id) return t
|
||||||
|
return curatedMap.get(id)?.title || t || id
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(id: string, pkg: PackageDataEntry): string {
|
||||||
|
const i = pkg['static-files']?.icon
|
||||||
|
return i || curatedMap.get(id)?.icon || `/assets/img/app-icons/${id}.png`
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTap(id: string, pkg: PackageDataEntry) {
|
||||||
|
if (canLaunch(pkg)) {
|
||||||
|
appLauncher.openSession(id)
|
||||||
|
} else {
|
||||||
|
emit('goToApp', id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
const el = scrollContainer.value
|
||||||
|
if (!el) return
|
||||||
|
const pageWidth = el.clientWidth
|
||||||
|
if (pageWidth === 0) return
|
||||||
|
activePage.value = Math.round(el.scrollLeft / pageWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToPage(index: number) {
|
||||||
|
const el = scrollContainer.value
|
||||||
|
if (!el) return
|
||||||
|
el.scrollTo({ left: index * el.clientWidth, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Persistent Mobile Tabs for Apps/Marketplace -->
|
<!-- Persistent Mobile Tabs for Apps/Marketplace -->
|
||||||
<div
|
<div
|
||||||
v-if="showAppsTabs"
|
v-if="showAppsTabs && !isAppSessionActive"
|
||||||
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2 glass-piece"
|
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2 glass-piece"
|
||||||
:class="{ 'glass-throw-mobile-tabs': showZoomIn }"
|
:class="{ 'glass-throw-mobile-tabs': showZoomIn }"
|
||||||
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
|
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
|
||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<!-- Persistent Mobile Tabs for Network/Cloud -->
|
<!-- Persistent Mobile Tabs for Network/Cloud -->
|
||||||
<div
|
<div
|
||||||
v-if="showNetworkTabs"
|
v-if="showNetworkTabs && !isAppSessionActive"
|
||||||
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2 glass-piece"
|
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2 glass-piece"
|
||||||
:class="{ 'glass-throw-mobile-tabs-2': showZoomIn }"
|
:class="{ 'glass-throw-mobile-tabs-2': showZoomIn }"
|
||||||
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
|
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
|
||||||
@ -58,8 +58,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Bottom Tab Bar -->
|
<!-- Mobile Bottom Tab Bar (hidden when app is open fullscreen) -->
|
||||||
<nav
|
<nav
|
||||||
|
v-if="!isAppSessionActive"
|
||||||
ref="mobileTabBar"
|
ref="mobileTabBar"
|
||||||
data-mobile-tab-bar
|
data-mobile-tab-bar
|
||||||
:aria-label="t('dashboard.mobileNav')"
|
:aria-label="t('dashboard.mobileNav')"
|
||||||
@ -74,7 +75,7 @@
|
|||||||
:to="item.path"
|
:to="item.path"
|
||||||
aria-current-value="page"
|
aria-current-value="page"
|
||||||
@click="appLauncher.closePanel()"
|
@click="appLauncher.closePanel()"
|
||||||
class="flex flex-col items-center justify-center w-full py-1.5 rounded-lg text-white/70 transition-all duration-300 relative z-10 gap-0.5"
|
class="flex items-center justify-center w-14 h-14 rounded-xl text-white/70 transition-all duration-300 relative z-10"
|
||||||
:class="{
|
:class="{
|
||||||
'nav-tab-active': item.isCombined
|
'nav-tab-active': item.isCombined
|
||||||
? (item.path === '/dashboard/apps'
|
? (item.path === '/dashboard/apps'
|
||||||
@ -99,17 +100,15 @@
|
|||||||
:d="path"
|
:d="path"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-[10px] leading-tight">{{ item.label }}</span>
|
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<!-- Chat launcher -->
|
<!-- Chat launcher -->
|
||||||
<button
|
<button
|
||||||
@click="router.push('/dashboard/chat')"
|
@click="router.push('/dashboard/chat')"
|
||||||
class="chat-launcher-btn-mobile flex flex-col items-center justify-center w-full py-1.5 rounded-lg transition-all duration-300 relative z-10 gap-0.5"
|
class="chat-launcher-btn-mobile flex items-center justify-center w-14 h-14 rounded-xl transition-all duration-300 relative z-10"
|
||||||
>
|
>
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path v-for="(path, index) in getIconPath('chat')" :key="index" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="path" />
|
<path v-for="(path, index) in getIconPath('chat')" :key="index" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="path" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-[10px] leading-tight">AIUI</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -141,6 +140,11 @@ const uiMode = useUIModeStore()
|
|||||||
|
|
||||||
const mobileTabBar = ref<HTMLElement | null>(null)
|
const mobileTabBar = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// Hide tab bar when an app session is open (fullscreen on mobile)
|
||||||
|
const isAppSessionActive = computed(() => {
|
||||||
|
return route.name === 'app-session' || !!appLauncher.panelAppId
|
||||||
|
})
|
||||||
|
|
||||||
// Show persistent tabs for Apps/Marketplace on mobile
|
// Show persistent tabs for Apps/Marketplace on mobile
|
||||||
const showAppsTabs = computed(() => {
|
const showAppsTabs = computed(() => {
|
||||||
if (typeof window === 'undefined') return false
|
if (typeof window === 'undefined') return false
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user