diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css
index ab52226a..de7ab005 100644
--- a/neode-ui/src/style.css
+++ b/neode-ui/src/style.css
@@ -155,6 +155,39 @@ select:focus-visible {
}
}
+@media (max-width: 920px) {
+ .dashboard-view [data-controller-zone="sidebar"] {
+ display: none !important;
+ }
+
+ .dashboard-view .mobile-top-tabs,
+ .dashboard-view [data-mobile-tab-bar] {
+ display: block !important;
+ }
+
+ .dashboard-view .dashboard-scroll-panel {
+ padding-left: 1rem !important;
+ padding-right: 1rem !important;
+ }
+
+ .mobile-scroll-pad {
+ padding-bottom: calc(var(--mobile-tab-bar-height, 88px) + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + 16px);
+ }
+
+ .mobile-scroll-pad-back {
+ padding-bottom: calc(var(--mobile-tab-bar-height, 88px) + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + 64px);
+ }
+
+ .mobile-safe-top {
+ padding-top: calc(var(--safe-area-top, env(safe-area-inset-top, 0px)) + 16px);
+ }
+
+ .marketplace-container .grid,
+ .discover-container .grid {
+ grid-template-columns: minmax(0, 1fr) !important;
+ }
+}
+
/* Haptic-like press feedback for all interactive elements */
button:active:not(:disabled),
[role="button"]:active,
@@ -281,12 +314,6 @@ input[type="radio"]:active + * {
display: none;
}
- @media (min-width: 768px) and (max-width: 920px) {
- .app-header-inline-tabs {
- display: flex;
- }
- }
-
@media (min-width: 921px) {
.app-header-desktop {
display: flex;
@@ -297,6 +324,24 @@ input[type="radio"]:active + * {
}
}
+ .apps-icon-grid-mobile {
+ display: block;
+ }
+
+ .apps-card-grid-desktop {
+ display: none;
+ }
+
+ @media (min-width: 921px) {
+ .apps-icon-grid-mobile {
+ display: none;
+ }
+
+ .apps-card-grid-desktop {
+ display: grid;
+ }
+ }
+
.category-tabs-wide {
flex: 0 1 auto;
min-width: 0;
diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue
index 394ea18b..dfcc1a22 100644
--- a/neode-ui/src/views/Apps.vue
+++ b/neode-ui/src/views/Apps.vue
@@ -203,7 +203,7 @@
-
+
-
+
(null)
+const MOBILE_LAYOUT_MAX_WIDTH = 920
+const viewportWidth = ref(typeof window === 'undefined' ? 1024 : window.innerWidth)
// App sessions own their mobile controls. Normal mobile launches use the route
// session; keeping this guard also protects any desktop-panel state on resize.
@@ -148,14 +150,14 @@ const isAppSessionActive = computed(() => route.name === 'app-session')
// Show persistent tabs for Apps/Marketplace on mobile
const showAppsTabs = computed(() => {
if (typeof window === 'undefined') return false
- if (window.innerWidth >= 768) return false
+ if (viewportWidth.value > MOBILE_LAYOUT_MAX_WIDTH) return false
return route.path.includes('/apps') || route.path.includes('/marketplace') || route.path.includes('/discover')
})
// Show persistent tabs for Network/Cloud on mobile
const showNetworkTabs = computed(() => {
if (typeof window === 'undefined') return false
- if (window.innerWidth >= 768) return false
+ if (viewportWidth.value > MOBILE_LAYOUT_MAX_WIDTH) return false
if (route.name === 'cloud-folder') return false
return route.path.includes('/server') || route.path.includes('/cloud') || route.path.includes('/web5') || route.path.includes('/mesh')
})
@@ -171,7 +173,7 @@ function readSafeAreaTop() {
}
const mobileTabPaddingTop = computed(() => {
- if (typeof window === 'undefined' || window.innerWidth >= 768) return 0
+ if (typeof window === 'undefined' || viewportWidth.value > MOBILE_LAYOUT_MAX_WIDTH) return 0
const sat = safeAreaTop.value
if (showAppsTabs.value && showNetworkTabs.value) return 160 + sat
if (showAppsTabs.value || showNetworkTabs.value) return 80 + sat
@@ -193,6 +195,7 @@ function updateTabBarHeight() {
}
function onResize() {
+ viewportWidth.value = window.innerWidth
updateTabBarHeight()
}
diff --git a/scripts/deploy-tailscale.sh b/scripts/deploy-tailscale.sh
index 078ea30f..3098bcee 100755
--- a/scripts/deploy-tailscale.sh
+++ b/scripts/deploy-tailscale.sh
@@ -147,6 +147,7 @@ deploy_node() {
step "Syncing code"
rsync -az --delete \
--exclude='.git' --exclude='node_modules' --exclude='target/debug' \
+ --exclude='.codex-target-*' --exclude='.codex-tmp' \
--exclude='target/release/deps' --exclude='target/release/build' \
--exclude='target/release/.fingerprint' --exclude='target/release/incremental' \
--exclude='web/dist' --exclude='.DS_Store' --exclude='image-recipe/build' \
diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh
index a98b02bf..306a80de 100755
--- a/scripts/deploy-to-target.sh
+++ b/scripts/deploy-to-target.sh
@@ -164,6 +164,7 @@ if [[ "$DRY_RUN" == "true" ]]; then
echo "Files that would be synced:"
rsync -avn --exclude '.git' --exclude 'target' --exclude 'node_modules' \
--exclude 'dist' --exclude 'web/dist' --exclude '*.iso' \
+ --exclude '.codex-target-*' --exclude '.codex-tmp' \
"$PROJECT_DIR/" "$TARGET_HOST:$TARGET_DIR/" 2>/dev/null | \
grep -E '^[<>]|^deleting' | head -50 || echo " (rsync check failed — SSH may be unavailable)"
echo ""
@@ -546,6 +547,8 @@ rsync -avz --delete \
--exclude 'target' \
--exclude 'dist' \
--exclude '.git' \
+ --exclude '.codex-target-*' \
+ --exclude '.codex-tmp' \
--exclude 'image-recipe/build' \
--exclude 'image-recipe/results' \
"$PROJECT_DIR/" "$TARGET_HOST:$TARGET_DIR/"