33 Commits

Author SHA1 Message Date
e94b0e811a Adjust sizes on manifest icons 2026-02-11 19:17:43 +00:00
04f6a28100 Provide new 'short_name' OB Maps to better fit on Android app drawer.
Update theme colour to accent colour to provide thematic separation of notification bar and header bar when running full screen on android.
2026-02-11 19:03:49 +00:00
54e3483a39 Include tabler icons and switch buttons to use icons 2026-02-11 19:01:24 +00:00
117d1f752e Switch to new resized logo with text 2026-02-11 18:37:10 +00:00
1207edf12b Add placeholders for next routes 2026-02-11 10:57:01 +00:00
b3d9eb6f33 Move to 'updated' and 'checked' dates, rather than 'created' 'checked & updated' 2026-02-11 10:52:15 +00:00
869a7296e8 Adjust margins/padding 2026-02-10 22:13:15 +00:00
7777671cbb Run prettify 2026-02-10 22:11:17 +00:00
353fd07b92 Fix: Lint warnings about non-interavtive content and onclick handlers. 2026-02-10 22:10:07 +00:00
a036b0ba63 Add apple-touch-icon <link> 2026-02-10 22:06:39 +00:00
0b53f8ed81 Add base for Wooton Basset Jn - Stoke Gifford Jn 2026-02-10 22:06:26 +00:00
ee92b52552 Add icons to webmanifest 2026-02-10 22:06:04 +00:00
4cc6140d86 Add typing 2026-02-10 22:05:49 +00:00
a458cc5e76 Ensure types are configured 2026-02-10 22:05:37 +00:00
cb9f826943 Adjust filtering to enable filtering by map contents 2026-02-10 22:05:25 +00:00
fa1da3686f Adjust 'home link' to use logo 2026-02-10 22:05:09 +00:00
d5270dc889 Add PIN logo SVG source 2026-02-10 20:55:07 +00:00
6366cb601b Add manifest icons. 2026-02-10 20:55:07 +00:00
fe1061875b Add new logo 2026-02-10 20:55:07 +00:00
699f2b760d Switch to Svelte resolve API 2026-02-10 20:54:31 +00:00
c8790e2aae Switch to new logo for favicon & provide ico fallback. 2026-02-10 20:54:30 +00:00
3fbffc7589 Adjust map boundaries.
Now: PAD-RDG, RDG-BRI

Was: PAD-Reading West Jn, Reading West Jn - BRI
2026-02-09 22:55:49 +00:00
84312abc75 Fix global CSS Import 2026-02-09 22:35:44 +00:00
c6fa8794e0 Add id field to RouteRow components 2026-02-09 22:24:42 +00:00
8e65fd396e Add manifest generator 2026-02-09 22:24:29 +00:00
8a05d8358f Update route information and enable direct linking. 2026-02-09 22:24:22 +00:00
a8c3117259 Add webmanifest and improve Vite build 2026-02-09 22:23:20 +00:00
2b2095604c Use standardised fonts 2026-02-09 22:23:01 +00:00
08f3d30e44 Update favicon 2026-02-07 23:11:00 +00:00
39511dd428 Improve 'branding' and mobile layout 2026-02-07 20:50:52 +00:00
cfcf651e41 Add favicon 2026-02-07 20:50:39 +00:00
cfa1d44dbc Run prettier 2026-02-07 00:38:56 +00:00
ca5e78e2b1 Add OwlBoard styling 2026-02-07 00:31:37 +00:00
31 changed files with 2324 additions and 1488 deletions

View File

@@ -6,4 +6,4 @@ bun.lock
bun.lockb bun.lockb
# Miscellaneous # Miscellaneous
/static/ #/static/

29
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@sveltejs/adapter-node": "^5.5.2", "@sveltejs/adapter-node": "^5.5.2",
"@sveltejs/kit": "^2.50.1", "@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tabler/icons-svelte": "^3.36.1",
"@types/node": "^22", "@types/node": "^22",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
@@ -1344,6 +1345,34 @@
"vite": "^6.3.0 || ^7.0.0" "vite": "^6.3.0 || ^7.0.0"
} }
}, },
"node_modules/@tabler/icons": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz",
"integrity": "sha512-f4Jg3Fof/Vru5ioix/UO4GX+sdDsF9wQo47FbtvG+utIYYVQ/QVAC0QYgcBbAjQGfbdOh2CCf0BgiFOF9Ixtjw==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/@tabler/icons-svelte": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/@tabler/icons-svelte/-/icons-svelte-3.36.1.tgz",
"integrity": "sha512-f48RDkXJr7dMbbWHho81rR91QiPleHTlOwJUM5uFhTqo7dXH4mNZxJo3tksQNmlIauh7PqoS3i+RY7YlZxg5yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tabler/icons": ""
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
},
"peerDependencies": {
"svelte": ">=3 <6 || >=5.0.0-next.0"
}
},
"node_modules/@types/cookie": { "node_modules/@types/cookie": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",

View File

@@ -19,6 +19,7 @@
"@sveltejs/adapter-node": "^5.5.2", "@sveltejs/adapter-node": "^5.5.2",
"@sveltejs/kit": "^2.50.1", "@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tabler/icons-svelte": "^3.36.1",
"@types/node": "^22", "@types/node": "^22",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",

View File

@@ -6,6 +6,8 @@ const inputDir = './static/mapFiles/yaml';
const outputDir = './static/mapFiles/json'; const outputDir = './static/mapFiles/json';
const indexFile = './static/map-index.json'; const indexFile = './static/map-index.json';
const noiseRegex = /\s+(single line|junction|jn|junc|jct|gf|north|south|east|west)\.?$/i;
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
const mapList = []; const mapList = [];
@@ -18,12 +20,37 @@ fs.readdirSync(inputDir).forEach((file) => {
const fileName = file.replace('.yaml', '.json'); const fileName = file.replace('.yaml', '.json');
fs.writeFileSync(path.join(outputDir, fileName), JSON.stringify(content)); fs.writeFileSync(path.join(outputDir, fileName), JSON.stringify(content));
const contentSet = new Set();
if (Array.isArray(content.routeDetail)) {
content.routeDetail.forEach((item) => {
if ((item.type === 'junction' || item.type === 'station') && item.name) {
let cleanName = item.name;
// Run the replacement in a loop or twice to catch nested noise
// e.g., "Reading West Junction"
// Pass 1: "Reading West"
// Pass 2: "Reading"
let previousName;
do {
previousName = cleanName;
cleanName = cleanName.replace(noiseRegex, '').trim();
} while (cleanName !== previousName);
if (cleanName) {
contentSet.add(cleanName);
}
}
});
}
mapList.push({ mapList.push({
routeId: content.routeId || null, routeId: content.routeId || null,
routeStart: content.routeStart || null, routeStart: content.routeStart || null,
routeEnd: content.routeEnd || null, routeEnd: content.routeEnd || null,
created: content.created || null, updated: content.updated || null,
checked: content.checked || null checked: content.checked || null,
contents: Array.from(contentSet)
}); });
} }
}); });

View File

@@ -6,6 +6,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
/> />
<link rel="manifest" href="/manifest.webmanifest" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

21
src/lib/assets/global.css Normal file
View File

