A calm, Montessori-inspired toddler game platform, built as a mobile-first Progressive Web App. Designed for a 3-year-old who cannot read yet - every interaction is understood through colour, shape, motion and exploration.
It is intentionally not a dopamine casino: no scores, no "game over", no loud celebrations, no ads, no tracking, no accounts.
Badge, repository and image references assume the GitHub repository
techlotse/TL-Gamesand the Docker Hub imagetechlotse/tl-games. Docker Hub repository names must be lowercase, so the image istl-gameseven though the GitHub repository isTL-Games. Adjust these if yours differ.
- App shell - installable PWA, offline after first load, iPhone-first.
- Home screen - seven large, readable-free game tiles.
- Seven games:
- Build Garage (
Werkstatt) - assembly: build ten vehicle types in turn, part by part - from a little car to a digger. - Flower Garden (
Blumengarten) - colour match: match each flower to the plant pot of the same colour. - Shape Sorting (
Formen) - shape match: sort each block into its hole. - Race (
Rennen) - steering: hold and move the car to drive around the obstacles and collect ten cheerful items to fill its smile. - Colouring (
Malen) - colouring: pick a colour and colour the picture in - tap to fill a region, or sweep the brush to paint freely. - Find-an-item (
Suchen) - searching: find the item shown in the frame among the others scattered across the scene. - Bagger (
Bagger) - platforming: run and hop a friendly excavator through a construction site, digging up gems on the way home.
- Build Garage (
- Difficulty progression - every game starts gentle and ramps up the longer the child plays, then resets to easy at the home screen.
- Light & dark themes (calm, muted - never neon), saved on-device.
- Parent area behind a hold-to-enter gate: theme, accessibility, progress.
- Hand-drawn SVG artwork - no external image assets.
React 18 - Vite 6 - TypeScript (strict) - Tailwind CSS 3 - shadcn/ui-style components - Lucide icons - Framer Motion (used sparingly) - Zustand - vite-plugin-pwa - Inter (bundled locally for offline use).
Requirements: Node 18+ (developed on Node 22).
npm install # install dependencies
npm run dev # start the dev server (http://localhost:5173)
npm run build # type-check + production build into dist/
npm run preview # serve the production build locallynpm run build, then deploydist/to any static host - or run the container (below). It must be served over HTTPS for the service worker.- Open the site in Safari (iPhone) or Chrome (Android).
- iPhone: Share -> Add to Home Screen. Android: menu -> Install app.
- Launch it from the home screen - it runs full-screen and works offline.
A published image is available on Docker Hub as techlotse/tl-games
(multi-arch: linux/amd64 and linux/arm64).
docker run --rm -p 8080:8080 techlotse/tl-games:latest
# then open http://localhost:8080Or with Compose - recommended, as it adds health checks and runtime hardening (read-only filesystem, dropped capabilities, no privilege escalation):
cp .env.example .env # adjust IMAGE_TAG / HOST_PORT if needed
docker compose up -dAvailable image tags:
latest- the most recent tagged release0.0.8,0.0- specific semantic versionsedge- the latest build frommain
Build the image yourself with docker build -t techlotse/tl-games:dev .
This project follows Semantic Versioning. The version in
package.json is the single source of truth - it is injected into the build
and shown in the in-app parent area; VERSION mirrors it and CHANGELOG.md
records every release.
Cut a release by pushing a Git tag:
git tag v0.0.8
git push origin v0.0.8That triggers the publish workflow, which builds the image, scans it with Trivy, pushes the versioned tags to Docker Hub, and creates a GitHub Release.
- CI (
.github/workflows/ci.yml) - type-checks and builds on every push and pull request;npm auditruns here. - Publish (
.github/workflows/publish.yml) - on push tomain, onv*tags, and monthly: builds the image, fails if Trivy finds fixable CRITICAL/HIGH CVEs, then builds multi-arch and pushes to Docker Hub. - Dependabot keeps npm packages, GitHub Actions and the Docker base image up to date.
docs/architecture.md- how the app is builtdocs/security.md- security & privacy posturedocs/roadmap.md- what is planned nextCLAUDE.md- guide for continuing developmentCHANGELOG.md- release history
src/
app/ App shell + a small store-driven router
components/
ui/ shadcn/ui-style primitives
layout/ AppShell, GameScreen frames
toddler/ GameTile, RoundButton, ParentGate, CompletionOverlay, ...
games/
shared/ Matching engine (useMatchingGame, MatchingBoard) + DraggablePiece
build-garage/ Assembly game - build a vehicle from parts
flower-garden/ Colour-matching game
shape-sorting/ Shape-matching game
race/ Steering game with a gentle game loop
colouring/ Colouring game - fill regions or finger-paint
find-item/ Find-an-item searching game
dig/ Bagger - a side-scrolling platform game
screens/ HomeScreen, ParentScreen
store/ Zustand state + localStorage persistence
theme/ tokens.css (CSS variables) + ThemeProvider
pwa/ manifest, service worker, registration
i18n/ German strings
lib/ Framework-agnostic helpers
Each game folder is self-contained: data.ts (content), logic.ts (rules),
art.tsx (modular inline SVG), the board where one is needed, and the screen
component.
- No reading required. Guidance is visual: glowing targets, bouncing arrows, happy sparkles. German text is short and for the parent.
- Huge, obvious targets. Every touch target is at least 64px.
- Forgiving by design. Wrong choices drift gently back; a part dropped on the car snaps to the nearest spot; a bump in Race only pauses the car. There is no failure state.
- Touch-first. Drag-and-drop, tap-to-place and hold-to-steer - whichever suits the game and small motor skills.
- Calm. Animations are gentle (180-220ms): no flashing, no confetti.
- Few choices at once, growing slowly as the child keeps playing.
- Respects the OS reduce-motion setting; a parent toggle forces it too.
- High-contrast mode and theme choice in the parent area.
- Visible focus rings, ARIA roles, keyboard-activatable pieces.
- iPhone safe-area insets handled throughout (
viewport-fit=cover).
Reachable from the small corner button on the home screen, then a hold-the-circle gate - easy for an adult, unlikely for a toddler. Inside: light/dark theme, high-contrast, reduced-motion, per-game progress and a progress reset. No accounts, no cloud - everything stays on the device.
No backend, no login, no ads, no analytics, no tracking, no external API calls.
The only stored data is a small set of preferences in localStorage. The
container runs as a non-root user with a read-only root filesystem, and every
response carries a strict Content-Security-Policy. See
docs/security.md for the full posture.
The architecture anticipates a later move to Capacitor / Expo / React Native / Tauri:
- Game rules live in framework-light hooks (each game's
logic.ts) with no DOM dependencies. - All browser-only calls (haptics, install detection) are isolated in
src/lib/platform.ts- the single file a native build would re-implement. - Navigation is a small store-driven router, not URL-coupled.
- Artwork is modular inline SVG, so it travels with the components.
The home screen supports up to 6 tiles. A new game needs a src/games/<id>/
folder (data.ts, logic.ts, art.tsx, a board if needed, screen component),
registration in src/app/routes.tsx, a GameId in the store, a tile-tone
token, and a German label. See CLAUDE.md for the step-by-step.
- The interface is German only; add languages alongside
src/i18n/de.ts. - The app name (
Spielgarten) lives insrc/i18n/de.tsandindex.html. - App icons are in
public/icons/; regenerate them if you change the artwork. - License: private / all rights reserved (
UNLICENSED).