Skip to content
Merged
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
19 changes: 19 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "web",
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "dev"],
"cwd": "apps/web",
"port": 5173
},
{
"name": "web-static",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "5188", "--directory", "apps/web/build"],
"cwd": ".",
"port": 5188
}
]
}
32 changes: 0 additions & 32 deletions .github/workflows/release.yml

This file was deleted.

44 changes: 26 additions & 18 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,33 @@ on:
jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [14.x, 16.x]
defaults:
run:
working-directory: apps/web

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: ${{ matrix.node-version }}
cache: yarn

- name: Build & test
run: |
yarn install --frozen-lockfile
npx playwright install
yarn build
yarn test
yarn dev &
yarn test:e2e
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile
working-directory: .

- name: Type check
run: bun run check

- name: Unit & component tests
run: bun run test

- name: Build
run: bun run build

- name: Install Playwright browser
run: bunx playwright install --with-deps chromium

- name: E2E tests
run: bun run test:e2e
60 changes: 52 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,54 @@
.DS_Store
# Dependencies
node_modules
/build
/.svelte-kit
/package
.vercel
.vercel_build_output
.pnp
.pnp.js

# Build outputs
dist
build
*.tsbuildinfo

# Generated files
apps/web/src/routeTree.gen.ts

# Environment variables
.env
.env.*
!.env.example
.env*.local

# IDEs and editors
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea
*.swp
*.swo
*~
.DS_Store

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Turbo
.turbo
.nx

# Better-T-Stack
.alchemy

# Testing
coverage
.nyc_output

# Misc
*.tgz
.cache
tmp
temp
3 changes: 3 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"ignorePatterns": []
}
11 changes: 11 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "unicorn", "oxc"],
"categories": {
"correctness": "error"
},
"rules": {},
"env": {
"builtin": true
}
}
56 changes: 56 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Learn Manoonchai

A browser typing trainer for the **Manoonchai** Thai keyboard layout. The app
translates physical key positions into Manoonchai characters itself, so a learner
can practice the layout without reconfiguring their operating system.

## Language

**Manoonchai layout**:
The Thai keyboard layout this app teaches. A mapping from physical key positions
(`KeyboardEvent.code`) to a base character and a shifted character.
_Avoid_: keymap (that is the on-screen widget, not the data).

**Lesson**:
A named unit of the curriculum, holding a pool of Thai words that introduce a
specific character or group of characters (e.g. "5. -่ (ไม้เอก)"). Lessons are
chosen freely; there is no locking or forced progression.
_Avoid_: level, chapter, exercise.

**Drill**:
One timed run the learner performs: a sequence of words sampled from the active
lesson's pool. The learner picks the length (e.g. 10 / 25 / 50 words). A drill
has a clear start (first keystroke) and end (last word committed).
_Avoid_: sentence, test, round, session.

**Word**:
A single Thai token within a drill, committed by pressing space. The learner's
input is compared against the target word to decide correctness.

**Keystroke**:
One Manoonchai character produced by the learner, recorded with its timestamp and
whether it matched the expected next character. The raw material for all stats.

**Raw WPM**:
Typing speed counting every character typed, correct or not, measured per one-second
window (chars ÷ 5 × 60). Spiky; reflects actual cadence.
_Avoid_: gross WPM.

**Net WPM**:
Speed after penalizing errors — the headline number shown at the end of a drill and
the smoothed line on the chart.
_Avoid_: adjusted WPM, real WPM.

**Accuracy**:
Share of keystrokes that matched the expected character, as a percentage over the
whole drill.

**Keymap**:
The on-screen rendering of the Manoonchai layout. Highlights and glows the next key
the learner should press.
_Avoid_: keyboard (ambiguous with the physical device).

**Next key**:
The character the learner is expected to type next, derived from the target word and
their input so far. Drives the keymap glow and the inline hint.
_Avoid_: nextchar.
61 changes: 61 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Product

## Register

product

## Users

Thai typists deliberately learning the **Manoonchai** keyboard layout (the
"Dvorak of Thai", an ergonomically re-engineered alternative to the standard
Kedmanee layout). They already type Thai fluently on another layout and are
retraining muscle memory. Context: sitting at a physical keyboard, full focus,
repeating short drills to build speed and accuracy. The job: practice a specific
character group until the new key positions feel automatic, and watch speed climb.

## Product Purpose

A browser typing trainer that teaches Manoonchai without the user having to
reconfigure their OS, by translating physical key positions into Manoonchai
characters in-app. Lessons introduce characters in a deliberate order; each drill
is a timed run of sampled words with live feedback (next-key keymap glow, prefix
spellcheck) and honest post-drill stats (raw WPM, net WPM, accuracy). Success: a
learner returns daily, climbs the lesson order, and sees their WPM curve rise.

