feat: implement arrivals panel with toggle functionality and improved layout

This commit is contained in:
2026-06-01 17:31:04 +08:00
parent c6704c9f70
commit 2dd31c3be7
2 changed files with 141 additions and 31 deletions
+92
View File
@@ -99,6 +99,21 @@ h1 {
margin-bottom: 0.65rem;
}
.arrivals-head-actions {
display: flex;
align-items: center;
gap: 0.45rem;
}
.sheet-grabber,
.mobile-sheet-toggle {
display: none;
}
.sheet-content {
display: block;
}
label {
display: flex;
flex-direction: column;
@@ -423,6 +438,10 @@ button:hover {
}
@media (max-width: 760px) {
body {
padding-bottom: 16.5rem;
}
.app-shell {
width: min(1100px, calc(100% - 1rem));
margin-top: 1rem;
@@ -459,6 +478,79 @@ button:hover {
padding: 0.85rem;
}
.arrivals-panel {
position: fixed;
left: 0.5rem;
right: 0.5rem;
bottom: 0.5rem;
z-index: 14;
margin-top: 0;
border-radius: 18px;
background: rgba(255, 251, 243, 0.96);
box-shadow: 0 16px 36px rgba(54, 39, 12, 0.22);
max-height: min(68vh, 520px);
overflow: hidden;
transition: transform 220ms ease, max-height 220ms ease;
}
.sheet-grabber {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
background: transparent;
padding: 0.1rem 0 0.45rem;
min-height: 20px;
}
.sheet-grabber span {
width: 44px;
height: 5px;
border-radius: 999px;
background: #cfbaa0;
}
.mobile-sheet-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
border-radius: 10px;
background: #efe2ca;
color: #5a4734;
font-size: 0.83rem;
padding: 0.35rem 0.58rem;
}
.arrivals-panel .panel-header.compact {
margin-bottom: 0.45rem;
}
.arrivals-panel .sheet-content {
max-height: calc(min(68vh, 520px) - 92px);
overflow: auto;
padding-bottom: 0.25rem;
}
.arrivals-panel.sheet-collapsed {
max-height: 76px;
}
.arrivals-panel.sheet-collapsed .panel-header,
.arrivals-panel.sheet-collapsed .sheet-content {
opacity: 0;
pointer-events: none;
}
.arrivals-panel.sheet-collapsed .panel-header {
margin: 0;
min-height: 0;
}
.arrivals-panel.sheet-collapsed .sheet-grabber {
padding: 0.52rem 0;
}
.btn-accent,
.btn-ghost {
min-height: 44px;
+49 -31
View File
@@ -10,6 +10,7 @@
let serviceNo = "";
let locating = false;
let loadingArrivals = false;
let mobileSheetOpen = true;
let userLocation = null;
let selectedStop = null;
@@ -534,40 +535,57 @@
</div>
</section>
<section class="panel">
<section class="panel arrivals-panel" class:sheet-collapsed={!mobileSheetOpen}>
<button
class="sheet-grabber"
type="button"
aria-label={mobileSheetOpen ? "Collapse arrivals panel" : "Expand arrivals panel"}
aria-expanded={mobileSheetOpen}
on:click={() => (mobileSheetOpen = !mobileSheetOpen)}
>
<span></span>
</button>
<div class="panel-header compact">
<h2>Arrivals</h2>
<span class={`status-pill ${statusState}`}>{statusText}</span>
</div>
<div class="arrivals-actions">
<label>
Service filter (optional)
<input bind:value={serviceNo} placeholder="e.g. 14" />
</label>
<button class="btn-accent" type="button" on:click={runArrival} disabled={loadingArrivals || locating}>
{loadingArrivals ? "Loading..." : "Refresh Arrivals"}
</button>
</div>
{#if services.length > 0}
<div class="arrival-board">
{#each services as service}
<article class="arrival-card">
<h3>Service {service.ServiceNo || "?"}</h3>
<p class="arrival-line">Next: {minutesUntil(service?.NextBus?.EstimatedArrival)}</p>
<p class="arrival-line">Following: {minutesUntil(service?.NextBus2?.EstimatedArrival)}</p>
<p class="arrival-line">Third: {minutesUntil(service?.NextBus3?.EstimatedArrival)}</p>
</article>
{/each}
<div class="arrivals-head-actions">
<span class={`status-pill ${statusState}`}>{statusText}</span>
<button class="mobile-sheet-toggle" type="button" on:click={() => (mobileSheetOpen = !mobileSheetOpen)}>
{mobileSheetOpen ? "Hide" : "Show"}
</button>
</div>
{:else}
<p class="hint">No arrivals loaded yet. Tap "Use Current Location" then "Refresh Arrivals".</p>
{/if}
</div>
<details class="raw-response">
<summary>Raw response</summary>
<pre class="response">{responseText}</pre>
</details>
<div class="sheet-content" aria-hidden={!mobileSheetOpen}>
<div class="arrivals-actions">
<label>
Service filter (optional)
<input bind:value={serviceNo} placeholder="e.g. 14" />
</label>
<button class="btn-accent" type="button" on:click={runArrival} disabled={loadingArrivals || locating}>
{loadingArrivals ? "Loading..." : "Refresh Arrivals"}
</button>
</div>
{#if services.length > 0}
<div class="arrival-board">
{#each services as service}
<article class="arrival-card">
<h3>Service {service.ServiceNo || "?"}</h3>
<p class="arrival-line">Next: {minutesUntil(service?.NextBus?.EstimatedArrival)}</p>
<p class="arrival-line">Following: {minutesUntil(service?.NextBus2?.EstimatedArrival)}</p>
<p class="arrival-line">Third: {minutesUntil(service?.NextBus3?.EstimatedArrival)}</p>
</article>
{/each}
</div>
{:else}
<p class="hint">No arrivals loaded yet. Tap "Use Current Location" then "Refresh Arrivals".</p>
{/if}
<details class="raw-response">
<summary>Raw response</summary>
<pre class="response">{responseText}</pre>
</details>
</div>
</section>
</main>