Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 112 additions & 12 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
main {
--page-bg: #ffffff;
--tagline-color: #999999;
--steam-color: #c4c4c4;
--control-border: #eeeeee;
--control-border-hover: #cccccc;
--control-color: #888888;
--counter-color: #aaaaaa;

position: relative;
display: block;
min-height: 100svh;
Expand All @@ -7,6 +15,86 @@ main {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
background:
linear-gradient(var(--page-bg), var(--page-bg));
color: var(--control-color);
transition: background-color 240ms ease, color 240ms ease;
}

main.night {
--page-bg: #0d1222;
--tagline-color: #aeb9d4;
--steam-color: #d6ddff;
--control-border: #2d395c;
--control-border-hover: #7484b8;
--control-color: #d8e1ff;
--counter-color: #8c9abf;

background:
radial-gradient(circle at 18% 16%, rgba(89, 111, 171, 0.24), transparent 28vw),
linear-gradient(180deg, #0b1020 0%, #111a32 64%, #19253f 100%);
}

.night-details {
position: fixed;
inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 300ms ease;
z-index: 0;
}

main.night .night-details {
opacity: 1;
}

.moon,
.star {
position: absolute;
color: #f7e8a6;
text-shadow: 0 0 14px rgba(247, 232, 166, 0.45);
}

.moon {
top: 8vh;
right: 11vw;
font-size: clamp(44px, 7vw, 76px);
transform: rotate(-12deg);
}

.star {
font-size: 18px;
animation: twinkle 2600ms ease-in-out infinite;
}

.star-a {
top: 15vh;
left: 14vw;
}

.star-b {
top: 27vh;
left: 76vw;
animation-delay: 500ms;
}

.star-c {
top: 54vh;
left: 22vw;
font-size: 14px;
animation-delay: 900ms;
}

.star-d {
top: 42vh;
left: 64vw;
font-size: 26px;
animation-delay: 1200ms;
}

@keyframes twinkle {
0%, 100% { opacity: 0.5; transform: scale(0.9); }
50% { opacity: 1; transform: scale(1.08); }
}

.hero {
Expand Down Expand Up @@ -46,7 +134,7 @@ main {
.tagline {
font-size: 13px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: #999;
color: var(--tagline-color);
letter-spacing: 0.04em;
}

Expand Down Expand Up @@ -85,24 +173,29 @@ main {
.steam {
position: fixed;
font-size: 22px;
color: #c4c4c4;
color: var(--steam-color);
pointer-events: none;
will-change: transform, opacity;
animation: puff 1400ms ease-out forwards;
z-index: 0;
}

main.night .steam {
text-shadow: 0 0 12px rgba(214, 221, 255, 0.42);
}

@keyframes puff {
0% { opacity: 0.7; transform: translate(0, 0) scale(0.8); }
100% { opacity: 0; transform: translate(0, -40px) scale(1.4); }
}

.mute-toggle {
.mute-toggle,
.theme-toggle {
position: fixed;
top: 16px;
right: 16px;
background: transparent;
border: 1px solid #eee;
background: color-mix(in srgb, var(--page-bg) 82%, transparent);
border: 1px solid var(--control-border);
border-radius: 999px;
width: 36px;
height: 36px;
Expand All @@ -111,18 +204,24 @@ main {
justify-content: center;
font-size: 16px;
cursor: pointer;
color: #888;
color: var(--control-color);
z-index: 10;
transition: border-color 150ms ease, transform 150ms ease;
transition: border-color 150ms ease, transform 150ms ease, background-color 150ms ease;
}

.theme-toggle {
top: 60px;
}

.mute-toggle:hover {
border-color: #ccc;
.mute-toggle:hover,
.theme-toggle:hover {
border-color: var(--control-border-hover);
transform: scale(1.05);
}

.mute-toggle:focus-visible {
outline: 2px solid #888;
.mute-toggle:focus-visible,
.theme-toggle:focus-visible {
outline: 2px solid var(--control-color);
outline-offset: 2px;
}

Expand All @@ -132,7 +231,7 @@ main {
left: 16px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
color: #aaa;
color: var(--counter-color);
letter-spacing: 0.02em;
pointer-events: none;
z-index: 10;
Expand All @@ -141,6 +240,7 @@ main {
@media (prefers-reduced-motion: reduce) {
.train-emoji:hover { animation: none; }
.steam { display: none; }
.star { animation: none; }

.train.ltr,
.train.rtl {
Expand Down
31 changes: 30 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import './App.css'

type Direction = 'ltr' | 'rtl'
type Theme = 'day' | 'night'

type Train = {
id: number
Expand Down Expand Up @@ -39,6 +40,10 @@ function App() {
if (typeof localStorage === 'undefined') return false
return localStorage.getItem('wtc:muted') === '1'
})
const [theme, setTheme] = useState<Theme>(() => {
if (typeof localStorage === 'undefined') return 'day'
return localStorage.getItem('wtc:theme') === 'night' ? 'night' : 'day'
})
const [hasDispatched, setHasDispatched] = useState(false)

const nextIdRef = useRef(1)
Expand All @@ -53,6 +58,10 @@ function App() {
localStorage.setItem('wtc:muted', muted ? '1' : '0')
}, [muted])

useEffect(() => {
localStorage.setItem('wtc:theme', theme)
}, [theme])

useEffect(() => {
if (chooRef.current) return
const choo = new Audio('/choo-choo.mp3')
Expand Down Expand Up @@ -141,7 +150,15 @@ function App() {
}, [dispatch])

return (
<main onClick={dispatch}>
<main className={theme} onClick={dispatch}>
<div className="night-details" aria-hidden="true">
<span className="moon">☾</span>
<span className="star star-a">✦</span>
<span className="star star-b">✧</span>
<span className="star star-c">✦</span>
<span className="star star-d">·</span>
</div>

<div className={`hero${hasDispatched ? ' dispatched' : ''}`} aria-hidden={hasDispatched}>
<span className="train-emoji" role="img" aria-label="train">🚂</span>
<span className="tagline">click anywhere to dispatch a train</span>
Expand Down Expand Up @@ -184,6 +201,18 @@ function App() {
{muted ? '🔇' : '🔊'}
</button>

<button
className="theme-toggle"
onClick={(e) => {
e.stopPropagation()
setTheme((current) => current === 'night' ? 'day' : 'night')
}}
aria-label={theme === 'night' ? 'Switch to day mode' : 'Switch to night mode'}
aria-pressed={theme === 'night'}
>
{theme === 'night' ? '☀️' : '🌙'}
</button>

<div className="counter" aria-live="polite">
{count} {count === 1 ? 'train' : 'trains'} dispatched
</div>
Expand Down