Split the trip page into tabs and move the header into the navbar

Three deep-linkable tabs (#/trip/:id/:tab): Trip (countdown, calendar,
map, summary), Money (costs + expenses side by side), Checklist. Panels
stay mounted; switching toggles visibility and updates the URL via
history.replaceState so an open day editor and the live tickers are
undisturbed. The Leaflet map revalidates its size on every trip-tab
activation but only auto-fits once, when its container first gains
real size - later visits keep the user's pan/zoom.

The full-width trip-header card is gone: the navbar now carries a
compact identity (back link, truncated name, date/days/currency) plus
a single overflow menu holding members, join code, edit (now a
centered modal) and delete. Dead header CSS removed; new styles live
in tabs.css.
This commit is contained in:
2026-08-30 13:31:32 +02:00
parent b0bdd2570c
commit b361fb2687
7 changed files with 397 additions and 138 deletions
+29 -6
View File
@@ -17,6 +17,13 @@ export function renderMap(tctx) {
const route = tctx.route || { stops: [], legs: [], totalKm: 0 };
const stops = route.stops || [];
// Leaflet initialised while its tab panel is hidden gets a zero-size
// container; tripDetail calls this on every activation of the trip tab so
// the map (and its fitBounds zoom, computed against whatever size the
// container had at creation time) catches up. No-op until the map actually
// exists — overwritten at the end of initMap() below.
tctx.mapInvalidate = () => {};
const section = el('section', { class: 'card map-section' });
section.appendChild(
el(
@@ -54,7 +61,7 @@ export function renderMap(tctx) {
// Leaflet needs the container attached with a real size, so init on the
// next tick after this section is mounted into the page.
setTimeout(() => initMap(mapDiv, stops, legs, kmEls), 0);
setTimeout(() => initMap(mapDiv, stops, legs, kmEls, tctx), 0);
return section;
}
@@ -75,7 +82,7 @@ function visibleLegs(route, stops) {
return result;
}
function initMap(mapDiv, stops, legs, kmEls) {
function initMap(mapDiv, stops, legs, kmEls, tctx) {
const L = window.L;
if (!L) {
mapDiv.appendChild(el('p', { class: 'muted' }, 'Map library failed to load.'));
@@ -126,13 +133,29 @@ function initMap(mapDiv, stops, legs, kmEls) {
return { leg, a, b, line, label, color };
});
if (points.length === 1) {
map.setView(points[0], 10);
} else {
map.fitBounds(L.latLngBounds(points).pad(0.2));
function fit() {
if (points.length === 1) map.setView(points[0], 10);
else map.fitBounds(L.latLngBounds(points).pad(0.2));
}
fit();
map.invalidateSize();
// A container sized 0 at creation time (hidden tab panel) gives fitBounds a
// bogus zoom that invalidateSize() alone won't correct, so the first
// activation after the container actually gets a real size must re-fit.
// But re-fitting on EVERY activation would silently discard any pan/zoom
// the user did on a previous visit to the tab — so fit() only runs once,
// the first time the container transitions from zero-width to sized.
let fitted = mapDiv.offsetWidth > 0;
tctx.mapInvalidate = () => {
if (!mapDiv.isConnected) return;
map.invalidateSize();
if (!fitted && mapDiv.offsetWidth > 0) {
fit();
fitted = true;
}
};
fetchRoadGeometry(L, map, mapDiv, legLayers, kmEls);
}