Add mobile bottom-sheet layout

On narrow screens the sidebar collapses to a 48px handle strip at the
bottom of the screen. Tapping the handle slides the panel up to 65vh,
revealing the full stop list and controls. The map takes the full
viewport width when the panel is closed or peek-visible.

The handle label updates dynamically to show the loaded route name.
The panel auto-opens when a route or route_master finishes loading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-02-28 10:02:14 +00:00
parent 2a82a9e8fa
commit 6927efca54
3 changed files with 90 additions and 0 deletions

View file

@ -30,6 +30,23 @@ let activeSlot = 'from'; // 'from' | 'to' | null
let selectedFrom = null; // stop name string or null let selectedFrom = null; // stop name string or null
let selectedTo = null; // stop name string or null let selectedTo = null; // stop name string or null
// ── Mobile panel ───────────────────────────────────────────────────────────
function isMobile() { return window.innerWidth < 768; }
function openPanel() {
document.getElementById('sidebar').classList.add('panel-open');
}
function updateHandleLabel(text) {
const el = document.getElementById('sidebar-handle-label');
if (el) el.textContent = text;
}
document.getElementById('sidebar-handle').addEventListener('click', () => {
document.getElementById('sidebar').classList.toggle('panel-open');
});
// ── Helpers ──────────────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────────────
/** /**
@ -351,6 +368,8 @@ async function loadRoute(relationId) {
document.getElementById('route-panel').classList.remove('d-none'); document.getElementById('route-panel').classList.remove('d-none');
document.getElementById('route-name').textContent = data.name; document.getElementById('route-name').textContent = data.name;
document.title = `${data.name} ${DEFAULT_TITLE}`; document.title = `${data.name} ${DEFAULT_TITLE}`;
updateHandleLabel(data.name);
if (isMobile()) openPanel();
document.getElementById('route-osm-link').href = document.getElementById('route-osm-link').href =
`https://www.openstreetmap.org/relation/${relationId}`; `https://www.openstreetmap.org/relation/${relationId}`;
@ -440,6 +459,8 @@ async function loadRouteMaster(relationId) {
document.getElementById('route-master-panel').classList.remove('d-none'); document.getElementById('route-master-panel').classList.remove('d-none');
document.getElementById('route-master-name').textContent = data.name; document.getElementById('route-master-name').textContent = data.name;
document.title = `${data.name} ${DEFAULT_TITLE}`; document.title = `${data.name} ${DEFAULT_TITLE}`;
updateHandleLabel(data.name);
if (isMobile()) openPanel();
document.getElementById('route-master-osm-link').href = document.getElementById('route-master-osm-link').href =
`https://www.openstreetmap.org/relation/${relationId}`; `https://www.openstreetmap.org/relation/${relationId}`;

View file

@ -10,10 +10,69 @@ html, body {
#sidebar { #sidebar {
height: 100%; height: 100%;
min-height: 0; /* flex items default to min-height:auto, preventing overflow scrolling */ min-height: 0; /* flex items default to min-height:auto, preventing overflow scrolling */
overflow: hidden;
display: flex;
flex-direction: column;
}
#sidebar-inner {
overflow-y: auto; overflow-y: auto;
flex: 1;
padding: 1rem 1rem 0; /* no bottom padding — WebKit ignores it on overflow containers */ padding: 1rem 1rem 0; /* no bottom padding — WebKit ignores it on overflow containers */
} }
/* ── Mobile bottom sheet ───────────────────────────────── */
#sidebar-handle {
height: 48px;
flex-shrink: 0;
cursor: pointer;
user-select: none;
border-bottom: 1px solid #f0f0f0;
background: #fff;
border-radius: 16px 16px 0 0;
}
#sidebar-handle-pill {
width: 36px;
height: 4px;
border-radius: 2px;
background: #ced4da;
flex-shrink: 0;
}
@media (max-width: 767.98px) {
#sidebar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100% !important;
max-width: 100% !important;
flex: none !important;
height: 65vh;
max-height: calc(100vh - 56px);
min-height: unset;
transform: translateY(calc(100% - 48px));
transition: transform 0.3s ease;
z-index: 1000;
border-radius: 16px 16px 0 0;
border-right: none !important;
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.15);
background: #fff;
}
#sidebar.panel-open {
transform: translateY(0);
}
#map {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
}
#map { #map {
height: 100%; height: 100%;
} }

View file

@ -23,6 +23,15 @@
<!-- Sidebar --> <!-- Sidebar -->
<div class="col-3 border-end" id="sidebar"> <div class="col-3 border-end" id="sidebar">
<!-- Mobile bottom-sheet handle (hidden on desktop) -->
<div id="sidebar-handle" class="d-flex d-md-none align-items-center gap-2 px-3">
<div id="sidebar-handle-pill"></div>
<span id="sidebar-handle-label" class="small text-muted">Controls &amp; stops</span>
</div>
<!-- Scrollable content area -->
<div id="sidebar-inner">
<!-- Load form --> <!-- Load form -->
<form method="post" action="{{ url_for('load') }}" class="mb-3"> <form method="post" action="{{ url_for('load') }}" class="mb-3">
<label class="form-label fw-semibold small">Relation ID or OSM URL</label> <label class="form-label fw-semibold small">Relation ID or OSM URL</label>
@ -111,6 +120,7 @@
<div id="stop-list"></div> <div id="stop-list"></div>
</div> </div>
</div><!-- /sidebar-inner -->
</div><!-- /sidebar --> </div><!-- /sidebar -->
<!-- Map --> <!-- Map -->