fix recording and revert nav tab

This commit is contained in:
Prabhaav Pillai
2026-09-18 20:04:20 -04:00
parent b295a57281
commit 990e68804c
6 changed files with 166 additions and 75 deletions
@@ -98,6 +98,7 @@
--appbar-height: 64px;
--bottomnav-height: 72px;
--bottomnav-clearance: calc(var(--bottomnav-height) + env(safe-area-inset-bottom, 0px) + var(--sp-5));
--touch-target: 44px;
--z-appbar: 100;
--z-drawer: 200;
@@ -1387,17 +1388,29 @@ button.gx-chip:hover {
}
}
.gx-content:has(.gx-navigation-view) {
margin-top: calc(-1 * (var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-1)));
padding: 0;
}
.gx-navigation-view {
min-height: 0;
position: relative;
width: 100%;
}
.gx-navigation-stage {
background: var(--surface-container-low);
min-height: calc(100dvh - var(--appbar-height));
height: 100dvh;
min-height: 0;
overflow: hidden;
position: relative;
width: 100%;
}
.gx-navigation-map {
height: calc(100dvh - var(--appbar-height));
min-height: 520px;
height: 100%;
min-height: 0;
width: 100%;
}
@@ -1422,7 +1435,10 @@ button.gx-chip:hover {
.gx-navigation-overlay {
inset: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: var(--sp-4);
padding-top: calc(var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-4));
pointer-events: none;
position: absolute;
z-index: 2;
@@ -1432,6 +1448,19 @@ button.gx-chip:hover {
pointer-events: auto;
}
.gx-navigation-tabs {
pointer-events: none;
position: absolute;
right: var(--sp-4);
top: calc(var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-4));
z-index: 3;
}
.gx-navigation-tabs .gx-tabs {
margin-bottom: 0;
pointer-events: auto;
}
.gx-navigation-search,
.gx-navigation-summary,
.gx-navigation-recent {
@@ -1527,87 +1556,98 @@ button.gx-chip:hover {
}
.gx-navigation-summary {
margin: var(--sp-4) 0 0;
padding: var(--sp-3);
margin: var(--sp-3) 0 0;
padding: var(--sp-2);
}
.gx-navigation-summary__title {
background: var(--surface-container-high);
border-radius: var(--radius-md);
font-size: var(--fs-lg);
align-items: center;
display: flex;
gap: var(--sp-1);
padding: 0 var(--sp-1);
}
.gx-navigation-summary__name {
font-size: var(--fs-base);
font-weight: var(--fw-bold);
min-width: 0;
overflow: hidden;
padding: var(--sp-2) var(--sp-3);
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.gx-navigation-summary__rows {
display: grid;
gap: var(--sp-1);
padding: var(--sp-3) var(--sp-2);
.gx-navigation-summary__fav {
color: var(--text-muted);
flex: none;
}
.gx-navigation-summary__rows > div {
align-items: center;
.gx-navigation-summary__fav.active { color: var(--accent-rose); }
.gx-navigation-metrics {
display: grid;
gap: var(--sp-2);
grid-template-columns: 28px 90px 1fr;
min-height: 28px;
grid-template-columns: repeat(3, minmax(0, 1fr));
padding: var(--sp-2) 0;
}
.gx-navigation-summary__rows strong {
font-weight: var(--fw-bold);
}
.gx-navigation-summary__icon {
font-size: 1.15rem;
.gx-navigation-metric {
align-items: center;
background: var(--surface-container-high);
border-radius: var(--radius-sm);
display: grid;
gap: 2px;
justify-items: center;
min-width: 0;
padding: var(--sp-2) var(--sp-1);
text-align: center;
}
.gx-navigation-route-picker {
border-top: 1px solid var(--outline);
display: grid;
gap: var(--sp-2);
margin-top: var(--sp-2);
padding: var(--sp-3) var(--sp-2) 0;
.gx-navigation-metric i {
color: var(--primary);
font-size: 1rem;
}
.gx-navigation-route-picker__title {
.gx-navigation-metric span {
color: var(--text-muted);
font-size: var(--fs-xs);
}
.gx-navigation-metric strong {
font-size: var(--fs-sm);
font-weight: var(--fw-bold);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.gx-navigation-route-picker {
display: flex;
gap: var(--sp-2);
overflow-x: auto;
padding: var(--sp-1) 0 var(--sp-2);
scrollbar-width: none;
}
.gx-navigation-route-picker::-webkit-scrollbar { display: none; }
.gx-navigation-route-option {
align-items: center;
background: var(--surface-container);
border: 1px solid var(--outline);
border-radius: var(--radius-sm);
border-radius: var(--radius-full);
color: var(--on-surface);
cursor: pointer;
display: flex;
gap: var(--sp-3);
justify-content: space-between;
min-height: 52px;
padding: var(--sp-2) var(--sp-3);
text-align: left;
width: 100%;
}
.gx-navigation-route-option > span {
display: grid;
gap: 2px;
min-width: 0;
display: inline-flex;
flex: 0 0 auto;
gap: 6px;
min-height: 36px;
padding: 0 var(--sp-3);
}
.gx-navigation-route-option small {
color: var(--text-muted);
font-size: var(--fs-xs);
}
.gx-navigation-route-option > strong:last-child {
white-space: nowrap;
}
@@ -1618,19 +1658,15 @@ button.gx-chip:hover {
border-color: var(--primary);
}
.gx-navigation-route-option.selected > strong:first-child {
color: var(--primary);
}
.gx-navigation-route-option.selected { color: var(--primary); }
.gx-navigation-summary__actions {
display: flex;
flex-wrap: wrap;
gap: var(--sp-2);
justify-content: center;
}
.gx-navigation-summary__actions .gx-btn {
flex: 1 1 190px;
flex: 1;
min-width: 0;
}
@@ -1638,9 +1674,6 @@ button.gx-chip:hover {
.gx-btn--success:hover { background: #6df3e9; }
.gx-btn--danger { background: var(--error); color: var(--on-error); }
.gx-btn--danger:hover { background: #ff7492; }
.gx-btn--favorite { background: var(--accent-rose); color: #fff; }
.gx-btn--favorite:hover,
.gx-btn--favorite.active { background: #ed4f94; }
.gx-navigation-recent {
margin: var(--sp-4) 0 0;
@@ -1658,7 +1691,7 @@ button.gx-chip:hover {
gap: var(--sp-3);
justify-content: center;
margin: 0;
min-height: calc(100dvh - var(--appbar-height));
min-height: 100%;
padding: var(--sp-6);
text-align: center;
}
@@ -1671,11 +1704,29 @@ button.gx-chip:hover {
@media (max-width: 767px) {
.gx-navigation-overlay {
padding: var(--sp-3);
padding-top: calc(var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-3));
padding-bottom: calc(var(--bottomnav-clearance) + 56px);
}
.gx-navigation-summary__rows > div {
grid-template-columns: 28px 82px 1fr;
.gx-navigation-tabs {
bottom: var(--bottomnav-clearance);
display: flex;
justify-content: center;
left: var(--sp-3);
right: var(--sp-3);
top: auto;
}
.gx-navigation-tabs .gx-tabs {
flex-wrap: nowrap;
justify-content: flex-start;
max-width: 100%;
overflow-x: auto;
padding: 2px;
scrollbar-width: none;
}
.gx-navigation-tabs .gx-tabs::-webkit-scrollbar { display: none; }
}
.gx-nav-section { margin-bottom: var(--sp-3); }
@@ -6,7 +6,7 @@ import { Tools } from "./views/Tools.js"
import { Recordings } from "./views/Recordings.js"
import { Logs } from "./views/Logs.js"
import { Tuning } from "./views/Tuning.js"
import { Navigation } from "./views/Navigation.js?v=nav-destination-4"
import { Navigation } from "./views/Navigation.js?v=nav-destination-6"
import { Vehicle } from "./views/Vehicle.js"
import { Bluetooth } from "./views/Bluetooth.js"
import { SystemTools } from "./views/SystemTools.js"
@@ -447,26 +447,29 @@ export const NavigationDestinationPanel = {
</section>
<section v-if="destination" class="gx-navigation-summary gx-card">
<div class="gx-navigation-summary__title">{{ destination.name || query || 'Destination' }}</div>
<div v-if="routeSummary" class="gx-navigation-summary__rows">
<div><span class="gx-navigation-summary__icon">🛣️</span><span>Distance:</span><strong>{{ formatDistance(routeSummary.distance) }}</strong></div>
<div><span class="gx-navigation-summary__icon">⌛</span><span>Duration:</span><strong>{{ formatDuration(routeSummary.duration) }}</strong></div>
<div><span class="gx-navigation-summary__icon">🕗</span><span>ETA:</span><strong>{{ formatEta(routeSummary.duration) }}</strong></div>
<div class="gx-navigation-summary__title">
<span class="gx-navigation-summary__name">{{ destination.name || query || 'Destination' }}</span>
<button type="button" class="gx-icon-btn gx-navigation-summary__fav" :class="{ active: isFavorite }" :aria-pressed="isFavorite" :title="isFavorite ? 'Remove from favorites' : 'Add to favorites'" @click="toggleFavorite">
<i class="bi" :class="isFavorite ? 'bi-heart-fill' : 'bi-heart'"></i>
</button>
</div>
<div v-if="routeSummary" class="gx-navigation-metrics">
<div class="gx-navigation-metric"><i class="bi bi-signpost-2" aria-hidden="true"></i><span>Distance</span><strong>{{ formatDistance(routeSummary.distance) }}</strong></div>
<div class="gx-navigation-metric"><i class="bi bi-clock" aria-hidden="true"></i><span>Duration</span><strong>{{ formatDuration(routeSummary.duration) }}</strong></div>
<div class="gx-navigation-metric"><i class="bi bi-clock-history" aria-hidden="true"></i><span>ETA</span><strong>{{ formatEta(routeSummary.duration) }}</strong></div>
</div>
<div v-if="routes.length > 1" class="gx-navigation-route-picker" aria-label="Choose a route">
<div class="gx-navigation-route-picker__title">Routes</div>
<button v-for="(route, index) in routes" :key="routeId(index)" type="button"
class="gx-navigation-route-option" :class="{ selected: selectedRouteId === routeId(index) }"
:aria-pressed="selectedRouteId === routeId(index)" :aria-label="'Select route ' + (index + 1)"
@click="selectRoute(route, routeId(index))">
<span><strong>Route {{ index + 1 }}</strong><small>{{ selectedRouteId === routeId(index) ? 'Selected' : (index === 0 ? 'Recommended' : 'Alternative') }}</small></span>
<strong>{{ formatDistance(route.distance) }} · {{ formatDuration(route.duration) }}</strong>
<strong>Route {{ index + 1 }}</strong>
<small>{{ formatDistance(route.distance) }} · {{ formatDuration(route.duration) }}</small>
</button>
</div>
<div class="gx-navigation-summary__actions">
<button v-if="navigationStarted" type="button" class="gx-btn gx-btn--danger" @click="cancelNavigation"><i class="bi bi-x-lg"></i> Cancel Navigation</button>
<button v-else type="button" class="gx-btn gx-btn--success" :disabled="loadingRoute || !hasRoutingKey" :title="hasRoutingKey ? 'Start Navigation' : 'A Mapbox secret key is required to start navigation'" @click="setDestination(destination)"><i class="bi bi-sign-turn-right"></i> {{ loadingRoute ? 'Calculating...' : 'Start Navigation' }}</button>
<button type="button" class="gx-btn gx-btn--favorite" :class="{ active: isFavorite }" @click="toggleFavorite"><i class="bi" :class="isFavorite ? 'bi-heart-fill' : 'bi-heart'"></i> {{ isFavorite ? 'Unfavorite' : 'Favorite' }}</button>
</div>
</section>
@@ -1,4 +1,4 @@
import { NavigationDestinationPanel } from "../components/NavigationDestinationPanel.js?v=nav-destination-4"
import { NavigationDestinationPanel } from "../components/NavigationDestinationPanel.js?v=nav-destination-6"
import { MapsPanel } from "../components/MapsPanel.js"
import { NavigationKeysPanel } from "../components/NavigationKeysPanel.js"
import { SpeedLimitsPanel } from "../components/SpeedLimitsPanel.js"
@@ -22,10 +22,15 @@ export const Navigation = {
})
},
template: `
<div class="gx-view">
<template v-if="tab === 'nav'">
<div class="gx-navigation-view">
<NavigationDestinationPanel />
<div class="gx-navigation-tabs"><GalaxyTabs :items="TABS" :active="tab" @select="selectTab" /></div>
</div>
</template>
<div v-else class="gx-view">
<h2 style="margin-top:0;">Navigation & Maps</h2>
<GalaxyTabs :items="TABS" :active="tab" @select="selectTab" />
<template v-if="tab === 'nav'"><NavigationDestinationPanel /></template>
<template v-if="tab === 'maps'"><MapsPanel /></template>
<template v-if="tab === 'keys'"><NavigationKeysPanel /></template>
<template v-if="tab === 'speeds'"><SpeedLimitsPanel /></template>
@@ -99,6 +99,7 @@ export const Recordings = {
if (this.sub === "screen" && !this.recordings.length && !this.screenLoading) this.loadScreenRecordings()
},
screenDisplayName(rec) {
if (!rec) return ""
return rec.is_custom_name ? rec.filename.replace(/\.mp4$/i, "").replace(/_/g, " ") : formatScreenDate(rec.timestamp)
},
async loadRoutes() {
@@ -502,8 +502,8 @@ def test_ui_all_remaining_classic_tools_native_no_embed():
assert "GalaxyEmbed" not in tuning and "LateralTuningPanel" in tuning
assert _read("js/components/MapsPanel.js") and _read("js/components/NavigationKeysPanel.js")
destination = _read("js/components/NavigationDestinationPanel.js")
assert '"./views/Navigation.js?v=nav-destination-4"' in _read("js/app.js")
assert '"../components/NavigationDestinationPanel.js?v=nav-destination-4"' in _read("js/views/Navigation.js")
assert '"./views/Navigation.js?v=nav-destination-6"' in _read("js/app.js")
assert '"../components/NavigationDestinationPanel.js?v=nav-destination-6"' in _read("js/views/Navigation.js")
assert "mapboxSuggest" in destination and "mapboxRetrieve" in destination
assert "mapboxGeocode" in destination and "mapboxDirections" in destination
assert "ref=\"map\"" in destination and "setNavigation(this.destination)" in destination
@@ -618,6 +618,7 @@ def test_ui_mobile_polish_regressions():
assert 'localDeviceUrl(status?.lanIp, "/recordings")' in recordings
assert 'localDeviceUrl(status?.lanIp, "/galaxy")' in galaxy
assert "gx-btn gx-btn--tonal" in recordings and "Open Recordings Locally" in recordings
assert "screenDisplayName(rec) {\n if (!rec) return" in recordings
assert "gx-btn gx-btn--tonal" in galaxy and "Open Galaxy Locally" in galaxy
home = _read("js/views/Home.js")
@@ -856,3 +857,33 @@ console.log("hierarchy logic OK")
result = subprocess.run([node, str(script)], capture_output=True, text=True)
assert result.returncode == 0, f"node failed:\n{result.stdout}\n{result.stderr}"
def test_ui_navigation_map_first_layout_regressions():
nav = _read("js/views/Navigation.js")
css = _read("css/material.css")
destination = _read("js/components/NavigationDestinationPanel.js")
# Destination tab is a map-first screen: the map fills the content area and
# runs behind the top appbar and the tab switcher floats above the bottom nav.
assert 'class="gx-navigation-view"' in nav and "gx-navigation-tabs" in nav
assert ".gx-content:has(.gx-navigation-view)" in css
assert "--bottomnav-clearance:" in css
assert "margin-top: calc(-1 * (var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-1)))" in css
assert "height: 100dvh" in css
assert "padding-top: calc(var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-4))" in css
assert ".gx-navigation-tabs {" in css
assert "bottom: var(--bottomnav-clearance)" in css
assert "padding-bottom: calc(var(--bottomnav-clearance) + 56px)" in css
# Route metrics use Bootstrap Icons consistently, not emoji.
assert "bi-signpost-2" in destination and "bi-clock-history" in destination
for emoji in ("🛣️", "⌛", "🕗"):
assert emoji not in destination
# The route summary stays compact so the map remains visible: metrics are a
# horizontal strip and route options are compact chips, not full-width rows.
assert 'class="gx-navigation-metrics"' in destination
assert destination.count('class="gx-navigation-metric"') == 3
assert "gx-navigation-summary__rows" not in destination and "gx-navigation-summary__rows" not in css
assert "grid-template-columns: repeat(3, minmax(0, 1fr))" in css