diff --git a/package-lock.json b/package-lock.json index d7b34a9..d252ed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -266,29 +267,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -875,6 +853,7 @@ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -885,6 +864,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -944,6 +924,7 @@ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", @@ -1174,6 +1155,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1264,6 +1246,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1399,6 +1382,7 @@ "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2287,6 +2271,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2348,6 +2333,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2511,6 +2497,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2597,6 +2584,7 @@ "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -2721,6 +2709,7 @@ "integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/App.css b/src/App.css index 33a51c9..84a81b1 100644 --- a/src/App.css +++ b/src/App.css @@ -138,9 +138,94 @@ main { z-index: 10; } +.night-toggle { + position: fixed; + top: 16px; + right: 60px; + background: transparent; + border: 1px solid #eee; + border-radius: 999px; + width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 16px; + cursor: pointer; + color: #888; + z-index: 10; + transition: border-color 150ms ease, transform 150ms ease; +} + +.night-toggle:hover { + border-color: #ccc; + transform: scale(1.05); +} + +.night-toggle:focus-visible { + outline: 2px solid #888; + outline-offset: 2px; +} + +.night-sky { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; +} + +.moon { + position: fixed; + top: 6vh; + right: 8vw; + font-size: 56px; + line-height: 1; + filter: drop-shadow(0 0 12px rgba(255, 240, 200, 0.35)); + animation: moon-glow 4s ease-in-out infinite; +} + +.star { + position: fixed; + font-size: 14px; + color: #f4f1d8; + opacity: 0.85; + text-shadow: 0 0 6px rgba(244, 241, 216, 0.6); + animation: twinkle 3s ease-in-out infinite; +} + +.star:nth-of-type(2n) { animation-duration: 4.2s; animation-delay: 0.6s; } +.star:nth-of-type(3n) { animation-duration: 5s; animation-delay: 1.2s; } +.star:nth-of-type(4n) { animation-duration: 3.6s; animation-delay: 1.8s; } + +@keyframes twinkle { + 0%, 100% { opacity: 0.3; transform: scale(0.9); } + 50% { opacity: 1; transform: scale(1.1); } +} + +@keyframes moon-glow { + 0%, 100% { filter: drop-shadow(0 0 10px rgba(255, 240, 200, 0.3)); } + 50% { filter: drop-shadow(0 0 18px rgba(255, 240, 200, 0.55)); } +} + +body.night .tagline { color: #8d96b3; } +body.night .counter { color: #6a7596; } +body.night .steam { color: #d8def0; } + +body.night .mute-toggle, +body.night .night-toggle { + border-color: #2a3450; + color: #b8c0db; +} + +body.night .mute-toggle:hover, +body.night .night-toggle:hover { + border-color: #4a5778; +} + @media (prefers-reduced-motion: reduce) { .train-emoji:hover { animation: none; } .steam { display: none; } + .star, .moon { animation: none; } .train.ltr, .train.rtl { diff --git a/src/App.tsx b/src/App.tsx index e50fd7f..12dd41d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,10 @@ function App() { if (typeof localStorage === 'undefined') return false return localStorage.getItem('wtc:muted') === '1' }) + const [night, setNight] = useState(() => { + if (typeof localStorage === 'undefined') return false + return localStorage.getItem('wtc:night') === '1' + }) const [hasDispatched, setHasDispatched] = useState(false) const nextIdRef = useRef(1) @@ -53,6 +57,11 @@ function App() { localStorage.setItem('wtc:muted', muted ? '1' : '0') }, [muted]) + useEffect(() => { + localStorage.setItem('wtc:night', night ? '1' : '0') + document.body.classList.toggle('night', night) + }, [night]) + useEffect(() => { if (chooRef.current) return const choo = new Audio('/choo-choo.mp3') @@ -134,6 +143,8 @@ function App() { dispatch() } else if (e.key === 'm' || e.key === 'M') { setMuted((m) => !m) + } else if (e.key === 'n' || e.key === 'N') { + setNight((n) => !n) } } window.addEventListener('keydown', onKey) @@ -142,6 +153,20 @@ function App() { return (
+ {night && ( + + )} +
๐Ÿš‚ click anywhere to dispatch a train @@ -184,6 +209,18 @@ function App() { {muted ? '๐Ÿ”‡' : '๐Ÿ”Š'} + +
{count} {count === 1 ? 'train' : 'trains'} dispatched
diff --git a/src/index.css b/src/index.css index bece4d7..f80d6a1 100644 --- a/src/index.css +++ b/src/index.css @@ -1,6 +1,11 @@ body { margin: 0; background: #fff; + transition: background 600ms ease; +} + +body.night { + background: radial-gradient(ellipse at top, #1a2238 0%, #0a0e1a 70%); } #root {