Initial grid staff view layout

This commit is contained in:
2026-05-21 23:56:59 +01:00
parent d10ca44023
commit 0eb2f4349c
7 changed files with 240 additions and 264 deletions

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import type { ApiStationsBoard } from '@owlboard/owlboard-ts';
import { formatUkTime, estClass, delayClassFromTimePair } from '$lib/utils/time';
import { fade } from 'svelte/transition';
import { fade, slide } from 'svelte/transition';
import { flip } from 'svelte/animate';
let { services }: { services: ApiStationsBoard.BoardService[] } = $props();
@@ -11,168 +12,122 @@
<section class="departure-board">
<div class="header">
<div class="header-row"></div>
<div class="header-row"></div>
<div class="header row">
<div class="upper-header-blank"></div>
<div class="upper-header-text">Arr</div>
<div class="upper-header-text">Dep</div>
</div>
<div class="header row">
<div class="cell">ID</div>
<div class="cell">Orig</div>
<div class="cell">Dest</div>
<div class="cell">Plt</div>
<div class="cell">Sch</div>
<div class="cell">Act</div>
<div class="cell">Sch</div>
<div class="cell">Act</div>
</div>
</div>
<div class="services">
<!-- Keyed EACH Here -->
{#each services as service (getRowKey(service))}
{@const realArrival = formatUkTime(service.ata || service.eta)}
{@const realDeparture = formatUkTime(service.atd || service.etd)}
{@const realPass = formatUkTime(service.atp || service.etp)}
<div
class="service-block"
animate:flip={{ duration: 400 }}
transition:slide={{ duration: 300 }}
>
<div class="service row">
<div class="cell id">{service.h}</div>
<div class="cell orig">
{#key service.og.t}
<div transition:fade={{ duration: 300 }}>
{service.og.t}
</div>
{/key}
</div>
<div class="cell dest">
{#key service.dt.t}<div transition:fade={{ duration: 300 }}>{service.dt.t}</div>{/key}
</div>
<div class="cell plt">
{#key service.p}<div transition:fade={{ duration: 300 }}>{service.p || '-'}</div>{/key}
</div>
{#if service.wtp}
<div class="cell pass">PASS</div>
<div class="cell sch">
{formatUkTime(service.wtp)}
</div>
<div class="cell act">
{#key realPass}
<div transition:fade={{ duration: 300 }}>
{realPass}
</div>
{/key}
</div>
{:else}
<div class="cell sch">{formatUkTime(service.sta)}</div>
<div class="cell act">
{#key realArrival}
<div transition:fade={{ duration: 300 }}>
{realArrival}
</div>
{/key}
</div>
<div class="cell sch">{formatUkTime(service.std)}</div>
<div class="cell act">
{#key realDeparture}
<div transition:fade={{ duration: 300 }}>
{realDeparture}
</div>
{/key}
</div>
{/if}
</div>
<div class="operator row">{service.o}</div>
{#if service?.cr?.r}
<div class="cancel-reason row" transition:slide={{ duration: 300 }}>{service.cr.r}</div>
{:else if service?.dr?.r}
<div class="delay-reason row" transition:slide={{ duration: 300 }}>{service.dr.r}</div>
{/if}
</div>
{/each}
</div>
</section>
<table class="departure-board">
<colgroup>
<col style="width:10%;" />
<!-- ID (Headcode) -->
<col style="width:17%;" />
<!-- ORIG (Origin) -->
<col style="width:17%;" />
<!-- DEST (Destination) -->
<col style="width:9%;" />
<!-- PLT (Platform) -->
<col style="width: 12%;" />
<!-- STA -->
<col style="width: 12%;" />
<!-- ETA/ATA -->
<col style="width: 12%;" />
<!-- STD -->
<col style="width: 11%;" />
<!-- ETD/ATD -->
</colgroup>
<thead>
<tr>
<th colspan="4" aria-hidden="true"></th>
<th scope="colgroup" colspan="2" class="upper-head"><abbr title="Arrival">Arr</abbr></th>
<th scope="colgroup" colspan="2" class="upper-head"><abbr title="Departure">Dep</abbr></th>
</tr>
<tr>
<th scope="col"><abbr title="Headcode">ID</abbr></th>
<th scope="col"><abbr title="Origin">Orig</abbr></th>
<th scope="col"><abbr title="Destination">Dest</abbr></th>
<th scope="col"><abbr title="Platform">Plt</abbr></th>
<th scope="col"><abbr title="Scheduled">Sch</abbr></th>
<th scope="col"><abbr title="Actual/Expected">Act</abbr></th>
<th scope="col"><abbr title="Scheduled">Sch</abbr></th>
<th scope="col"><abbr title="Actual/Expected">Act</abbr></th>
</tr>
</thead>
{#each services as service (getRowKey(service))}
<tbody>
<tr
in:fade={{duration: 150, delay:150}}
out:fade={{duration:150}}
class="service-row"
class:serviceCancelled={service.c}
class:servicePass={service.wtp}
class:serviceNonPassenger={!service.ip}
>
<td class="id-cell">{service.h}</td>
<td class="orig-cell"
><abbr title={service.og.n.toLocaleUpperCase()}>{service.og.t}</abbr></td
>
<td class="dest-cell"
><abbr title={service.dt.n.toLocaleUpperCase()}>{service.dt.t}</abbr></td
>
<td class="plt-cell" class:platSup={service.ps} class:platChange={service.pc}
>{service.p || '-'}</td
>
<!-- Handle different display for a passing train -->
{#if service.wtp}
<td class="pass-cell" colspan="2">Pass</td>
<td class="time-cell">{formatUkTime(service.wtp)}</td>
<!-- If cancelled, show '-', otherwise check for RT or show time -->
<td
class="time-cell {estClass(service.atp, service.etp)} {delayClassFromTimePair(
service.wtp,
service.atp || service.etp
)}"
>
<span>
{service.c
? '-'
: delayClassFromTimePair(service.wtp, service.atp || service.etp) === 'delay-rt'
? 'RT'
: formatUkTime(service.atp || service.etp)}
</span>
</td>
{:else}
<td class="time-cell">{formatUkTime(service.sta)}</td>
<!-- Arrival Actual/Expected -->
<td
class="time-cell {estClass(service.ata, service.eta)} {delayClassFromTimePair(
service.sta,
service.ata || service.eta
)}"
>
<span>
{service.c
? '-'
: delayClassFromTimePair(service.sta, service.ata || service.eta) === 'delay-rt'
? 'RT'
: formatUkTime(service.ata || service.eta)}
</span>
</td>
<td class="time-cell">{formatUkTime(service.std)}</td>
<!-- Departure Actual/Expected -->
<td
class="time-cell {estClass(service.atd, service.etd)} {delayClassFromTimePair(
service.std,
service.atd || service.etd
)}"
>
<span>
{service.c
? '-'
: delayClassFromTimePair(service.std, service.atd || service.etd) === 'delay-rt'
? 'RT'
: formatUkTime(service.atd || service.etd)}
</span>
</td>
{/if}
</tr>
{#if service.o}
<tr class="toc-coach-row" in:fade={{ duration: 150, delay: 150 }} out:fade={{ duration: 150 }}>
<td colspan="8">
{service.o}
</td>
</tr>
{/if}
{#if service.c && service.cr?.r}
<tr class="cancel-row" in:fade={{ duration: 150, delay: 150 }} out:fade={{ duration: 150 }}>
<td colspan="8">
{service.cr.r}
{#if service.cr.l}
{service.cr.n ? 'near' : 'at'}
{service.cr.l}
{/if}
</td>
</tr>
{:else if service.dr?.r}
<tr class="delay-row" in:fade={{ duration: 150, delay: 150 }} out:fade={{ duration: 150 }}>
<td colspan="9">
{service.dr.r}
{#if service.dr.l}
{service.dr.n ? 'near' : 'at'}
{service.dr.l}
{/if}
</td>
</tr>
{/if}
</tbody>
{/each}
</table>
<style>
.departure-board {
width: 100%;
max-width: 100%;
margin: 5px auto;
border-collapse: collapse;
table-layout: fixed;
box-sizing: border-box;
overflow: hidden;
}
.upper-header-blank {
grid-column: span 4;
}
.upper-header-text {
grid-column: span 2;
text-align: center;
}
.row {
display: grid;
grid-template-columns: 10% 17% 17% 9% 12% 12% 12% 11%;
align-items: center;
text-align: center;
gap: 0;
box-sizing: border-box;
font-family: 'Inconsolata Variable', monospace;
font-size: clamp(1rem, 0.475rem + 2.8vw, 1.35rem);
font-variant-ligatures: additional-ligatures;
}
.service-block {
width: 100%;
max-width: 100%;
padding-bottom: 7px;
}
.departure-board td {
@@ -222,146 +177,136 @@
}
/* Special Row Styles */
.cancel-row td,
.delay-row td {
font-size: 0.88rem;
.operator.row,
.delay-reason.row,
.cancel-reason.row {
grid-template-columns: 1fr;
text-align: left;
font-size: 0.88rem;
}
.cancel-row td[colspan],
.delay-row td[colspan] {
padding-left: 0ch;
}
.cancel-row td {
.cancel-reason.row {
color: rgb(255, 131, 131);
font-weight: 400;
}
.delay-row td {
.delay-reason.row {
color: var(--delay-orange);
font-style: italic;
}
@media (min-width: 375px) {
.cancel-row td,
.delay-row td {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.08rem;
}
}
@media (min-width: 420px) {
.cancel-row td,
.delay-row td {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.12rem;
}
}
@media (min-width: 550px) {
.cancel-row td,
.delay-row td {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.18rem;
}
}
@media (min-width: 620px) {
.cancel-row td,
.delay-row td {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.2rem;
}
}
.toc-coach-row td {
font-family: 'URW Gothic', sans-serif;
font-size: 0.7rem;
.operator.row {
/* font-family: 'URW Gothic', sans-serif; */
padding: 0;
color: rgb(187, 187, 255);
}
@media (min-width: 400px) {
.toc-coach-row td {
font-size: 0.8rem;
}
}
@media (min-width: 550px) {
.toc-coach-row td {
font-size: 0.85rem;
}
}
/* Column Specifics */
.id-cell {
.cell.id {
text-align: left;
font-weight: 400;
font-stretch: 85%;
filter: brightness(0.75);
}
@media (min-width: 400px) {
.id-cell {
.cell.id {
font-stretch: 90%;
}
}
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 80%;
font-weight: 400;
}
.orig-cell {
.cell.orig {
text-align: left;
color: var(--location-yellow);
}
.dest-cell {
.cell.dest {
text-align: right;
color: var(--location-yellow);
}
@media (min-width: 350px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 85%;
}
@media (min-width: 400px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 90%;
}
}
@media (min-width: 490px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 95%;
}
}
@media (min-width: 520px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 100%;
}
.orig-cell {
.cell.orig {
text-align: right;
padding-right: 7px;
}
.dest-cell {
.cell.dest {
text-align: left;
padding-left: 7px;
}
}
@media (min-width: 600px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
letter-spacing: 0.05ch;
}
}
}
.plt-cell {
.cell.plt {
text-align: center;
font-weight: 410;
font-stretch: 100%;
}
@media (min-width: 525px) {
.plt-cell {
.cell.plt {
font-stretch: 110%;
}
}
.plt-cell.platSup {
.cell.plt.platSup {
font-weight: 200;
opacity: 0.3;
}
.plt-cell.platChange {
.cell.plt.platChange {
animation: fast-pulse 2s ease-out infinite;
}
.service-row.serviceCancelled .plt-cell {
@@ -377,27 +322,36 @@
color: rgb(255, 131, 131);
}
.time-cell {
.cell.pass {
grid-column: span 2;
}
.cell.sch,
.cell.act {
text-align: center;
font-stretch: 72%;
}
@media (min-width: 350px) {
.time-cell {
.cell.sch,
.cell.act {
font-stretch: 77%;
}
}
@media (min-width: 400px) {
.time-cell {
.cell.sch,
.cell.act {
font-stretch: 82%;
}
}
@media (min-width: 525px) {
.time-cell {
.cell.sch,
.cell.act {
font-stretch: 90%;
}
}
@media (min-width: 600px) {
.time-cell {
.cell.sch,
.cell.act {
font-stretch: 100%;
}
}
@@ -419,12 +373,12 @@
}
/* Time Types */
.est {
.cell.act.estimate {
font-style: italic;
opacity: 0.75;
font-weight: 350;
}
.act {
.cell.act {
font-weight: 600;
}
</style>