34 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
6c10cb628e Fix linter warnings 2026-02-06 23:54:50 +00:00
34 changed files with 2576 additions and 1674 deletions

View File

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

29
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@sveltejs/adapter-node": "^5.5.2",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tabler/icons-svelte": "^3.36.1",
"@types/node": "^22",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
@@ -1344,6 +1345,34 @@
"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": {
"version": "0.6.0",
"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/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tabler/icons-svelte": "^3.36.1",
"@types/node": "^22",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",

View File

@@ -6,6 +6,8 @@ const inputDir = './static/mapFiles/yaml';
const outputDir = './static/mapFiles/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 });
const mapList = [];
@@ -18,16 +20,41 @@ fs.readdirSync(inputDir).forEach((file) => {
const fileName = file.replace('.yaml', '.json');
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({
routeId: content.routeId || null,
routeStart: content.routeStart || null,
routeEnd: content.routeEnd || null,
created: content.created || null,
updated: content.updated || null,
checked: content.checked || null,
contents: Array.from(contentSet)
});
}
});
fs.writeFileSync(indexFile, JSON.stringify(mapList));
console.log(`Generated ${mapList.length} map files and index.`)
console.log(`Generated ${mapList.length} map files and index.`);

View File

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

View File

@@ -1,20 +1,20 @@
import { type HandleFetch } from "@sveltejs/kit";
import { type HandleFetch } from '@sveltejs/kit';
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
if (request.url.startsWith('https://maps.owlboard.info')) {
const newUrl = request.url.replace('https://maps.owlboard.info', 'http://localhost:3000');
const headers = new Headers(request.headers);
headers.set('host', 'maps.owlboard.info');
if (request.url.startsWith('https://maps.owlboard.info')) {
const newUrl = request.url.replace('https://maps.owlboard.info', 'http://localhost:3000');
request = new Request(newUrl, {
method: request.method,
headers: headers,
body: request.body,
// @ts-ignore - 'duplex' is needed for node fetch with bodies
duplex: 'half'
});
}
const headers = new Headers(request.headers);
headers.set('host', 'maps.owlboard.info');
return fetch(request);
}
request = new Request(newUrl, {
method: request.method,
headers: headers,
body: request.body,
// @ts-expect-error - 'duplex' is needed for node fetch with bodies
duplex: 'half'
});
}
return fetch(request);
};

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;
$: Icon = components[feature.type] || components.default;
const slugify = (str?: string) =>
str?.toLocaleLowerCase().trim().replace(/\s+/g, '-') ?? 'unknown';
</script>
<div class="row-container">
<div class="row-container" id={slugify(feature.name)}>
<div class="mileage-col">
<span class="miles">{feature.miles + 'm' || ''}</span>
<span class="chains">{feature.chains + 'ch' || ''}</span>

View File