## Brand Personality

A focused instrument. Calm, precise, keyboard-centric. Three words: **precise,
quiet, tactile**. The interface recedes so the typing line and the keymap are the
heroes; the single moment of energy is the next-key glow. Serious tool for adults,
not a game.

## Anti-references

- **Generic SaaS / AI-slop**: no cream-bg + single-accent template, no identical
card grids, no tiny-uppercase tracked eyebrows, nothing that reads "AI generated".
- **Childish edu-app**: no mascots, confetti, primary-color gamification, or badge
soup.
- **v1's unthemed look**: no Tailwind-default gray buttons, default green, or the
neon multi-layer box-shadow glow. Fresh identity.
- **A literal Monkeytype clone**: borrow the genre's restraint, not its exact
mono-amber-on-charcoal appearance.

## Design Principles

1. **The instrument is the layout.** The keymap and the typing line are the product;
chrome stays out of their way.
2. **One moment of energy.** Restraint everywhere so the next-key cue and a finished
drill land with impact. Glow is earned, not ambient.
3. **Honest readouts.** Stats say exactly what they measure (raw vs net WPM, accuracy);
no flattering or mislabeled numbers.
4. **Feedback never depends on color alone.** Correct / error / next states also carry
shape, weight, or motion, since the entire loop is red/green.
5. **Thai-first typography.** Thai script is the primary content, set to read
beautifully at size, not as an afterthought to a Latin UI.

## Accessibility & Inclusion

WCAG 2.1 AA contrast across light and dark themes. Full keyboard operability (it is a
typing app: every action reachable without a mouse). `prefers-reduced-motion` honored
on every animation (glow, chart draw, drill transitions degrade to instant/crossfade).
State color is always paired with a non-color cue.
55 changes: 49 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
[![logo](https://manoonchai.com/_next/image?url=%2Fmanoonchai.png&w=64&q=75)](https://www.manoonchai.com/)
# Learn Manoonchai v2

# Learn Manoonchai
A browser typing trainer for the **Manoonchai** Thai keyboard layout. It maps
physical key positions to Manoonchai characters in-app, so you can practice the
layout without reconfiguring your OS. A full rewrite of v1 on a modern stack.

## Development
- **Domain language**: [CONTEXT.md](./CONTEXT.md) (lesson, drill, raw/net WPM, …)
- **Product brief**: [PRODUCT.md](./PRODUCT.md)
- **Key decisions**: [docs/adr/](./docs/adr/)

Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
## Stack

SvelteKit 2 · Svelte 5 (runes) · Tailwind v4 · TypeScript · Bun workspace ·
Oxlint + Oxfmt · Vite+ (Vitest) · self-hosted Sarabun · LayerChart.

## What it does

- **Lessons** introduce characters in a deliberate order; pick any of 30+ freely.
- A **drill** is a timed run of 10/25/50 words sampled from the active lesson.
- Live feedback: next-key keymap glow, per-character correctness, prefix check.
- Honest end-of-drill stats: raw WPM, net WPM, accuracy, and a raw/net chart with
error markers (see [ADR 0001](./docs/adr/0001-honest-stats-engine.md)).
- Light/dark themes (system default), full keyboard operability, reduced-motion.

`Tab` restarts a drill · `Esc` opens settings.

## Getting started

```bash
yarn
yarn dev
bun install
bun run dev # → http://localhost:5173
```

## Project structure

```
.
├── apps/web/
│ └── src/lib/
│ ├── engine/ # pure domain logic (layout, drill, stats, …) + unit tests
│ ├── components/ # UI + component tests
│ ├── state/ # runes-backed, persisted settings
│ └── data/ # ported Manoonchai layout + lesson word pools
├── packages/ # shared env + tsconfig
└── v1/ # the previous app, archived
```

## Scripts (run inside `apps/web`)

- `bun run dev` / `bun run build` / `bun run preview`
- `bun run check` — `svelte-check` type checking
- `bun run test` — Vitest unit + component tests
- `bun run test:e2e` — Playwright end-to-end (drives a full drill)

Repo-wide lint/format from the `v2/` root: `bun run lint`, `bun run format`.
24 changes: 24 additions & 0 deletions apps/web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
node_modules

# Output
.output
.vercel
.netlify
.wrangler
.alchemy
/.svelte-kit
/build

# OS
.DS_Store
Thumbs.db

# Env
.env
.env.*
!.env.example
!.env.test

# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
File renamed without changes.
Loading
Loading