diff --git a/README.md b/README.md index e048cfa..9b339a6 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ trips** into one view for the Fireside Communications / Tracksolid fleet. (vehicle / cost centre / city) plus the **first trip** and **last trip** — each with its **reverse-geocoded location and timestamp** — alongside the KPI totals (trips, km, driving/idle hours, vehicles, drivers, date range). +- **Fixed chrome (no floating panels).** Filters are a **fixed dock on the right**; + in trips view the trips are **cards in a fixed bottom bar** (horizontally + scrollable) that mirrors the top bar. Click a card to fit + animate that route. Live: diff --git a/index.html b/index.html index 58c9f57..c0467a6 100644 --- a/index.html +++ b/index.html @@ -44,7 +44,11 @@ font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); overflow: hidden; } - .app { display: grid; grid-template-rows: auto auto 1fr; height: 100vh; } + .app { + display: grid; height: 100vh; width: 100vw; overflow: hidden; + grid-template-rows: auto auto 1fr; + grid-template-columns: minmax(0, 1fr); /* clamp so the trip-card scroller can't widen the page */ + } /* Trips context bar (second row, trips mode only): active filter + first/last trip bookends with reverse-geocoded location + timestamp. */ @@ -117,29 +121,34 @@ .clock .label { font-size: 9.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; } .clock b { font-weight: 600; } - /* ── Map + floating cards ──────────────────────────────────────────── */ - /* Pin to row 3 so the map always takes the 1fr track whether or not the - tripbar (row 2) is shown — otherwise a hidden tripbar lets the map fall - into the auto row and collapse. */ - #map { position: relative; min-height: 0; height: 100%; grid-row: 3; } + /* ── Body: map + fixed right filter dock + fixed bottom trips bar ───── */ header { grid-row: 1; } .tripbar { grid-row: 2; } + .body { + grid-row: 3; min-height: 0; + display: grid; + /* minmax(0,…) so the horizontal trip-card scroller can't blow out the track */ + grid-template-columns: minmax(0, 1fr) 270px; /* map | fixed filter dock */ + grid-template-rows: minmax(0, 1fr) auto; /* map | fixed trips bar */ + } + #map { grid-column: 1; grid-row: 1; position: relative; min-height: 0; } .placeholder { position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted); font-size: 13px; text-align: center; padding: 32px; pointer-events: none; z-index: 1; } - .floating { - position: absolute; z-index: 5; background: rgba(30,35,46,.95); - border: 1px solid var(--border); border-radius: 10px; - box-shadow: 0 8px 28px rgba(0,0,0,.45); backdrop-filter: blur(6px); + + /* Fixed right filter dock (full body height) */ + .filters { + grid-column: 2; grid-row: 1 / span 2; + background: var(--panel); border-left: 1px solid var(--border); + padding: 16px 16px 18px; overflow-y: auto; } - .filters { bottom: 22px; right: 16px; width: 250px; padding: 14px 14px 16px; } - .filters h3, .triplist h3 { + .filters h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .6px; - color: var(--muted); margin: 0 0 12px; font-weight: 600; + color: var(--muted); margin: 0 0 14px; font-weight: 600; } - .field { display: flex; flex-direction: column; margin-bottom: 11px; } + .field { display: flex; flex-direction: column; margin-bottom: 13px; } .field label { font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); margin-bottom: 4px; @@ -149,46 +158,57 @@ border: 1px solid var(--border); border-radius: 6px; font: 13px system-ui; width: 100%; } select:focus, input:focus { outline: 2px solid var(--accent); outline-offset: -1px; } - select[multiple] { min-height: 84px; max-height: 150px; padding: 5px; } + select[multiple] { min-height: 120px; max-height: 200px; padding: 5px; } select[multiple] option { padding: 4px 6px; border-radius: 3px; } select[multiple] option:checked { background: var(--accent); color: #1a1009; } .hint { font-size: 9.5px; color: var(--muted); margin-top: 3px; line-height: 1.3; } .custom { display: none; } .custom.show { display: block; } .btn { - width: 100%; padding: 9px; margin-top: 4px; background: var(--accent); + width: 100%; padding: 10px; margin-top: 6px; background: var(--accent); color: #1a1009; border: 0; border-radius: 6px; font: 600 13px system-ui; cursor: pointer; } .btn:hover { background: var(--accent-hover); } .btn:disabled { background: #4b5563; color: #cbd5e1; cursor: wait; } - .triplist { - bottom: 22px; left: 16px; width: 280px; max-height: calc(100% - 44px); - display: none; flex-direction: column; padding: 12px 12px 8px; + /* Fixed bottom trips bar — horizontal scrolling cards (mirrors the top bar) */ + .tripsbar { + grid-column: 1; grid-row: 2; display: none; + background: var(--panel); border-top: 1px solid var(--border); + flex-direction: column; min-width: 0; } - .triplist.show { display: flex; } - .triplist .back { + .tripsbar.show { display: flex; } + .tripsbar-head { + display: flex; align-items: center; gap: 14px; padding: 7px 16px 0; + } + .tripsbar-head .back { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; - color: var(--accent); font: 600 12px system-ui; background: none; border: 0; - padding: 0 0 10px; margin-bottom: 6px; border-bottom: 1px solid var(--border); + color: var(--accent); font: 600 12px system-ui; background: none; border: 0; padding: 0; } - .triplist .back:hover { color: var(--accent-hover); } - .triplist .scroll { overflow-y: auto; min-height: 0; } - .trip-row { - display: grid; grid-template-columns: 7px 24px 1fr auto; gap: 8px; - align-items: center; padding: 7px 0; border-bottom: 1px solid var(--border); - font-size: 12px; cursor: pointer; + .tripsbar-head .back:hover { color: var(--accent-hover); } + .tripsbar-title { + font-size: 11px; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); font-weight: 600; } - .trip-row:hover { background: rgba(255,255,255,.03); } - .trip-row.active { background: rgba(232,149,74,.14); } - .trip-row.active .seq, .trip-row.active .time { color: var(--accent); } - .trip-row .swatch { width: 4px; height: 22px; border-radius: 2px; } - .trip-row .seq { color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; } - .trip-row .veh { color: var(--text); font-weight: 500; } - .trip-row .time { color: var(--muted); font-size: 11px; } - .trip-row .km { font-variant-numeric: tabular-nums; color: var(--text); white-space: nowrap; } - .trips-empty { color: var(--muted); padding: 24px 0; text-align: center; font-size: 12px; } - .trips-more { color: var(--muted); padding: 10px 0 2px; text-align: center; font-size: 11px; } + .tripsbar-title span { color: var(--text); } + .trip-cards { + display: flex; gap: 10px; overflow-x: auto; overflow-y: hidden; + padding: 10px 16px 14px; scrollbar-width: thin; min-width: 0; + } + .trip-card { + flex: 0 0 auto; width: 150px; cursor: pointer; + background: var(--bg); border: 1px solid var(--border); + border-left: 4px solid var(--swatch, var(--muted)); border-radius: 8px; + padding: 8px 10px; display: flex; flex-direction: column; gap: 3px; + } + .trip-card:hover { border-color: var(--accent); background: var(--panel-2); } + .trip-card.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); } + .trip-card .tc-top { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; } + .trip-card .tc-seq { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 11px; } + .trip-card .tc-km { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; } + .trip-card .tc-veh { color: var(--text); font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .trip-card .tc-time { color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; } + .trips-empty { color: var(--muted); padding: 18px 4px; font-size: 12px; } + .trips-more { flex: 0 0 auto; align-self: center; color: var(--muted); font-size: 11px; padding: 0 12px; white-space: nowrap; } .error { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); @@ -338,24 +358,17 @@ -
-
Loading live fleet…
- - -
- -