@@ -4,7 +4,15 @@
export let feature: {
position: 'over' | 'under';
category: 'rail' |'stream'| 'foot' | 'aroad' | 'minorRoad' | 'motorway' | 'waterway' | 'pipeline';
category:
| 'rail'
| 'stream'
| 'foot'
| 'aroad'
| 'minorRoad'
| 'motorway'
| 'waterway'
| 'pipeline';
roadName?: string;
};
export let activeElec: ElecType;
@@ -98,11 +106,10 @@
{#if isOver}
<BaseTrack {activeElec} height={64} />
<!-- White edge, to provide separation between abutment line and bridge content -->
<rect x="0" y={32 - s.width / 2 - 4} width="64" height={s.width + 8} fill="white" />
<!-- White edge, to provide separation between abutment line and bridge content -->
<rect x=0 y={32 - (s.width / 2) - 4} width="64" height="{s.width + 8}" fill="white" />
<rect x="6" y={32 - (s.width / 2)} width="52" height={s.width} fill={s.bg} />
<rect x="6" y={32 - s.width / 2} width="52" height={s.width} fill={s.bg} />
{#if s.type === 'road'}
<line
@@ -120,15 +127,17 @@
<line x1={xLeft} y1="36" x2={xRight} y2="36" stroke="#cbd5e1" stroke-width="1.5" />
{/if}
<g fill="none" stroke={s.stroke} stroke-width="2.5" stroke-linecap="square">
<path d={`M 20 ${32 - s.width / 2 - 6} L 20 ${32 - s.width / 2 - 4} L 44 ${32 - s.width / 2 - 4} L 44 ${32 - s.width / 2 - 6}`} />
<path d={`M 20 ${32 + s.width / 2 + 6} L 20 ${32 + s.width / 2 + 4} L 44 ${32 + s.width / 2 + 4} L 44 ${32 + s.width / 2 + 6}`} />
</g>
<g fill="none" stroke={s.stroke} stroke-width="2.5" stroke-linecap="square">
<path
d={`M 20 ${32 - s.width / 2 - 6} L 20 ${32 - s.width / 2 - 4} L 44 ${32 - s.width / 2 - 4} L 44 ${32 - s.width / 2 - 6}`}
/>
<path
d={`M 20 ${32 + s.width / 2 + 6} L 20 ${32 + s.width / 2 + 4} L 44 ${32 + s.width / 2 + 4} L 44 ${32 + s.width / 2 + 6}`}
/>
</g>
{:else}
<rect x="6" y={32 - (s.width / 2)} width="52" height={s.width} fill={s.bg} />
<rect x="6" y={32 - s.width / 2} width="52" height={s.width} fill={s.bg} />
{#if s.type === 'road'}
<line
@@ -148,9 +157,13 @@
<rect x="26" y={yTop} width="12" height="64" fill="white" />
<g stroke={s.stroke} stroke-width="2.5" fill="none" stroke-linecap="square">
<path d={`M 24 ${(32 - (s.width / 2)) - 5} L 25 ${(32 - (s.width / 2)) - 5} L 25 ${(32 - (s.width / 2)) + s.width + 5} L 24 ${(32 - (s.width / 2)) + s.width + 5}`} />
<path
d={`M 24 ${32 - s.width / 2 - 5} L 25 ${32 - s.width / 2 - 5} L 25 ${32 - s.width / 2 + s.width + 5} L 24 ${32 - s.width / 2 + s.width + 5}`}
/>
<path d={`M 40 ${(32 - (s.width / 2)) - 5} L 39 ${(32 - (s.width / 2)) - 5} L 39 ${(32 - (s.width / 2)) + s.width + 5} L 40 ${(32 - (s.width / 2)) + s.width + 5}`} />
<path
d={`M 40 ${32 - s.width / 2 - 5} L 39 ${32 - s.width / 2 - 5} L 39 ${32 - s.width / 2 + s.width + 5} L 40 ${32 - s.width / 2 + s.width + 5}`}
/>
</g>
<BaseTrack {activeElec} height={64} />

View File

@@ -18,7 +18,7 @@
};
const getPath = (side: 'left' | 'right') => {
const yStart = visualUp ? 64 : 0;
const yStart = visualUp ? 64 : 8;
const yEnd = visualUp ? 8 : 56;
const xEnd = side === 'right' ? 56 : 8;
return `M 32 ${yStart} Q 32 32 ${xEnd} ${yEnd}`;
@@ -34,7 +34,7 @@
</script>
<svg viewBox="0 0 64 64" width="64" height="64" class="junction">
{#each paths as d}
{#each paths as d (d)}
<path {d} fill="none" stroke={branchColour} stroke-width="5" stroke-linecap="round" />
{/each}
<BaseTrack {activeElec} height={64} />

View File

@@ -2,11 +2,12 @@
export let feature: {
routeName: string;
routeId: string;
entryPoint: string;
};
</script>
<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="header-row">
<span class="sub-text">Go to</span>

View File

@@ -1,11 +1,18 @@
<script lang="ts">
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 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>
<!--
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">
<BaseTrack {activeElec} height={64} />
</svg>

View File

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

View File

@@ -1,11 +1,14 @@
<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();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<link rel="apple-touch-icon" href={appleLogo} />
</svelte:head>
{@render children()}

View File

@@ -1,283 +1,315 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
import logo from '$lib/assets/round-logo-text.svg';
import type { PageData } from './$types';
import { resolve } from '$app/paths';
let searchTerm = '';
let { data }: { data: PageData } = $props();
let searchTerm = $state('');
const formatDate = (dateVal: string | null) => {
if (!dateVal) return '---';
const d = new Date(dateVal);
return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: '2-digit' });
};
let filteredMaps = $derived(
data.maps.filter((m) => {
const term = searchTerm.toLowerCase();
$: filteredMaps = data.maps.filter(m =>
m.routeId.toString().includes(searchTerm) ||
m.routeStart.toLowerCase().includes(searchTerm.toLowerCase()) ||
m.routeEnd.toLowerCase().includes(searchTerm.toLowerCase())
);
if (m.routeId.toString().includes(term)) return true;
const isVerifiedRecently = (dateVal: string | null) => {
if (!dateVal) return 'draft';
const checkedDate = new Date(dateVal).getTime();
const oneYearAgo = Date.now() - (365 * 24 * 60 * 60 * 1000);
return checkedDate > oneYearAgo ? 'verified' : 'stale';
};
return m.contents.some((location) => location.toLowerCase().includes(term));
})
);
const vibrate = (pattern: number | number[] = 10) => {
if (typeof window !== 'undefined' && window.navigator.vibrate) {
window.navigator.vibrate(pattern);
}
};
const isVerifiedRecently = (dateVal: string | null) => {
if (!dateVal) return 'draft';
const checkedDate = new Date(dateVal).getTime();
const oneYearAgo = Date.now() - 365 * 24 * 60 * 60 * 1000;
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>
<div class="page-wrapper">
<header class="main-header">
<div class="brand-container">
<img src="https://owlboard.info/images/logo/wide_logo.svg" alt="OwlBoard Logo" class="main-logo" />
<div class="brand-text">
<h1>Routes</h1>
<p class="sub-brand">by OwlBoard</p>
</div>
<div class="search-container">
<input
type="text"
bind:value={searchTerm}
placeholder="Search index..."
class="search-input"
/>
</div>
</header>
<header class="main-header">
<div class="brand-container">
<img src={logo} alt="OwlBoard Logo" class="main-logo" />
</div>
</header>
<div class="list-container">
{#each filteredMaps as map}
<a href="/map/{map.routeId.toString().padStart(4, '0')}" class="card">
<div class="card-top">
<span class="route-id">{map.routeId.toString().padStart(4, '0')}</span>
<span class="status-badge {isVerifiedRecently(map.checked)}">
{#if isVerifiedRecently(map.checked) === 'verified'}
Reviewed
{:else if isVerifiedRecently(map.checked) === 'stale'}
Needs review
{:else}
Draft
{/if}
</span>
</div>
<div class="card-body">
<div class="location origin">{map.routeStart}</div>
<div class="path-arrow"></div>
<div class="location destination">{map.routeEnd}</div>
</div>
<div class="list-container">
<a href="https://owlboard.info" class="button-link">Go to OwlBoard Live Departures & PIS</a>
<div class="card-footer">
<span>Created: {formatDate(map.created)}</span>
{#if map.checked}
<span>• Checked: {formatDate(map.checked)}</span>
{/if}
</div>
</a>
{:else}
<div class="empty-state">No maps found.</div>
{/each}
</div>
<input type="text" bind:value={searchTerm} placeholder="Search" class="search-input" />
{#each filteredMaps as map (map.routeId)}
<a
href={resolve(`/map/${map.routeId.toString().padStart(4, '0')}`)}
class="card"
onclick={() => vibrate(10)}
>
<div class="card-top">
<span class="route-id">{map.routeId.toString().padStart(4, '0')}</span>
<span class="status-badge {isVerifiedRecently(map.checked)}">
{#if isVerifiedRecently(map.checked) === 'verified'}
Reviewed
{:else if isVerifiedRecently(map.checked) === 'stale'}
Needs review
{:else}
Draft
{/if}
</span>
</div>
<div class="card-body">
<div class="location origin">{map.routeStart}</div>
<div class="path-arrow">to</div>
<div class="location destination">{map.routeEnd}</div>
</div>
<div class="card-footer">
<span>Updated: {formatDate(map.updated)}</span>
{#if map.checked}
<span>• Checked: {formatDate(map.checked)}</span>
{/if}
</div>
</a>
{:else}
<div class="empty-state">No maps found.</div>
{/each}
</div>
</div>
<style>
/* Mobile-First Base Styles */
:global(body) {
margin: 0;
background-color: #f8fafc;
color: #0f172a;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Mobile-First Base Styles */
:global(body) {
margin: 0;
background-color: #404c55;
background-image: radial-gradient(#2b343c, #404c55);
color: #0f172a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.page-wrapper {
padding: 1rem;
max-width: 800px;
margin: 0 auto;
}
.page-wrapper {
padding: 1rem;
max-width: 800px;
margin: 0 auto;
}
.main-header {
margin-bottom: 1.5rem;
}
.main-header {
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 {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.brand-container {
padding: 15px;
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
gap: 1rem;
}
.main-logo {
/* Adjust height to fit your logo's aspect ratio */
height: 48px;
width: auto;
display: block;
}
.main-logo {
height: 52px;
width: auto;
display: block;
}
.brand-text h1 {
font-size: 1.5rem;
font-weight: 900;
margin: 0;
line-height: 1;
letter-spacing: -0.05em;
font-style: italic;
}
.search-input {
width: 100%;
max-width: 500px;
font-family: 'urwgothic';
margin: auto;
height: 40px;
padding: 0.8rem 1rem;
text-align: center;
border-radius: 30px;
border: none;
font-size: 1rem;
transition: all 0.3s ease;
box-sizing: border-box; /* Ensures padding doesn't break width */
outline: none;
}
.sub-brand {
font-size: 0.65rem;
font-weight: 800;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.1em;
margin: 0.2rem 0 0 0;
}
.search-input:hover {
box-shadow: rgba(0, 0, 0, 0.46);
}
/* Desktop layout adjustment */
@media (min-width: 640px) {
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
}
.list-container {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 90px 1rem 1rem 1rem;
isolation: isolate;
z-index: 1;
}
.brand-container {
margin-bottom: 0;
}
@media (max-width: 600px) {
.list-container {
padding-top: 90px;
gap: 0.75rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.main-logo {
height: 56px;
}
}
.card {
padding: 10px 12px;
margin: 0;
border-radius: 4px;
}
.search-input {
width: 100%;
padding: 0.8rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 1rem;
box-sizing: border-box; /* Ensures padding doesn't break width */
outline: none;
}
.location {
font-size: 1rem;
margin-bottom: 0.1rem;
font-weight: 500;
}
.search-input:focus {
border-color: #3b82f6;
}
.path-arrow {
color: #fff;
font-size: 0.8rem;
margin: 0.1rem 0;
line-height: 0.75;
}
/* Card Layout (Mobile) */
.list-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.card-footer {
font-size: 0.6rem;
line-height: 0.8rem;
padding-bottom: 2px;
}
}
.card {
display: block;
background: white;
padding: 1.25rem;
border-radius: 16px;
text-decoration: none;
color: inherit;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
transition: transform 0.1s ease;
}
.card {
display: block;
background: #3c6f79;
padding: 1.25rem;
border-radius: 35px;
text-decoration: none;
color: #4fd1d1;
text-shadow: 2px 1px 10px rgba(0, 0, 0, 0.29);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.482);
transition: transform 0.1s ease;
cursor: pointer;
}
.card:active {
transform: scale(0.98); /* Tactile feedback for mobile */
}
.card:hover {
transform: scale(1.01);
z-index: 5;
}
.card-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.card:active {
transform: scale(0.99); /* Tactile feedback for mobile */
}
.route-id {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-weight: 800;
color: #3b82f6;
background: #eff6ff;
padding: 0.2rem 0.5rem;
border-radius: 6px;
}
.card-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.card-body {
margin-bottom: 1rem;
}
.route-id {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-weight: 800;
color: #fff;
background: #2d2d2d;
text-shadow: none;
padding: 0.2rem 0.5rem;
border-radius: 6px;
}
.location {
font-size: 1.125rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.card-body {
margin-bottom: 1rem;
}
.path-arrow {
color: #cbd5e1;
font-size: 0.9rem;
margin: 0.2rem 0;
}
.location {
font-family: 'urwgothic';
font-size: 1.23rem;
font-weight: 700;
}
.card-footer {
font-size: 0.7rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
border-top: 1px solid #f1f5f9;
padding-top: 0.75rem;
}
.path-arrow {
color: #fff;
font-family: 'urwgothic';
font-size: 0.9rem;
margin: 0.2rem 0;
}
/* Update your existing badge styles */
.status-badge {
font-size: 0.65rem;
font-weight: 800;
text-transform: uppercase;
padding: 0.3rem 0.6rem;
border-radius: 999px;
white-space: nowrap; /* Prevents label snapping on small screens */
}
.card-footer {
font-size: 0.7rem;
color: #e2ebeb;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
padding-top: 0;
}
.verified {
background: #dcfce7;
color: #166534;
}
.status-badge {
font-size: 0.65rem;
font-weight: 800;
text-shadow: none;
text-transform: uppercase;
padding: 0.3rem 0.6rem;
border-radius: 999px;
white-space: nowrap;
}
.stale {
background: #fef3c7; /* Amber/Yellow background */
color: #92400e; /* Dark brown/gold text */
border: 1px solid #fcd34d;
}
.verified {
background: #dcfce7;
color: #166534;
}
.draft {
background: #f1f5f9;
color: #475569;
}
.stale {
background: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
}
/* Desktop Adjustments */
@media (min-width: 640px) {
.page-wrapper {
padding: 3rem 1.5rem;
}
.draft {
background: #f1f5f9;
color: #475569;
}
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.button-link {
text-decoration: none;
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;
}
.search-input {
width: 300px;
}
.path-arrow {
display: inline-block;
margin: 0 0.5rem;
transform: rotate(-90deg); /* Turn down arrow into right arrow */
}
.card-body {
display: flex;
align-items: center;
}
}
</style>
.button-link:hover {
background: #2b2b2b;
}
</style>

View File

@@ -1,17 +1,26 @@
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 }) => {
const response = await fetch('map-index.json');
const response = await fetch('map-index.json');
if (!response.ok) {
return { maps: [] };
}
if (!response.ok) {
return { maps: [] };
}
const maps = await response.json();
const maps = await response.json();
return {
maps: maps.sort((a: any, b: any) => {
return Number(a.routeId) - Number(b.routeId);
})
};
};
return {
maps: maps.sort((a: any, b: any) => {
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,6 +2,10 @@
import RouteRow from '$lib/components/RouteRow.svelte';
import RouteEndLink from '$lib/components/mapIcons/RouteEndLink.svelte';
import { slide } from 'svelte/transition';
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[]
export let data;
@@ -10,6 +14,7 @@
let visibleTypes = {
station: true,
minorBridge: false,
bridge: true,
crossovers: true,
loop: true,
@@ -58,39 +63,45 @@
</script>
<div class="map-layout">
<header class="top-nav">
<div class="nav-cluster">
<a href="/" class="home-link" title="Back to Index">
<span class="home-icon"></span>
</a>
<div class="route-stack">
{#if data?.route}
<h1 class="primary-station">
{reversed ? data.route.routeEnd : data.route.routeStart}
</h1>
<span class="secondary-station">
to {reversed ? data.route.routeStart : data.route.routeEnd}
</span>
{/if}
</div>
</div>
<header class="top-nav">
<div class="nav-cluster">
<a href={resolve('/')} aria-label="Home" class="home-link" title="Back to Index">
<img src={logo} alt="OwlBoard Logo" class="nav-logo" />
</a>
<div class="quick-actions">
<button class="icon-btn" on:click={() => (reversed = !reversed)}>
{reversed ? 'UP' : 'DN'}
</button>
<button class="icon-btn" on:click={() => (showFilters = !showFilters)}> Settings </button>
</div>
</header>
<div class="route-stack">
{#if data?.route}
<h1 class="primary-station">
{reversed ? data.route.routeEnd : data.route.routeStart}
</h1>
<span class="secondary-station">
to {reversed ? data.route.routeStart : data.route.routeEnd}
</span>
{/if}
</div>
</div>
<div class="quick-actions">
<button class="icon-btn" onclick={() => (reversed = !reversed)}>
<IconArrowsExchange />
</button>
<button class="icon-btn" onclick={() => (showFilters = !showFilters)}> <IconSettings /> </button>
</div>
</header>
{#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="drawer-header">
<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
viewBox="0 0 24 24"
width="20"
@@ -106,11 +117,11 @@
<div class="drawer-content">
<div class="filter-flex">
{#each Object.keys(visibleTypes) as type}
{#each Object.keys(visibleTypes) as type (type)}
<button
class="filter-chip"
class:active={visibleTypes[type]}
on:click={() => toggleFilter(type)}
onclick={() => toggleFilter(type)}
>
{formatLabel(type)}
</button>
@@ -143,112 +154,101 @@
padding-top: 0;
}
.top-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: #0f172a;
color: white;
gap: 1rem;
}
.top-nav {
position: fixed;
width: 100%;
box-sizing: border-box;
top: 0;
height: 80px;
left: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0;
background: #3c6f79;
color: #e1ebeb;
gap: 1rem;
z-index: 10000;
}
.nav-cluster {
display: flex;
align-items: center;
gap: 1rem;
min-width: 0; /* Prevents flex children from overflowing */
}
.nav-cluster {
display: flex;
align-items: center;
gap: 1rem;
min-width: 0; /* Prevents flex children from overflowing */
}
.home-link {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
color: #94a3b8;
text-decoration: none;
font-size: 1.2rem;
flex-shrink: 0; /* Keeps button from squishing on mobile */
}
.home-link {
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
padding-left: 0;
margin-left: 15px;
flex-shrink: 0;
transition: all 0.3s ease;
}
.home-link:hover {
background: #334155;
color: white;
border-color: #475569;
}
@media (max-width: 350px) {
.home-link {
width: 42px;
height: 42px;
}
}
.route-stack {
display: flex;
flex-direction: column;
min-width: 0;
}
.home-link:hover {
transform: translateY(-1px) scale(1.05);
filter: brightness(1.1);
}
.primary-station {
font-size: 1rem;
font-weight: 800;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.home-link:active {
transform: scale(0.95);
}
.secondary-station {
font-size: 0.7rem;
color: #94a3b8;
text-transform: uppercase;
font-weight: 600;
}
.route-stack {
display: flex;
flex-direction: column;
min-width: 0;
}
.quick-actions {
display: flex;
gap: 0.5rem;
}
.primary-station {
font-size: 1rem;
font-weight: 800;
margin: 0;
white-space: nowrap;
color: #cce9e9;
overflow: hidden;
text-overflow: ellipsis;
}
.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;
}
.secondary-station {
font-size: 0.7rem;
color: #cce9e9;
text-transform: uppercase;
font-weight: 600;
}
.quick-actions {
display: flex;
gap: 0.5rem;
margin-right: 15px;
}
.map-spine {
padding-top: 72px;
}
.secondary-station {
font-size: 0.85rem;
font-weight: 500;
color: #64748b;
letter-spacing: 0.025em;
margin-top: 2px;
}
@media (min-width: 768px) {
.primary-station {
@media (min-width: 536px) {
.primary-station {
font-size: 1.5rem;
}
.secondary-station {
font-size: 1rem;
}
}
.top-nav {
padding: 0 2rem;
height: 90px;
}
.map-spine {
padding-top: 90px;
}
@media (min-width: 768px) {
h1 {
font-size: 1.5rem;
letter-spacing: -0.03em;
@@ -267,6 +267,7 @@
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 150;
transition: all 0.3s ease;
}
.filter-drawer {
@@ -274,7 +275,7 @@
bottom: 0;
left: 0;
right: 0;
background: white;
background: #3c6f79;
z-index: 200;
border-radius: 20px 20px 0 0;
box-shadow: 0 -8px 20px rgba(0, 0, 0, 0.15);
@@ -287,17 +288,17 @@
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem 0.75rem;
border-bottom: 1px solid #f1f5f9;
border-bottom: 1px solid #e1ebeb;
}
.drawer-header h3 {
margin: 0;
font-size: 1rem;
color: #1e293b;
color: #e1ebeb;
}
.close-icon {
background: #f1f5f9;
background: #404c55;
border: none;
border-radius: 50%;
width: 32px;
@@ -306,7 +307,12 @@
align-items: center;
justify-content: center;
cursor: pointer;
color: #64748b;
color: #e1ebeb;
transition: all 0.3s ease;
}
.close-icon:hover {
background: #2d2d2d;
}
.drawer-content {
@@ -323,14 +329,14 @@
padding: 0.5rem 1rem;
border-radius: 999px;
border: 1px solid #e2e8f0;
background: #f8fafc;
background: #ff6060;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.filter-chip.active {
background: #1e293b;
background: #00725b;
color: white;
border-color: #1e293b;
}
@@ -345,23 +351,28 @@
display: inline-flex;
align-items: center;
gap: 0.4rem;
border: none;
padding: 0.5rem 0.8rem;
border-radius: 12px;
border: 1px solid #e2e8f0;
background: #f8fafc;
color: #475569;
background: #404c55;
color: #e1ebeb;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
text-transform: uppercase;
letter-spacing: 0.02em;
transition: all 0.3s ease;
}
@media (max-width: 350px) {
.icon-btn {
padding: 0.3rem 0.3rem;
}
}
.icon-btn:hover {
background: #f1f5f9;
border-color: #cbd5e1;
color: #1e293b;
background: #2d2d2d;
}
.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.

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
routeStart: Reading West Jn
routeStart: Reading
routeEnd: Bristol TM
routeId: 0002
created: 2026-02-04
updated: 2026-02-04
checked:
signallerStart: TVSC Reading WS
signallerEnd: TVSC Temple Meads WS
@@ -11,8 +11,48 @@ elecStart:
elecEnd:
elec: none
routeDetail:
- type: crossovers
name: Scours Lane Junction
description: Line diverges
miles: 38
chains: 90
- type: continues
routeName: Paddington - Reading
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
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

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';
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit()],
build: {
assetsInlineLimit: 0,
cssCodeSplit: true,
sourcemap: false,
target: 'esnext',
modulePreload: {
polyfill: false
}
}
});