Compare commits
3 Commits
v3.0.0-dev
...
v3.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| bc56e66178 | |||
| 0ef9e5b56f | |||
| b88f4f3b35 |
@@ -9,6 +9,24 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<table class="departure-board">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4" aria-hidden="true"></th>
|
<th colspan="4" aria-hidden="true"></th>
|
||||||
@@ -29,10 +47,19 @@
|
|||||||
|
|
||||||
{#each services as service (getRowKey(service))}
|
{#each services as service (getRowKey(service))}
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="service-row" class:serviceCancelled={service.c} class:servicePass={service.wtp} class:serviceNonPassenger={!service.ip}>
|
<tr
|
||||||
|
class="service-row"
|
||||||
|
class:serviceCancelled={service.c}
|
||||||
|
class:servicePass={service.wtp}
|
||||||
|
class:serviceNonPassenger={!service.ip}
|
||||||
|
>
|
||||||
<td class="id-cell">{service.h}</td>
|
<td class="id-cell">{service.h}</td>
|
||||||
<td class="orig-cell"><abbr title={service.og.n.toLocaleUpperCase()}>{service.og.t}</abbr></td>
|
<td class="orig-cell"
|
||||||
<td class="dest-cell"><abbr title={service.dt.n.toLocaleUpperCase()}>{service.dt.t}</abbr></td>
|
><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}
|
<td class="plt-cell" class:platSup={service.ps} class:platChange={service.pc}
|
||||||
>{service.p || '-'}</td
|
>{service.p || '-'}</td
|
||||||
>
|
>
|
||||||
@@ -95,9 +122,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{#if service.o}
|
||||||
|
<tr class="toc-coach-row">
|
||||||
|
<td colspan="8">
|
||||||
|
{service.o}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
{#if service.c && service.cr?.r}
|
{#if service.c && service.cr?.r}
|
||||||
<tr class="cancel-row">
|
<tr class="cancel-row">
|
||||||
<td colspan="9">
|
<td colspan="8">
|
||||||
{service.cr.r}
|
{service.cr.r}
|
||||||
{#if service.cr.l}
|
{#if service.cr.l}
|
||||||
{service.cr.n ? 'near' : 'at'}
|
{service.cr.n ? 'near' : 'at'}
|
||||||
@@ -126,20 +160,20 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 5px auto;
|
margin: 5px auto;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.departure-board td {
|
.departure-board td {
|
||||||
font-family: 'Inconsolate Variable', monospace;
|
font-family: 'Inconsolata Variable', monospace;
|
||||||
font-size: 1rem;
|
font-size: clamp(1rem, 0.475rem + 2.8vw, 1.35rem);
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-variant-ligatures: additional-ligatures;
|
font-variant-ligatures: additional-ligatures;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead,
|
thead,
|
||||||
.cancel-row td,
|
.cancel-row td,
|
||||||
.delay-row td {
|
.delay-row td {
|
||||||
font-family: 'URW Gothic', sans-serif;
|
letter-spacing: -0.15ch;
|
||||||
letter-spacing: 0.02ch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
@@ -149,11 +183,15 @@
|
|||||||
background: var(--color-bg-dark);
|
background: var(--color-bg-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
abbr {
|
abbr {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
cursor:help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tbody tr:first-child td {
|
||||||
|
border-top: 5px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* Row Logic */
|
/* Row Logic */
|
||||||
.serviceCancelled {
|
.serviceCancelled {
|
||||||
@@ -165,22 +203,22 @@
|
|||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.serviceNonPassenger td {
|
.serviceNonPassenger td {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-weight: 200;
|
font-weight: 290;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Special Row Styles */
|
/* Special Row Styles */
|
||||||
.cancel-row td,
|
.cancel-row td,
|
||||||
.delay-row td {
|
.delay-row td {
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
padding-bottom: 8px;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-row td[colspan],
|
.cancel-row td[colspan],
|
||||||
.delay-row td[colspan] {
|
.delay-row td[colspan] {
|
||||||
padding-left: 0.75ch;
|
padding-left: 0ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-row td {
|
.cancel-row td {
|
||||||
@@ -192,18 +230,64 @@
|
|||||||
color: var(--delay-orange);
|
color: var(--delay-orange);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
@media (min-width: 375px) {
|
||||||
|
.cancel-row td,
|
||||||
|
.delay-row td {
|
||||||
|
font-size: 1.08rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 420px) {
|
||||||
|
.cancel-row td,
|
||||||
|
.delay-row td {
|
||||||
|
font-size: 1.12rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 550px) {
|
||||||
|
.cancel-row td,
|
||||||
|
.delay-row td {
|
||||||
|
font-size: 1.18rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 620px) {
|
||||||
|
.cancel-row td,
|
||||||
|
.delay-row td {
|
||||||
|
font-size: 1.20rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-coach-row td {
|
||||||
|
font-family: 'URW Gothic', sans-serif;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
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 */
|
/* Column Specifics */
|
||||||
.id-cell {
|
.id-cell {
|
||||||
text-align: center;
|
text-align: left;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-stretch: 80%;
|
font-stretch: 85%;
|
||||||
filter: brightness(0.75);
|
filter: brightness(0.75);
|
||||||
}
|
}
|
||||||
|
@media (min-width: 400px) {
|
||||||
|
.id-cell {
|
||||||
|
font-stretch: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
.orig-cell,
|
.orig-cell,
|
||||||
.dest-cell {
|
.dest-cell {
|
||||||
font-stretch: 90%;
|
font-stretch: 80%;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
.orig-cell {
|
.orig-cell {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -213,10 +297,53 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--location-yellow);
|
color: var(--location-yellow);
|
||||||
}
|
}
|
||||||
|
@media (min-width: 350px) {
|
||||||
|
.orig-cell,
|
||||||
|
.dest-cell {
|
||||||
|
font-stretch: 85%;
|
||||||
|
}
|
||||||
|
@media (min-width: 400px) {
|
||||||
|
.orig-cell,
|
||||||
|
.dest-cell {
|
||||||
|
font-stretch: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 490px) {
|
||||||
|
.orig-cell,
|
||||||
|
.dest-cell {
|
||||||
|
font-stretch: 95%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 520px) {
|
||||||
|
.orig-cell,
|
||||||
|
.dest-cell {
|
||||||
|
font-stretch: 100%;
|
||||||
|
}
|
||||||
|
.orig-cell {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 7px;
|
||||||
|
}
|
||||||
|
.dest-cell {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.orig-cell,
|
||||||
|
.dest-cell {
|
||||||
|
letter-spacing: 0.05ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.plt-cell {
|
.plt-cell {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 405;
|
font-weight: 410;
|
||||||
font-stretch: 70%;
|
font-stretch: 100%;
|
||||||
|
}
|
||||||
|
@media (min-width: 525px) {
|
||||||
|
.plt-cell {
|
||||||
|
font-stretch: 110%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.plt-cell.platSup {
|
.plt-cell.platSup {
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
@@ -230,6 +357,7 @@
|
|||||||
}
|
}
|
||||||
.pass-cell {
|
.pass-cell {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-stretch: 100%;
|
||||||
}
|
}
|
||||||
/* Colour orig and dest values when cancelled */
|
/* Colour orig and dest values when cancelled */
|
||||||
.service-row.serviceCancelled .orig-cell,
|
.service-row.serviceCancelled .orig-cell,
|
||||||
@@ -239,7 +367,27 @@
|
|||||||
|
|
||||||
.time-cell {
|
.time-cell {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-stretch: 70%;
|
font-stretch: 72%;
|
||||||
|
}
|
||||||
|
@media (min-width: 350px) {
|
||||||
|
.time-cell {
|
||||||
|
font-stretch: 77%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 400px) {
|
||||||
|
.time-cell {
|
||||||
|
font-stretch: 82%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 525px) {
|
||||||
|
.time-cell {
|
||||||
|
font-stretch: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.time-cell {
|
||||||
|
font-stretch: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RT Logic */
|
/* RT Logic */
|
||||||
|
|||||||
@@ -153,7 +153,7 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 200 900;
|
font-weight: 200 900;
|
||||||
font-stretch: 50% 200%;
|
font-stretch: 50% 200%;
|
||||||
src: url(type/Inconsolata/latin-wdth-normal.woff2) format('woff2-variations');
|
src: url(/type/inconsolata/latin-wdth-normal.woff2) format('woff2-variations');
|
||||||
unicode-range:
|
unicode-range:
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $page.error?.owlCode === 'NETWORK_DISCONNECTED'}
|
{#if page.error?.owlCode === 'NETWORK_DISCONNECTED'}
|
||||||
<Button onclick={() => window.location.reload()} color={'accent'}>Retry</Button>
|
<Button onclick={() => window.location.reload()} color={'accent'}>Retry</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button href={'/'} color={'accent'}>Return to Home</Button>
|
<Button href={'/'} color={'accent'}>Return to Home</Button>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { slide, fade } from 'svelte/transition';
|
import { slide, fade } from 'svelte/transition';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import { navigating } from '$app/state';
|
import { navigating } from '$app/state';
|
||||||
import { LOCATIONS } from '$lib/locations-object.svelte';
|
import { LOCATIONS } from '$lib/locations-object.svelte';
|
||||||
import { nearestStationsState } from '$lib/geohash.svelte';
|
|
||||||
|
|
||||||
import Loading from '$lib/components/ui/Loading.svelte';
|
import Loading from '$lib/components/ui/Loading.svelte';
|
||||||
import TimezoneWarning from '$lib/components/ui/TimezoneWarning.svelte';
|
import TimezoneWarning from '$lib/components/ui/TimezoneWarning.svelte';
|
||||||
@@ -17,7 +17,19 @@
|
|||||||
|
|
||||||
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
|
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
|
||||||
|
|
||||||
onMount(() => LOCATIONS.init());
|
onMount(async () => {
|
||||||
|
LOCATIONS.init();
|
||||||
|
if (browser && 'serviceWorker' in navigator) {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.register('/service-worker.js', {
|
||||||
|
type: 'module',
|
||||||
|
});
|
||||||
|
console.info('OwlBoard Service Worker registered', registration.scope);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service Worker installation failed: ', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
@@ -147,7 +159,7 @@
|
|||||||
<h1 class="viewport-guard-title">Narrow Gauge Detected</h1>
|
<h1 class="viewport-guard-title">Narrow Gauge Detected</h1>
|
||||||
<p>
|
<p>
|
||||||
Just as trains need the right track width, our data needs a bit more room to stay on the rails.
|
Just as trains need the right track width, our data needs a bit more room to stay on the rails.
|
||||||
Please expand your view to at least 300px to view the app.
|
Please expand your view to at least 320px to view the app.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -319,7 +331,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 299px) {
|
@media (max-width: 319px) {
|
||||||
.viewport-guard {
|
.viewport-guard {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -90,7 +90,8 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: calc(100dvh - 60px);
|
height: calc(100dvh - 60px);
|
||||||
width: 100%;
|
width: 97%;
|
||||||
|
max-width: 700px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
76
src/service-worker.ts
Normal file
76
src/service-worker.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/// <reference types="@sveltejs/kit" />
|
||||||
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
|
import type { ApiEnvelope } from '@owlboard/api-schema-types';
|
||||||
|
import { build, files, version } from '$service-worker';
|
||||||
|
|
||||||
|
const sw = self as unknown as ServiceWorkerGlobalScope;
|
||||||
|
const CACHE_NAME = `owlboard-cache-${version}`;
|
||||||
|
const ASSETS = [...build, ...files, '/'];
|
||||||
|
|
||||||
|
sw.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
sw.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((keys) => {
|
||||||
|
return Promise.all(
|
||||||
|
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
sw.addEventListener('fetch', (event) => {
|
||||||
|
const { request } = event;
|
||||||
|
if (request.method !== 'GET') return;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// 1. Static Assets (Cache-First)
|
||||||
|
if (ASSETS.includes(url.pathname) || url.pathname.startsWith('/_app/')) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(request).then((res) => res || fetch(request))
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Offline API Fallback (Network-First)
|
||||||
|
if (url.pathname.startsWith('/api') || url.hostname.includes('api')) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(request).catch(() => {
|
||||||
|
const errorObject: ApiEnvelope.Envelope = {
|
||||||
|
e: {
|
||||||
|
code: "NETWORK_DISCONNECTED",
|
||||||
|
msg: "Cannot connect to the OwlBoard server"
|
||||||
|
},
|
||||||
|
t: Math.floor(Date.now() / 1000)
|
||||||
|
};
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(errorObject),
|
||||||
|
{
|
||||||
|
status: 503,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Navigation Fallback (The Offline 404 Catch-All)
|
||||||
|
// This catches top-level page navigations and hard refreshes
|
||||||
|
if (request.mode === 'navigate' || request.headers.get('accept')?.includes('text/html')) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(request).catch(() => {
|
||||||
|
// The network request failed entirely (offline).
|
||||||
|
// Serve the cached root shell so SvelteKit can boot.
|
||||||
|
return caches.match('/') as Promise<Response>;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user