Trips

-
-
No trips.
-
+
+
+
Loading live fleet…
- -
+ + + + +
+
+ + TRIPS +
+
+
No trips.
+
@@ -1038,11 +1062,12 @@ function switchToTripsMode() { clearTrail(); trailedVehicle = null; liveMarkers.forEach(m => m.getElement().style.display = 'none'); clusterMarkers.forEach(m => m.remove()); clusterMarkers.clear(); - document.getElementById('triplist').classList.add('show'); + document.getElementById('tripsbar').classList.add('show'); document.getElementById('tripbar').classList.add('show'); document.getElementById('live-pill').classList.add('show'); document.getElementById('stale-chip').style.display = 'none'; const ph = document.getElementById('placeholder'); if (ph) ph.style.display = 'none'; + if (map) setTimeout(() => map.resize(), 0); // map column shrank — let it reflow } // "YYYY-MM-DD HH:MM:SS" → "DD Mon HH:MM" (EAT, compact) for the bookends. @@ -1130,7 +1155,7 @@ function renderTrips(payload, sel) { } const b = new maplibregl.LngLatBounds(); g.features.forEach(f => Array.isArray(f.geometry?.coordinates) && f.geometry.coordinates.forEach(c => b.extend(c))); - if (!b.isEmpty()) map.fitBounds(b, { padding: { top: 50, right: 290, bottom: 50, left: 310 }, duration: 600, maxZoom: 14 }); + if (!b.isEmpty()) map.fitBounds(b, { padding: 50, duration: 600, maxZoom: 14 }); }; if (map.isStyleLoaded()) draw(); else map.once('load', draw); } @@ -1157,24 +1182,24 @@ function renderTripList(features) { const frag = document.createDocumentFragment(); features.slice(0, MAX_TRIPS_IN_LIST).forEach(f => { const p = f.properties || {}; - const row = document.createElement('div'); - row.className = 'trip-row'; - row.innerHTML = ` - - #${p.daily_seq ?? '?'} - ${escapeHtml(p.vehicle_number || '—')} - ${escapeHtml((p.start_time || '').slice(11, 16))}→${escapeHtml((p.end_time || '').slice(11, 16))} - ${Number(p.distance_km ?? 0).toLocaleString()} km`; - row.addEventListener('click', () => { + const card = document.createElement('div'); + card.className = 'trip-card'; + card.style.setProperty('--swatch', p.color || 'var(--muted)'); + card.innerHTML = ` +
#${p.daily_seq ?? '?'} + ${Number(p.distance_km ?? 0).toLocaleString()} km
+
${escapeHtml(p.vehicle_number || '—')}
+
${escapeHtml((p.start_time || '').slice(11, 16))} → ${escapeHtml((p.end_time || '').slice(11, 16))}
`; + card.addEventListener('click', () => { const coords = f.geometry?.coordinates; if (!Array.isArray(coords) || !coords.length || !map) return; - document.querySelectorAll('.trip-row.active').forEach(r => r.classList.remove('active')); - row.classList.add('active'); + document.querySelectorAll('.trip-card.active').forEach(r => r.classList.remove('active')); + card.classList.add('active'); const b = new maplibregl.LngLatBounds(); coords.forEach(c => b.extend(c)); - map.fitBounds(b, { padding: { top: 60, right: 300, bottom: 60, left: 320 }, duration: 500, maxZoom: 15 }); + map.fitBounds(b, { padding: 60, duration: 500, maxZoom: 15 }); setTimeout(() => animateTrip(f), 550); }); - frag.appendChild(row); + frag.appendChild(card); }); scroll.appendChild(frag); if (features.length > MAX_TRIPS_IN_LIST) { @@ -1236,11 +1261,12 @@ function showTripPlaceholder(msg) { function backToLive() { mode = 'live'; clearAnim(); removeTripLayers(); - document.getElementById('triplist').classList.remove('show'); + document.getElementById('tripsbar').classList.remove('show'); document.getElementById('tripbar').classList.remove('show'); document.getElementById('live-pill').classList.remove('show'); document.getElementById('stale-chip').style.display = ''; liveMarkers.forEach(m => m.getElement().style.display = ''); + if (map) setTimeout(() => map.resize(), 0); // bottom bar gone — map grows back if (liveFeatures.length) applyLiveFilters(); // rebuild cluster bubbles/pins const ph = document.getElementById('placeholder'); if (ph) ph.style.display = 'none'; startPolling();