A guided thumb-ergotherapy web app. It walks you through a five-exercise routine — one exercise per screen, with hold timers, repetition counters, and figure photos — and keeps track of how many sessions you have finished today against a daily goal of 3–4.
It is a plain static site: no build step, no dependencies, no backend. All
state lives in the browser's localStorage.
A program is a named routine: an ordered set of exercises plus the daily session goal they should be done against. There is one today — Program 2, five exercises, three to four sessions a day — and the app selects it automatically.
The structure is there so more can be added. A program is one entry in the
PROGRAMS array in app.js:
{
id: "program-2",
name: "Program 2",
goalMin: 3,
goalMax: 4,
exercises: PROGRAM_2_EXERCISES,
}Add a second entry and a picker appears on the home screen on its own; the home headline, the session dots, and the daily-goal copy all read from the active program, and each program keeps its own session count for the day.
| # | Exercise | Type | Hold | Reps |
|---|---|---|---|---|
| 1 | Thumb into the palm | timed hold | 10 s | 5–10 |
| 2 | Thumb to each finger | tap sequence | — | 10–15 |
| 3 | Thumb in an L | timed hold | 10 s | 5–10 |
| 4 | Clothespin pinch | timed hold | 3–5 s (selectable) | 10–15 |
| 5 | Squeeze a ball | timed hold | 3–5 s (selectable) | 10–15 |
Each exercise also shows the original French instruction it was transcribed from, so it can be checked against the therapist's sheet.
- Home shows today's session count as dots, plus the sound and theme toggles, and a Start a session button.
- Timed holds run a countdown ring. When it reaches zero the rep is counted, a chime and a short vibration fire, and a brief rest (1.6 s) runs before the next rep is armed.
- The tap sequence (exercise 2) advances one step per tap — index, middle, ring, pinky, then a slide down the pinky. The full pass counts as one rep.
- You can move on once the exercise's minimum reps are reached; hitting the maximum marks it complete automatically. Reaching the max on the last exercise ends the session.
- Done increments today's counter and tells you where you stand against the 3–4 goal.
- Daily tracker — session count keyed to the calendar date, so it resets on its own each day, and stored per program.
- Sound and haptics — a short Web Audio chime per rep plus
navigator.vibratewhere supported. Toggleable, and the choice is persisted. - Theme switch —
Autofollows the system preference,PaleandDarkoverride it. The browser chrome colour follows via thetheme-colormeta tag. - Installable — a web app manifest with
display: standalone, an SVG icon, and iOS web-app meta tags, so it can be added to a phone home screen. Note there is no service worker yet, so it is not usable fully offline.
app.js is loaded as an ES module and the manifest is fetched relatively, so
opening index.html as a file:// URL will not work — it needs to be served
over HTTP. Any static file server will do; from the repository root:
# Python (bundled with macOS and most Linux distributions)
python3 -m http.server 8000# Node, no install needed
npx serve .# PHP
php -S localhost:8000Then open http://localhost:8000/.
The app is designed for a phone screen, and installing it or testing vibration is easier on the real device. Serve on all interfaces and browse to your machine's LAN address:
python3 -m http.server 8000 --bind 0.0.0.0
# then visit http://<your-computer-ip>:8000/ from the phoneNote that installing as a PWA generally requires a secure context —
localhost counts, a plain-HTTP LAN address does not — so for the full
install experience use a tunnel that terminates TLS (ngrok http 8000, cloudflared tunnel --url http://localhost:8000, or similar).
Because it is entirely static, the repository can be published as-is to GitHub Pages, Netlify, Cloudflare Pages, or any static host — no build command, and the output directory is the repository root.
index.html markup shell, meta tags, font and asset links
app.js exercise data, session state machine, timers, rendering
styles.css design tokens (light + dark) and all component styles
manifest.json PWA manifest
favicon.svg app icon
figures/ photo for each exercise
app.js re-renders by replacing #app's inner HTML on each state change; the
countdown ring is the one exception, patched in place on a 50 ms interval so
the animation stays smooth.
Program 2's exercises are the PROGRAM_2_EXERCISES array at the top of
app.js. Each entry carries its title, the original French original line, a
detail instruction, hold seconds (or null), repsMin/repsMax, a type
of "hold" or "sequence", and a figure key into the FIGURES map. Adding an
exercise means adding an entry and a matching figure — no other changes needed.
Stored state lives under the thumbwise-v1 key: the date, the selected
program, sound and theme choices, and today's session counts keyed by program
id. Older state that predates programs held a single session number; it is
migrated to Program 2's count on load.
Both styles.css and app.js are linked with a ?v=N cache-busting query
parameter in index.html; bump it when deploying a change so returning users
do not get a stale cached copy.