@@ -0,0 +1,21 @@
/* FONTS */
@font-face {
font-family: 'urwgothic';
src:
url('/fonts/urwgothic/urwgothic.woff2') format('woff2'),
url('/font/urwgothic/urwgothic.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'urwgothic';
src:
url('/fonts/urwgothic/urwgothicDemi.woff2') format('woff2'),
url('/font/urwgothic/urwgothicDemi.woff') format('woff');
font-weight: 900;
font-style: normal;
}
[id] {
scroll-margin-top: 100px;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" viewBox="0 0 562.09 562.09"><defs><radialGradient xlink:href="#a" id="b" cx="256" cy="256" r="254.04" fx="256" fy="256" gradientTransform="translate(27 27)" gradientUnits="userSpaceOnUse"/><linearGradient id="a"><stop offset="0" stop-color="#2b343c"/><stop offset="1" stop-color="#404c55"/></linearGradient></defs><path fill="url(#b)" d="M694.32 283a411.32 393.92 0 0 1-410.68 393.92 411.32 393.92 0 0 1-411.96-392.69 411.32 393.92 0 0 1 409.4-395.14A411.32 393.92 0 0 1 694.3 280.54" transform="translate(-1.96 -1.96)"/><path fill="#4fd1d1" d="M281.04 88.47c-70.95 0-128.48 57.53-128.48 128.48 0 14.11 2.33 28.12 6.88 41.47l-.01-.01 7.36 17.3a124.7 124.7 0 0 0 5.25 9.1l.97 1.69 108.03 187.1 108.03-187.1.98-1.7a128.5 128.5 0 0 0 5.25-9.08l7.35-17.28v-.02a128.48 128.48 0 0 0 6.87-41.47C409.52 146 352 88.47 281.04 88.47Zm-84.01 49.27 2.52 2.38c7.74 7.29 16.97 12.23 28.03 15.02 4.64 1.17 4.62 1.17 8.1-.34 25.92-11.2 64.62-11.22 91.01-.03 7.27 3.08 25.1-4.12 36.09-14.59a30.17 30.17 0 0 1 2.43-2.18c.35 0 .48 6.13.2 9.02a49.7 49.7 0 0 1-9.29 24.82l-1.47 2.06.7.94c38.07 51.55-7.46 115.42-60.6 85-1.3-.74-2.45-1.24-2.57-1.11-.2.2-5.5 10.18-9.4 17.68-1.76 3.39-1.4 3.4-3.26-.14-4.77-9.12-9.38-17.7-9.5-17.7-.08 0-1.18.58-2.45 1.31-52.6 30.1-99.03-34.13-60.98-84.33.78-1.02 1.1-1.66.95-1.87-7.68-10.44-11.39-21.86-10.7-32.92zm44.85 39.23a35.62 35.62 0 0 0-15.48 3.92c-35.26 17.47-16.98 71.79 21.65 64.33a35.5 35.5 0 0 0 22.5-14.71c1.25-1.78 1.67-2.22 1.88-1.93.14.2 2.14 3.61 4.44 7.59 4.87 8.43 4.1 7.82 5.9 4.67a497.41 497.41 0 0 1 7.32-12.44c.14-.08.96.9 1.86 2.22 16.09 23.62 52.73 18.48 61.43-8.6 8.52-26.58-16.98-51.8-43.57-43.1-12.13 3.98-20.13 12.66-27.95 30.34l-.7 1.56-1.38-3.13c-8.8-20-22.71-30.89-37.9-30.72zm76.41 18.05c4.34-.12 8.8 1.43 12.33 4.95 11.83 11.83.35 31.55-16.05 27.58-8.43-2.04-13.82-10.73-12.05-19.4 1.65-8.07 8.54-12.92 15.77-13.13zm-75.7.04c8.54-.48 16.99 5.68 17.6 15.6.66 11.02-10.1 19.75-20.85 16.88-12.5-3.33-16.72-18.35-7.72-27.46a16.6 16.6 0 0 1 10.97-5.02zm6.22 4.93c-1.1.07-2.17.56-3 1.54-3.25 3.86 1.28 9.24 5.7 6.74a4.52 4.52 0 0 0 2.21-3.84c0-2.82-2.5-4.61-4.9-4.44zm63.97.06c-3.15.2-5.02 3.3-3.63 6.36 1.05 2.31 4.26 3.26 6.3 1.87 3.77-2.56 2.44-7.95-2.03-8.23a5.19 5.19 0 0 0-.64 0z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" viewBox="0 0 508.09 508.09"><defs><radialGradient xlink:href="#a" id="b" cx="256" cy="256" r="254.04" fx="256" fy="256" gradientUnits="userSpaceOnUse"/><linearGradient id="a"><stop offset="0" stop-color="#2b343c"/><stop offset="1" stop-color="#404c55"/></linearGradient></defs><circle cx="256" cy="256" r="254.04" fill="url(#b)" transform="translate(-1.96 -1.96)"/><path fill="#4fd1d1" d="M254.04 61.47c-70.95 0-128.48 57.53-128.48 128.48 0 14.11 2.33 28.12 6.88 41.47l-.01-.01 7.36 17.3a124.7 124.7 0 0 0 5.25 9.1l.97 1.69 108.03 187.1 108.03-187.1.98-1.7a128.5 128.5 0 0 0 5.25-9.08l7.35-17.28v-.02a128.48 128.48 0 0 0 6.87-41.47C382.52 119 325 61.47 254.04 61.47Zm-84.01 49.27 2.52 2.38c7.74 7.29 16.97 12.23 28.03 15.02 4.64 1.17 4.62 1.17 8.1-.34 25.92-11.2 64.62-11.22 91.01-.03 7.27 3.08 25.1-4.12 36.09-14.59a30.17 30.17 0 0 1 2.43-2.18c.35 0 .48 6.13.2 9.02a49.7 49.7 0 0 1-9.29 24.82l-1.47 2.06.7.94c38.07 51.55-7.46 115.42-60.6 85-1.3-.74-2.45-1.24-2.57-1.11-.2.2-5.5 10.18-9.4 17.68-1.76 3.39-1.4 3.4-3.26-.14-4.77-9.12-9.38-17.7-9.5-17.7-.08 0-1.18.58-2.45 1.31-52.6 30.1-99.03-34.13-60.98-84.33.78-1.02 1.1-1.66.95-1.87-7.68-10.44-11.39-21.86-10.7-32.92zm44.85 39.23a35.62 35.62 0 0 0-15.48 3.92c-35.26 17.47-16.98 71.79 21.65 64.33a35.5 35.5 0 0 0 22.5-14.71c1.25-1.78 1.67-2.22 1.88-1.93.14.2 2.14 3.61 4.44 7.59 4.87 8.43 4.1 7.82 5.9 4.67a497.41 497.41 0 0 1 7.32-12.44c.14-.08.96.9 1.86 2.22 16.09 23.62 52.73 18.48 61.43-8.6 8.52-26.58-16.98-51.8-43.57-43.1-12.13 3.98-20.13 12.66-27.95 30.34l-.7 1.56-1.38-3.13c-8.8-20-22.71-30.89-37.9-30.72zm76.41 18.05c4.34-.12 8.8 1.43 12.33 4.95 11.83 11.83.35 31.55-16.05 27.58-8.43-2.04-13.82-10.73-12.05-19.4 1.65-8.07 8.54-12.92 15.77-13.13zm-75.7.04c8.54-.48 16.99 5.68 17.6 15.6.66 11.02-10.1 19.75-20.85 16.88-12.5-3.33-16.72-18.35-7.72-27.46a16.6 16.6 0 0 1 10.97-5.02zm6.22 4.93c-1.1.07-2.17.56-3 1.54-3.25 3.86 1.28 9.24 5.7 6.74a4.52 4.52 0 0 0 2.21-3.84c0-2.82-2.5-4.61-4.9-4.44zm63.97.06c-3.15.2-5.02 3.3-3.63 6.36 1.05 2.31 4.26 3.26 6.3 1.87 3.77-2.56 2.44-7.95-2.03-8.23a5.19 5.19 0 0 0-.64 0z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -6,9 +6,12 @@
export let reversed: boolean = false; export let reversed: boolean = false;
$: Icon = components[feature.type] || components.default; $: Icon = components[feature.type] || components.default;
const slugify = (str?: string) =>
str?.toLocaleLowerCase().trim().replace(/\s+/g, '-') ?? 'unknown';
</script> </script>
<div class="row-container"> <div class="row-container" id={slugify(feature.name)}>
<div class="mileage-col"> <div class="mileage-col">
<span class="miles">{feature.miles + 'm' || ''}</span> <span class="miles">{feature.miles + 'm' || ''}</span>
<span class="chains">{feature.chains + 'ch' || ''}</span> <span class="chains">{feature.chains + 'ch' || ''}</span>

View File

@@ -2,11 +2,12 @@
export let feature: { export let feature: {
routeName: string; routeName: string;
routeId: string; routeId: string;
entryPoint: string;
}; };
</script> </script>
<div class="link-wrapper"> <div class="link-wrapper">
<a href="/map/{feature.routeId}" class="wide-button"> <a href="/map/{feature.routeId}#{feature.entryPoint}" class="wide-button">
<div class="content"> <div class="content">
<div class="header-row"> <div class="header-row">
<span class="sub-text">Go to</span> <span class="sub-text">Go to</span>

View File

@@ -1,11 +1,18 @@
<script lang="ts"> <script lang="ts">
import BaseTrack from '$lib/components/mapIcons/BaseTrack.svelte'; import BaseTrack from '$lib/components/mapIcons/BaseTrack.svelte';
import type { ElecType } from '$lib/railStyles';
export let activeElec: string; export let activeElec: ElecType;
export let feature: any; export let feature: any;
export let reversed: boolean; export let reversed: boolean;
const pinSVG: string = `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 256.96 385.14"><path fill="#4fd1d1" d="M128.48 0C57.52 0 0 57.52 0 128.48c0 14.1 2.32 28.12 6.88 41.47l-.01-.01 7.35 17.3c1.63 3.1 3.38 6.14 5.25 9.1l.98 1.68 108.03 187.12 108.03-187.12.98-1.69a128.5 128.5 0 0 0 5.25-9.09l7.34-17.28a128.48 128.48 0 0 0 6.88-41.48C256.96 57.52 199.44 0 128.48 0ZM44.47 49.27l2.52 2.37c7.74 7.3 16.96 12.23 28.02 15.02 4.65 1.17 4.62 1.17 8.11-.34 25.92-11.2 64.61-11.21 91-.03 7.28 3.09 25.1-4.12 36.1-14.58a30.17 30.17 0 0 1 2.43-2.18c.34 0 .47 6.13.19 9.02a49.7 49.7 0 0 1-9.28 24.82l-1.48 2.06.7.94c38.07 51.55-7.45 115.42-60.6 85-1.3-.74-2.45-1.24-2.56-1.12-.2.2-5.5 10.18-9.4 17.68-1.77 3.4-1.41 3.4-3.26-.13-4.77-9.13-9.38-17.71-9.5-17.71-.09 0-1.19.6-2.45 1.32-52.6 30.1-99.04-34.14-60.98-84.34.77-1.02 1.1-1.66.94-1.86-7.67-10.45-11.38-21.86-10.7-32.92zM89.3 88.5a35.62 35.62 0 0 0-15.47 3.91c-35.27 17.48-16.99 71.8 21.65 64.34a35.5 35.5 0 0 0 22.49-14.72c1.26-1.78 1.68-2.21 1.88-1.93.15.2 2.15 3.62 4.44 7.6 4.88 8.42 4.11 7.82 5.91 4.67a554.85 554.85 0 0 1 7.31-12.45c.15-.07.97.91 1.86 2.23 16.1 23.61 52.74 18.48 61.43-8.6 8.53-26.59-16.97-51.8-43.57-43.1-12.12 3.98-20.12 12.66-27.95 30.34l-.69 1.56-1.38-3.14c-8.81-20-22.72-30.88-37.9-30.71zm76.42 18.05c4.34-.12 8.8 1.42 12.33 4.95 11.83 11.83.35 31.55-16.05 27.58-8.43-2.05-13.82-10.73-12.05-19.41 1.64-8.06 8.54-12.91 15.77-13.12zm-75.7.03c8.54-.47 16.98 5.68 17.59 15.6.67 11.03-10.1 19.76-20.84 16.89-12.5-3.34-16.73-18.36-7.72-27.46a16.6 16.6 0 0 1 10.97-5.03zm6.22 4.93c-1.1.08-2.18.56-3 1.54-3.25 3.87 1.28 9.25 5.69 6.75a4.52 4.52 0 0 0 2.22-3.85c0-2.81-2.5-4.6-4.91-4.44zm63.97.07c-3.15.2-5.02 3.3-3.64 6.36 1.05 2.3 4.27 3.26 6.3 1.87 3.77-2.56 2.44-7.96-2.02-8.23a5.19 5.19 0 0 0-.64 0z"/></svg>`;
</script> </script>
<!--
Check for 'feature.side' and place map pin on that side of the track.
Use 'reversed' to switch sides if the route is reversed.
-->
<svg viewBox="0 0 64 64" width="64" height="64" class="loops"> <svg viewBox="0 0 64 64" width="64" height="64" class="loops">
<BaseTrack {activeElec} height={64} /> <BaseTrack {activeElec} height={64} />
</svg> </svg>

View File

@@ -16,6 +16,7 @@ export const components = {
crossovers: Crossover, crossovers: Crossover,
siteof: SiteOf, siteof: SiteOf,
bridge: Bridge, bridge: Bridge,
minorBridge: Bridge,
crossover: Crossover, crossover: Crossover,
crossing: Crossing, crossing: Crossing,
loop: Loop, loop: Loop,

View File

@@ -1,11 +1,14 @@
<script lang="ts"> <script lang="ts">
import favicon from '$lib/assets/favicon.svg'; import favicon from '$lib/assets/round-logo.svg';
import appleLogo from '$lib/assets/apple-touch-icon.png';
import '$lib/assets/global.css';
let { children } = $props(); let { children } = $props();
</script> </script>
<svelte:head> <svelte:head>
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
<link rel="apple-touch-icon" href={appleLogo} />
</svelte:head> </svelte:head>
{@render children()} {@render children()}

View File

@@ -1,23 +1,27 @@
<script lang="ts"> <script lang="ts">
import logo from '$lib/assets/round-logo-text.svg';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { base } from '$app/paths'; import { resolve } from '$app/paths';
export let data: PageData;
let searchTerm = ''; let { data }: { data: PageData } = $props();
let searchTerm = $state('');
const formatDate = (dateVal: string | null) => { let filteredMaps = $derived(
if (!dateVal) return '---'; data.maps.filter((m) => {
const d = new Date(dateVal); const term = searchTerm.toLowerCase();
return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: '2-digit' });
};
$: filteredMaps = data.maps.filter( if (m.routeId.toString().includes(term)) return true;
(m) =>
m.routeId.toString().includes(searchTerm) || return m.contents.some((location) => location.toLowerCase().includes(term));
m.routeStart.toLowerCase().includes(searchTerm.toLowerCase()) || })
m.routeEnd.toLowerCase().includes(searchTerm.toLowerCase())
); );
const vibrate = (pattern: number | number[] = 10) => {
if (typeof window !== 'undefined' && window.navigator.vibrate) {
window.navigator.vibrate(pattern);
}
};
const isVerifiedRecently = (dateVal: string | null) => { const isVerifiedRecently = (dateVal: string | null) => {
if (!dateVal) return 'draft'; if (!dateVal) return 'draft';
@@ -26,51 +30,40 @@
return checkedDate > oneYearAgo ? 'verified' : 'stale'; return checkedDate > oneYearAgo ? 'verified' : 'stale';
}; };
const formatDate = (dateVal: string | null) => {
if (!dateVal) return 'N/A';
const date = new Date(dateVal);
// Check for invalid dates to avoid "NaN/NaN/NaN"
if (isNaN(date.getTime())) return 'Invalid Date';
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear().toString().slice(-2);
return `${d}/${m}/${y}`;
};
</script> </script>
<div class="page-wrapper"> <div class="page-wrapper">
<header class="main-header"> <header class="main-header">
<div class="brand-container"> <div class="brand-container">
<img <img src={logo} alt="OwlBoard Logo" class="main-logo" />
src="https://owlboard.info/images/logo/wide_logo.svg"
alt="OwlBoard Logo"
class="main-logo"
/>
<div class="brand-text">
<h1>Routes</h1>
</div>
<div class="search-container">
<input
type="text"
bind:value={searchTerm}
placeholder="Search index..."
class="search-input"
/>
</div>
</div> </div>
</header> </header>
<div class="list-container"> <div class="list-container">
<a <a href="https://owlboard.info" class="button-link">Go to OwlBoard Live Departures & PIS</a>
href="https://owlboard.info"
class="card external-card" <input type="text" bind:value={searchTerm} placeholder="Search" class="search-input" />
target="_blank"
rel="noopener noreferrer"
>
<div class="card-top">
<span class="route-id system-tag">OWL</span>
<span class="status-badge external">External Tool</span>
</div>
<div class="card-body">
<div class="location">OwlBoard</div>
<p class="description">Live Departure Boards and PIS Codes</p>
</div>
<div class="card-footer">
<span>Visit owlboard.info ↗</span>
</div>
</a>
{#each filteredMaps as map (map.routeId)} {#each filteredMaps as map (map.routeId)}
<a href="{base}/map/{map.routeId.toString().padStart(4, '0')}" class="card"> <a
href={resolve(`/map/${map.routeId.toString().padStart(4, '0')}`)}
class="card"
onclick={() => vibrate(10)}
>
<div class="card-top"> <div class="card-top">
<span class="route-id">{map.routeId.toString().padStart(4, '0')}</span> <span class="route-id">{map.routeId.toString().padStart(4, '0')}</span>
<span class="status-badge {isVerifiedRecently(map.checked)}"> <span class="status-badge {isVerifiedRecently(map.checked)}">
@@ -91,7 +84,7 @@
</div> </div>
<div class="card-footer"> <div class="card-footer">
<span>Created: {formatDate(map.created)}</span> <span>Updated: {formatDate(map.updated)}</span>
{#if map.checked} {#if map.checked}
<span>• Checked: {formatDate(map.checked)}</span> <span>• Checked: {formatDate(map.checked)}</span>
{/if} {/if}
@@ -107,7 +100,8 @@
/* Mobile-First Base Styles */ /* Mobile-First Base Styles */
:global(body) { :global(body) {
margin: 0; margin: 0;
background-color: #f8fafc; background-color: #404c55;
background-image: radial-gradient(#2b343c, #404c55);
color: #0f172a; color: #0f172a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
} }
@@ -120,94 +114,115 @@
.main-header { .main-header {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
position: fixed;
height: 80px;
top: 0;
left: 0;
box-sizing: border-box;
width: 100%;
background: #3c6f79;
color: #ebebeb;
z-index: 100;
} }
.brand-container { .brand-container {
padding: 15px;
display: flex; display: flex;
width: 100%;
align-items: center; align-items: center;
box-sizing: border-box;
gap: 1rem; gap: 1rem;
margin-bottom: 1.5rem;
} }
.main-logo { .main-logo {
/* Adjust height to fit your logo's aspect ratio */ height: 52px;
height: 48px;
width: auto; width: auto;
display: block; display: block;
} }
.brand-text h1 {
font-size: 1.5rem;
font-weight: 900;
margin: 0;
line-height: 1;
letter-spacing: -0.05em;
font-style: italic;
}
.sub-brand {
font-size: 0.65rem;
font-weight: 800;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.1em;
margin: 0.2rem 0 0 0;
}
/* Desktop layout adjustment */
@media (min-width: 640px) {
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
}
.brand-container {
margin-bottom: 0;
}
.main-logo {
height: 56px;
}
}
.search-input { .search-input {
width: 100%; width: 100%;
max-width: 500px;
font-family: 'urwgothic';
margin: auto;
height: 40px;
padding: 0.8rem 1rem; padding: 0.8rem 1rem;
border: 2px solid #e2e8f0; text-align: center;
border-radius: 12px; border-radius: 30px;
border: none;
font-size: 1rem; font-size: 1rem;
transition: all 0.3s ease;
box-sizing: border-box; /* Ensures padding doesn't break width */ box-sizing: border-box; /* Ensures padding doesn't break width */
outline: none; outline: none;
} }
.search-input:focus { .search-input:hover {
border-color: #3b82f6; box-shadow: rgba(0, 0, 0, 0.46);
} }
/* Card Layout (Mobile) */
.list-container { .list-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
padding: 90px 1rem 1rem 1rem;
isolation: isolate;
z-index: 1;
}
@media (max-width: 600px) {
.list-container {
padding-top: 90px;
gap: 0.75rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.card {
padding: 10px 12px;
margin: 0;
border-radius: 4px;
}
.location {
font-size: 1rem;
margin-bottom: 0.1rem;
font-weight: 500;
}
.path-arrow {
color: #fff;
font-size: 0.8rem;
margin: 0.1rem 0;
line-height: 0.75;
}
.card-footer {
font-size: 0.6rem;
line-height: 0.8rem;
padding-bottom: 2px;
}
} }
.card { .card {
display: block; display: block;
background: white; background: #3c6f79;
padding: 1.25rem; padding: 1.25rem;
border-radius: 16px; border-radius: 35px;
text-decoration: none; text-decoration: none;
color: inherit; color: #4fd1d1;
border: 1px solid #e2e8f0; text-shadow: 2px 1px 10px rgba(0, 0, 0, 0.29);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.482);
transition: transform 0.1s ease; transition: transform 0.1s ease;
cursor: pointer; cursor: pointer;
} }
.card:hover {
transform: scale(1.01);
z-index: 5;
}
.card:active { .card:active {
transform: scale(0.98); /* Tactile feedback for mobile */ transform: scale(0.99); /* Tactile feedback for mobile */
} }
.card-top { .card-top {
@@ -220,8 +235,9 @@
.route-id { .route-id {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-weight: 800; font-weight: 800;
color: #3b82f6; color: #fff;
background: #eff6ff; background: #2d2d2d;
text-shadow: none;
padding: 0.2rem 0.5rem; padding: 0.2rem 0.5rem;
border-radius: 6px; border-radius: 6px;
} }
@@ -231,30 +247,31 @@
} }
.location { .location {
font-size: 1.125rem; font-family: 'urwgothic';
font-size: 1.23rem;
font-weight: 700; font-weight: 700;
letter-spacing: -0.02em;
} }
.path-arrow { .path-arrow {
color: #acc7e8; color: #fff;
font-family: 'urwgothic';
font-size: 0.9rem; font-size: 0.9rem;
margin: 0.2rem 0; margin: 0.2rem 0;
} }
.card-footer { .card-footer {
font-size: 0.7rem; font-size: 0.7rem;
color: #64748b; color: #e2ebeb;
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
letter-spacing: 0.05em; letter-spacing: 0.05em;
border-top: 1px solid #f1f5f9; padding-top: 0;
padding-top: 0.75rem;
} }
.status-badge { .status-badge {
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 800; font-weight: 800;
text-shadow: none;
text-transform: uppercase; text-transform: uppercase;
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
border-radius: 999px; border-radius: 999px;
@@ -277,56 +294,22 @@
color: #475569; color: #475569;
} }
@media (min-width: 640px) { .button-link {
.page-wrapper { text-decoration: none;
padding: 3rem 1.5rem; border: none;
background: #3c6f79;
max-width: 360px;
padding: 8px 25px;
margin: auto;
text-align: center;
border-radius: 25px;
width: auto;
color: #e2ebeb;
transition: 0.3s all ease;
font-weight: 620;
} }
.main-header { .button-link:hover {
display: flex; background: #2b2b2b;
justify-content: space-between;
align-items: center;
}
.search-input {
width: 300px;
}
.path-arrow {
display: inline-block;
margin: 0 0.5rem;
}
.card-body {
display: flex;
align-items: center;
}
}
.external-card {
border-left: 4px solid #6366f1;
background: linear-gradient(to right, #ffffff, #f5f3ff);
}
.system-tag {
background: #eef2ff !important;
color: #4338ca !important;
}
.status-badge.external {
background: #6366f1;
color: white;
}
.description {
margin: 0.25rem 0 0 0;
font-size: 0.9rem;
color: #64748b;
font-weight: 500;
}
.external-card:hover {
border-color: #4338ca;
background: #f5f3ff;
} }
</style> </style>

View File

@@ -1,5 +1,14 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export interface RouteMapIndex {
routeId: string | number;
routeStart: string;
routeEnd: string;
updated: string;
checked: string;
contents: string[];
}
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('map-index.json'); const response = await fetch('map-index.json');
@@ -12,6 +21,6 @@ export const load: PageLoad = async ({ fetch }) => {
return { return {
maps: maps.sort((a: any, b: any) => { maps: maps.sort((a: any, b: any) => {
return Number(a.routeId) - Number(b.routeId); return Number(a.routeId) - Number(b.routeId);
}) }) as RouteMapIndex[]
}; };
}; };

View File

@@ -0,0 +1,45 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import logo from '$lib/assets/round-logo.svg';
import maskableLogo from '$lib/assets/maps-logo-maskable.svg';
import appleLogo from '$lib/assets/apple-touch-icon.png';
export const prerender = true;
export const GET: RequestHandler = ({ url }) => {
const manifest = {
name: 'OwlBoard Maps',
short_name: 'OB Maps',
start_url: '/',
display: 'standalone',
theme_color: '#4fd1d1',
background_color: '#3d4952',
icons: [
{
src: logo,
sizes: '48x48 72x72 96x96 128x128 256x256 512x512 any',
type: 'image/svg+xml',
purpose: 'any'
},
{
src: maskableLogo,
sizes: '48x48 72x72 96x96 128x128 256x256 512x512 any',
type: 'image/svg+xml',
purpose: 'maskable'
},
{
src: appleLogo,
sizes: '180x180',
type: 'image/png',
purpose: 'any'
}
]
};
return json(manifest, {
headers: {
'Content-Type': 'application/manifest+json',
'Cache-Control': 'public, max-age=3600'
}
});
};

View File

@@ -2,7 +2,10 @@
import RouteRow from '$lib/components/RouteRow.svelte'; import RouteRow from '$lib/components/RouteRow.svelte';
import RouteEndLink from '$lib/components/mapIcons/RouteEndLink.svelte'; import RouteEndLink from '$lib/components/mapIcons/RouteEndLink.svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { base } from '$app/paths'; import { resolve } from '$app/paths';
import logo from '$lib/assets/round-logo.svg';
import { IconArrowsExchange, IconSettings } from '@tabler/icons-svelte';
// data.route contains: routeStart, routeEnd, routeId, elecStart, elecEnd, routeDetail[] // data.route contains: routeStart, routeEnd, routeId, elecStart, elecEnd, routeDetail[]
export let data; export let data;
@@ -11,6 +14,7 @@
let visibleTypes = { let visibleTypes = {
station: true, station: true,
minorBridge: false,
bridge: true, bridge: true,
crossovers: true, crossovers: true,
loop: true, loop: true,
@@ -61,8 +65,8 @@
<div class="map-layout"> <div class="map-layout">
<header class="top-nav"> <header class="top-nav">
<div class="nav-cluster"> <div class="nav-cluster">
<a href="{base}/" class="home-link" title="Back to Index"> <a href={resolve('/')} aria-label="Home" class="home-link" title="Back to Index">
<span class="home-icon"></span> <img src={logo} alt="OwlBoard Logo" class="nav-logo" />
</a> </a>
<div class="route-stack"> <div class="route-stack">
@@ -78,20 +82,26 @@
</div> </div>
<div class="quick-actions"> <div class="quick-actions">
<button class="icon-btn" on:click={() => (reversed = !reversed)}> <button class="icon-btn" onclick={() => (reversed = !reversed)}>
{reversed ? 'UP' : 'DN'} <IconArrowsExchange />
</button> </button>
<button class="icon-btn" on:click={() => (showFilters = !showFilters)}> Settings </button> <button class="icon-btn" onclick={() => (showFilters = !showFilters)}> <IconSettings /> </button>
</div> </div>
</header> </header>
{#if showFilters} {#if showFilters}
<div class="backdrop" on:click={() => (showFilters = false)}></div> <div
class="backdrop"
role="button"
tabindex="0"
onclick={() => (showFilters = false)}
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && (showFilters = false)}
></div>
<div class="filter-drawer" transition:slide> <div class="filter-drawer" transition:slide>
<div class="drawer-header"> <div class="drawer-header">
<h3>Visibility Filters</h3> <h3>Visibility Filters</h3>
<button class="close-icon" on:click={() => (showFilters = false)} aria-label="Close"> <button class="close-icon" onclick={() => (showFilters = false)} aria-label="Close">
<svg <svg
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="20" width="20"
@@ -111,7 +121,7 @@
<button <button
class="filter-chip" class="filter-chip"
class:active={visibleTypes[type]} class:active={visibleTypes[type]}
on:click={() => toggleFilter(type)} onclick={() => toggleFilter(type)}
> >
{formatLabel(type)} {formatLabel(type)}
</button> </button>
@@ -145,13 +155,20 @@
} }
.top-nav { .top-nav {
position: fixed;
width: 100%;
box-sizing: border-box;
top: 0;
height: 80px;
left: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0.5rem 1rem; padding: 0 0;
background: #0f172a; background: #3c6f79;
color: white; color: #e1ebeb;
gap: 1rem; gap: 1rem;
z-index: 10000;
} }
.nav-cluster { .nav-cluster {
@@ -165,21 +182,28 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 36px; width: 52px;
height: 36px; height: 52px;
background: #1e293b; padding-left: 0;
border: 1px solid #334155; margin-left: 15px;
border-radius: 8px; flex-shrink: 0;
color: #94a3b8; transition: all 0.3s ease;
text-decoration: none; }
font-size: 1.2rem;
flex-shrink: 0; /* Keeps button from squishing on mobile */ @media (max-width: 350px) {
.home-link {
width: 42px;
height: 42px;
}
} }
.home-link:hover { .home-link:hover {
background: #334155; transform: translateY(-1px) scale(1.05);
color: white; filter: brightness(1.1);
border-color: #475569; }
.home-link:active {
transform: scale(0.95);
} }
.route-stack { .route-stack {
@@ -193,13 +217,14 @@
font-weight: 800; font-weight: 800;
margin: 0; margin: 0;
white-space: nowrap; white-space: nowrap;
color: #cce9e9;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.secondary-station { .secondary-station {
font-size: 0.7rem; font-size: 0.7rem;
color: #94a3b8; color: #cce9e9;
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
} }
@@ -207,49 +232,23 @@
.quick-actions { .quick-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
} margin-right: 15px;
.icon-btn {
background: #1e293b;
border: 1px solid #334155;
color: white;
padding: 0.4rem 0.6rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: bold;
cursor: pointer;
white-space: nowrap;
} }
.map-spine { .map-spine {
padding-top: 72px; padding-top: 72px;
} }
.secondary-station { @media (min-width: 536px) {
font-size: 0.85rem;
font-weight: 500;
color: #64748b;
letter-spacing: 0.025em;
margin-top: 2px;
}
@media (min-width: 768px) {
.primary-station { .primary-station {
font-size: 1.5rem; font-size: 1.5rem;
} }
.secondary-station { .secondary-station {
font-size: 1rem; font-size: 1rem;
} }
.top-nav {
padding: 0 2rem;
height: 90px;
}
.map-spine {
padding-top: 90px;
} }
@media (min-width: 768px) {
h1 { h1 {
font-size: 1.5rem; font-size: 1.5rem;
letter-spacing: -0.03em; letter-spacing: -0.03em;
@@ -268,6 +267,7 @@
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
z-index: 150; z-index: 150;
transition: all 0.3s ease;
} }
.filter-drawer { .filter-drawer {
@@ -275,7 +275,7 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: white; background: #3c6f79;
z-index: 200; z-index: 200;
border-radius: 20px 20px 0 0; border-radius: 20px 20px 0 0;
box-shadow: 0 -8px 20px rgba(0, 0, 0, 0.15); box-shadow: 0 -8px 20px rgba(0, 0, 0, 0.15);
@@ -288,17 +288,17 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1.25rem 1.5rem 0.75rem; padding: 1.25rem 1.5rem 0.75rem;
border-bottom: 1px solid #f1f5f9; border-bottom: 1px solid #e1ebeb;
} }
.drawer-header h3 { .drawer-header h3 {
margin: 0; margin: 0;
font-size: 1rem; font-size: 1rem;
color: #1e293b; color: #e1ebeb;
} }
.close-icon { .close-icon {
background: #f1f5f9; background: #404c55;
border: none; border: none;
border-radius: 50%; border-radius: 50%;
width: 32px; width: 32px;
@@ -307,7 +307,12 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
color: #64748b; color: #e1ebeb;
transition: all 0.3s ease;
}
.close-icon:hover {
background: #2d2d2d;
} }
.drawer-content { .drawer-content {
@@ -324,14 +329,14 @@
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 999px; border-radius: 999px;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
background: #f8fafc; background: #ff6060;
font-size: 0.875rem; font-size: 0.875rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.filter-chip.active { .filter-chip.active {
background: #1e293b; background: #00725b;
color: white; color: white;
border-color: #1e293b; border-color: #1e293b;
} }
@@ -346,23 +351,28 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
border: none;
padding: 0.5rem 0.8rem; padding: 0.5rem 0.8rem;
border-radius: 12px; border-radius: 12px;
border: 1px solid #e2e8f0; background: #404c55;
background: #f8fafc; color: #e1ebeb;
color: #475569;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.02em; letter-spacing: 0.02em;
transition: all 0.3s ease;
}
@media (max-width: 350px) {
.icon-btn {
padding: 0.3rem 0.3rem;
}
} }
.icon-btn:hover { .icon-btn:hover {
background: #f1f5f9; background: #2d2d2d;
border-color: #cbd5e1;
color: #1e293b;
} }
.icon-btn:active { .icon-btn:active {

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +1,8 @@
routeStart: Paddington routeStart: Paddington
routeEnd: Reading West Jn routeEnd: Reading
routeId: 0001 routeId: 0001
created: 2026-02-04 updated: 2026-02-09
checked: 2026-02-06 checked: 2026-02-09
signallerStart: TVSC Paddington WS signallerStart: TVSC Paddington WS
signallerEnd: TVSC Reading WS signallerEnd: TVSC Reading WS
elecStart: elecStart:
@@ -154,11 +154,11 @@ routeDetail:
miles: 3 miles: 3
chains: 0 chains: 0
- type: junction - type: siteof
name: Old Oak Common West name: Old Oak Common West
diverges: left diverges: left
direction: up direction: up
description: Out of Use for HS2 Works description: Junction has been removed
miles: 3 miles: 3
chains: 20 chains: 20
@@ -180,7 +180,8 @@ routeDetail:
name: Friars Junction name: Friars Junction
diverges: left diverges: left
direction: up direction: up
description: Out of Use for HS2 Works elecBranch: none
description: Out of Use for HS2 Works. Track temporarily disconnected.
miles: 3 miles: 3
chains: 53 chains: 53
@@ -314,6 +315,7 @@ routeDetail:
name: West Ealing Junction name: West Ealing Junction
diverges: left diverges: left
direction: down direction: down
elecBranch: none
description: Dn/Up Greenford to Greenford & West Ealing Sidings description: Dn/Up Greenford to Greenford & West Ealing Sidings
miles: 6 miles: 6
chains: 54 chains: 54
@@ -329,6 +331,7 @@ routeDetail:
name: Hanwell Junction name: Hanwell Junction
diverges: left diverges: left
direction: up direction: up
elecBranch: none
description: Dn/Up Loop to Greenford & West Ealing Sidings description: Dn/Up Loop to Greenford & West Ealing Sidings
miles: 7 miles: 7
chains: 19 chains: 19
@@ -371,6 +374,8 @@ routeDetail:
- type: loop - type: loop
position: left position: left
name: Hanwell Up & Dn Goods Loops name: Hanwell Up & Dn Goods Loops
description: Both on Up side
elecLoop: none
miles: 8 miles: 8
chains: 20 chains: 20
@@ -392,6 +397,7 @@ routeDetail:
name: Southall East Junction name: Southall East Junction
diverges: right diverges: right
direction: up direction: up
elecBranch: none
description: Brentford Waste Terminal & Southall Depot description: Brentford Waste Terminal & Southall Depot
miles: 8 miles: 8
chains: 62 chains: 62
@@ -638,6 +644,7 @@ routeDetail:
name: Langley GF name: Langley GF
diverges: left diverges: left
description: Langley Sidings description: Langley Sidings
elecBranch: none
direction: up direction: up
miles: 15 miles: 15
chains: 76 chains: 76
@@ -1179,7 +1186,7 @@ routeDetail:
diverges: right diverges: right
direction: up direction: up
elecBranch: none elecBranch: none
description: to Southern Lines (Waterloo/Guildford) description: Up/Dn Reading Spur to Southern Lines (Waterloo/Guildford)
miles: 35 miles: 35
chains: 40 chains: 40
@@ -1187,6 +1194,7 @@ routeDetail:
position: under position: under
category: rail category: rail
name: Low Level Tunnel name: Low Level Tunnel
description: Reading Low Level line
miles: 35 miles: 35
chains: 40 chains: 40
@@ -1211,43 +1219,6 @@ routeDetail:
miles: 35 miles: 35
chains: 78 chains: 78
- type: bridge
name: Caversham Road
position: under
category: minorRoad
miles: 35
chains: 11
- type: junction
diverges: right
direction: down
name: Westbury Line Junction
description: to Oxford Road Jn
miles: 36
chains: 17
- type: junction
diverges: right
direction: down
name: Caversham Road Junction
description: Reading Feeder Main/Relief diverge and pass under Reading Viaduct to Oxford Rd Jn
miles: 36
chains: 22
- type: crossovers
name: Reading High Level Junction
description: Down Reading Festival Connects to Down Main
miles: 36
chains: 47
- type: junction
diverges: right
direction: up
name: Reading West Junction
description: to Oxford Road Junction (From relief lines only)
miles: 37
chains: 17
- type: continues - type: continues
routeName: Reading West Jn - Bristol TM routeName: Reading - Bristol TM
routeId: "0002" routeId: '0002'

View File

@@ -1,7 +1,7 @@
routeStart: Reading West Jn routeStart: Reading
routeEnd: Bristol TM routeEnd: Bristol TM
routeId: 0002 routeId: 0002
created: 2026-02-04 updated: 2026-02-04
checked: checked:
signallerStart: TVSC Reading WS signallerStart: TVSC Reading WS
signallerEnd: TVSC Temple Meads WS signallerEnd: TVSC Temple Meads WS
@@ -12,10 +12,47 @@ elecEnd:
elec: none elec: none
routeDetail: routeDetail:
- type: continues - type: continues
routeName: Paddington - Reading West Jn routeName: Paddington - Reading
routeId: "0001" routeId: '0001'
- type: station
name: Reading
miles: 35
chains: 78
- type: bridge
name: Caversham Road
position: under
category: minorRoad
miles: 35
chains: 11
- type: junction
diverges: right
direction: down
name: Westbury Line Junction
description: to Oxford Road Jn
miles: 36
chains: 17
- type: junction
diverges: right
direction: down
name: Caversham Road Junction
description: Reading Feeder Main/Relief diverge and pass under Reading Viaduct to Oxford Rd Jn
miles: 36
chains: 22
- type: crossovers - type: crossovers
name: Scours Lane Junction name: Reading High Level Junction
description: Line diverges description: Down Reading Festival Connects to Down Main
miles: 38 miles: 36
chains: 90 chains: 47
- type: junction
diverges: right
direction: up
name: Reading West Junction
description: to Oxford Road Junction (From relief lines only)
miles: 37
chains: 17

View File

@@ -0,0 +1,12 @@
routeStart: Swindon Junction
routeEnd: Standish Junction
routeId: 0230
updated: 2026-02-11
checked: 2026-02-11
signallerStart: TVSC Swindon WS
signallerEnd: Gloucester PSB
elecStart:
elec: 25kvac
eco: Didcot (TVSC)
elecEnd:
elec: none

View File

@@ -0,0 +1,652 @@
routeStart: Wootton Bassett Junction
routeEnd: Stoke Gifford Junction
routeId: 0240
updated: 2026-02-11
checked: 2026-02-11
signallerStart: TVSC Swindon WS
signallerEnd: TVSC Stoke Gifford WS
elecStart:
elec: 25kvac
eco: Didcot (TVSC)
elecEnd:
elec: 25kvac
eco: Didcot (TVSC)
routeDetail:
- type: continues
routeName: Reading - Bristol TM
entryPoint: wootton-bassett-junction
routeId: '0002'
- type: junction
diverges: right
direction: down
name: Wootton Basset Junction
description: to Chippenham & Bristol via Bath
miles: 83
chains: 7
- type: loop
name: Up Wootton Basset Goods Line
description: Loop spans junction
position: left
miles: 83
chains: 10
- type: bridge
position: over
name: Bath Road
roadName: A3102
category: aroad
miles: 83
chains: 29
- type: crossovers
name: Wootton Basset West
miles: 84
chains: 7
- type: minorBridge
name: Whitehill Lane
position: under
category: road
miles: 84
chains: 13
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 84
chains: 39
- type: bridge
position: over
name: Motorway
roadName: M4
category: motorway
miles: 84
chains: 50
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 85
chains: 15
- type: minorBridge
position: over
name: Callow Hill
category: road
miles: 85
chains: 29
- type: minorBridge
name: Farm Bridge
position: over
category: road
miles: 85
chains: 58
- type: minorBridge
name: Brinkworth Brooklands
position: over
category: road
miles: 86
chains: 75
- type: crossing
kind: foot
name: Brinkworth Foot Crossing
miles: 87
chains: 17
- type: minorBridge
name: Farm Bridge
category: road
position: over
miles: 88
chains: 12
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 88
chains: 46
- type: minorBridge
name: School Hill
position: under
category: road
miles: 88
chains: 66
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 89
chains: 9
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 89
chains: 29
- type: minorBridge
name: Idover Lane
position: under
category: road
miles: 89
chains: 46
- type: minorBridge
name: The Street
position: under
category: road
miles: 89
chains: 70
- type: bridge
name: River Avon
position: under
category: waterway
miles: 90
chains: 26
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 90
chains: 60
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 91
chains: 36
- type: minorBridge
name: Startley Road
position: under
category: road
miles: 91
chains: 50
- type: minorBridge
name: Rodbourne Lane
description: or 'Pound Hill'
position: under
category: road
miles: 92
chains: 2
- type: minorBridge
name: Cabbage Lane
position: over
category: road
miles: 92
chains: 28
- type: crossing
name: Kingway Barn Foot Crossing
kind: foot
miles: 93
chains: 7
- type: bridge
name: Kingway
position: under
category: aroad
roadName: A429
miles: 93
chains: 30
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 93
chains: 70
- type: loop
position: right
name: Down Hullavington Goods Loop
miles: 94
chains: 5
- type: minorBridge
position: under
name: Court Farm
category: road
miles: 94
chains: 9
- type: minorBridge
position: under
name: Bradfield Cottages
category: road
miles: 94
chains: 27
- type: loop
name: Up Hullavington Goods Loop
position: left
miles: 94
chains: 42
- type: crossovers
name: Hullavington
miles: 94
chains: 45
- type: minorBridge
name: Gills Hunting
position: over
category: road
miles: 94
chains: 62
- type: bridge
name: Aqueduct
position: over
category: stream
miles: 95
chains: 22
- type: minorBridge
name: Happy Lands
position: over
category: road
miles: 95
chains: 40
- type: minorBridge
name: Pig Lane
position: over
category: road
miles: 95
chains: 69
- type: minorBridge
name: Fosse Way
category: road
position: over
miles: 96
chains: 47
- type: minorBridge
name: Pipeline
category: pipeline
position: over
miles: 96
chains: 48
- type: minorBridge
name: Footbridge
position: over
category: foot
miles: 96
chains: 64
- type: tunnel
name: Alterton Tunnel
tunnelType: whole
length: 0mi 506yd
miles: 97
chains: 46
- type: signallerChange
from: TVSC Swindon WS (SW)
to: TVSC Stoke Gifford WS (BL)
miles: 98
chains: 0
- type: minorBridge
name: Footbridge
category: foot
position: over
miles: 98
chains: 2
- type: minorBridge
name: Footbridge
category: foot
positon: over
miles: 98
chains: 17
- type: minorBridge
name: Alderton Road
position: over
category: road
miles: 98
chains: 40
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 98
chains: 76
- type: minorBridge
name: Farm Bridge
position: under
category: road
miles: 99
chains: 7
- type: minorBridge
name: Footbridge
category: foot
position: over
miles: 99
chains: 40
- type: bridge
name: Luckington Road
category: road
roadName: B4040
position: over
miles: 99
chains: 46
- type: minorBridge
name: Footbridge
category: foot
position: over
miles: 99
chains: 59
- type: siteof
name: Badminton
side: centre
miles: 100
chains: 1
- type: bridge
name: Station Road
position: over
category: road
miles: 100
chains: 4
- type: minorBridge
name: Footbridge
category: foot
position: over
miles: 100
chains: 33
- type: bridge
name: Aqueduct
position: over
category: stream
miles: 100
chains: 57
- type: minorBridge
name: Footbridge
category: foot
position: over
miles: 100
chains: 65
- type: tunnel
tunnelType: whole
length: 2mi 926yd
name: Chipping Sodbury Tunnel
miles: 102
chains: 21
description: 10 telephones on Up side
- type: minorBridge
position: over
name: Footbridge
category: foot
miles: 103
chains: 63
- type: bridge
name: Frome Bridge
position: over
category: stream
miles: 103
chains: 75
- type: minorBridge
name: Colt's Green Bridge
category: road
position: over
miles: 104
chains: 19
- type: loop
name: Chipping Sodbury Goods Loop
position: left
description: Bidirectional with access to Chipping Sodbury Sidings
miles: 104
chains: 31
- type: minorBridge
name: Farm Bridge
category: road
position: over
miles: 104
chains: 56
- type: minorBridge
name: Kingrove Lane
position: over
category: road
miles: 104
chains: 77
- type: bridge
name: Aqueduct
position: over
category: stream
miles: 105
chains: 18
- type: minorBridge
name: Dodington Road
category: road
position: over
miles: 105
chains: 19
- type: bridge
name: Aqueduct
category: stream
position: over
miles: 105
chains: 38
- type: minorBridge
name: Footbridge
category: foot
position: over
miles: 106
chains: 32
- type: bridge
name: Westerleigh Road
category: road
position: under
miles: 107
chains: 4
- type: junction
diverges: left
direction: up
name: Westerleigh Junction
miles: 107
chains: 14
description: Up/Dn Charfield towards Gloucester
- type: bridge
name: Westerleigh Branch
category: rail
position: under
miles: 107
chains: 15
- type: minorBridge
name: Dodmoor Farm
category: road
position: under
miles: 107
chains: 33
- type: minorBridge
name: Farm Bridge
category: road
position: under
miles: 107
chains: 70
- type: minorBridge
name: Boxhenge Farm Lane
category: road
position: under
miles: 108
chains: 10
- type: minorBridge
name: The Dramway
position: over
category: foot
miles: 108
chains: 25
- type: minorBridge
name: Henfield Road
position: under
category: road
miles: 107
chains: 52
- type: bridge
name: Bristol Road
category: aroad
position: under
roadName: A432
miles: 108
chains: 78
- type: minorBridge
name: Park Lane
position: under
category: road
miles: 109
chains: 27
- type: bridge
name: Upford Viaduct
positon: under
category: waterway
miles: 109
chains: 58
- type: minorBridge
name: Hicks Common Lane
position: over
category: road
miles: 109
chains: 71
- type: minorBridge
name: Down Road
category: road
position: over
miles: 110
chains: 15
- type: minorBridge
name: Mill Lane
category: road
position: under
miles: 110
chains: 24
- type: bridge
name: Winterbourne Viaduct
category: road
position: under
miles: 110
chains: 38
- type: minorBridge
name: Farm Bridge
category: road
position: under
miles: 110
chains: 48
- type: bridge
name: M4 Motorway
category: motorway
roadName: M4
position: under
miles: 110
chains: 63
- type: minorBridge
name: Old Gloucester Road
category: road
roadName: B4427
position: under
miles: 110
chains: 70
- type: minorBridge
name: Curtis Lane
position: over
category: road
miles: 111
chains: 14
- type: crossovers
name: Stoke Gifford East Junction
miles: 111
chains: 20
- type: minorBridge
name: Pearson's Brick Yard
category: road
position: over
miles: 111
chains: 32
- type: station
name: Bristol Parkway
description: Stoke Gifford Dn Sidings on Down side
miles: 111
chains: 62
- type: bridge
name: Bristol Road
category: road
position: under
miles: 111
chains: 77
- type: junction
name: Stoke Gifford No.1 Junction
diverges: right
direction: down
description: Up/Dn Filton & Bristol TM
miles: 111
chains: 79
- type: continues
routeName: Bristol TM - Cardiff West Shunt
entryPoint: patchway-junction
routeId: '9999'

View File

@@ -0,0 +1,12 @@
routeStart: Westerleigh Junction
routeEnd: Gloucester
routeId: 2420
updated: 2026-02-11
checked: 2026-02-11
signallerStart: TVSC Swindon WS
signallerEnd: Gloucester PSB
elecStart:
elec: 25kvac
eco: Didcot (TVSC)
elecEnd:
elec: none

View File

@@ -2,5 +2,14 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()] plugins: [sveltekit()],
build: {
assetsInlineLimit: 0,
cssCodeSplit: true,
sourcemap: false,
target: 'esnext',
modulePreload: {
polyfill: false
}
}
}); });