![]()
audioPlaying.value && currentSrc.value
// Uniform card cover ratio across every file type so folders, images, videos
// and documents all render at the same height in the grid (previously images/
// videos were square while folders were 4/3, giving a ragged, mismatched grid).
-const aspectClass = computed(() => 'aspect-[4/3]')
+// Aspect is now driven entirely by .cloud-grid-card-cover CSS (4/3 desktop,
+// square on mobile) so the ratio is deterministic regardless of Tailwind layer
+// ordering.
const coverBg = computed(() => {
if (props.item.isDir) return 'bg-amber-500/10'
diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css
index 4677bdf6..90e919f1 100644
--- a/neode-ui/src/style.css
+++ b/neode-ui/src/style.css
@@ -1827,6 +1827,22 @@ html.modal-scroll-locked .dashboard-scroll-panel {
}
}
+/* Mobile: square, tappable tiles + bottom clearance so the last row scrolls
+ above the tab bar / back button (matches .mobile-scroll-pad). */
+@media (max-width: 767px) {
+ .cloud-card-grid,
+ .cloud-file-list {
+ padding-bottom: calc(
+ var(--mobile-tab-bar-height, 88px) +
+ var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) +
+ var(--audio-player-height, 0px) + 24px
+ );
+ }
+ .cloud-grid-card-cover {
+ aspect-ratio: 1 / 1;
+ }
+}
+
.cloud-grid-card {
display: flex;
flex-direction: column;
@@ -1856,6 +1872,9 @@ html.modal-scroll-locked .dashboard-scroll-panel {
.cloud-grid-card-cover {
position: relative;
width: 100%;
+ /* Fallback aspect when the Tailwind aspect-[4/3] utility is unavailable, so
+ the cover never collapses to zero height. */
+ aspect-ratio: 4 / 3;
overflow: hidden;
border-radius: 0.625rem;
}
diff --git a/neode-ui/src/views/Dashboard.vue b/neode-ui/src/views/Dashboard.vue
index 34d85cc4..bc6b1580 100644
--- a/neode-ui/src/views/Dashboard.vue
+++ b/neode-ui/src/views/Dashboard.vue
@@ -290,14 +290,19 @@ function activeNetKey(): string {
let touchStartX = 0
let touchStartY = 0
let touchStartTime = 0
+let swipeSuppressed = false
function onContentTouchStart(e: TouchEvent) {
const t = e.touches[0]
if (!t) return
+ // Don't begin a tab swipe when the gesture starts on an app icon — let the
+ // icon handle the tap/long-press. Swiping anywhere else still changes tabs.
+ swipeSuppressed = !!(e.target instanceof Element && e.target.closest('.app-icon-item'))
touchStartX = t.clientX
touchStartY = t.clientY
touchStartTime = e.timeStamp
}
function onContentTouchEnd(e: TouchEvent) {
+ if (swipeSuppressed) { swipeSuppressed = false; return }
const t = e.changedTouches[0]
if (!t) return
const dx = t.clientX - touchStartX