diff --git a/index.html b/index.html
index 67f127c..b780cd6 100644
--- a/index.html
+++ b/index.html
@@ -28,7 +28,16 @@
-
+
diff --git a/src/app.ts b/src/app.ts
index 5f75f12..c391b3d 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,8 +1,9 @@
-import type { ScreenName } from "./types";
-import { openDB, getProgress } from "./storage/db";
-import { initWelcomeScreen } from "./ui/screens";
+import type { ScreenName, KeyboardLayout } from "./types";
+import { openDB, getProgress, saveProgress } from "./storage/db";
+import { initWelcomeScreen, initLessonScreen } from "./ui/screens";
let db: IDBDatabase;
+let lessonCleanup: (() => void) | null = null;
export function showScreen(name: ScreenName): void {
document.querySelectorAll(".screen").forEach((el) => {
@@ -18,14 +19,57 @@ export function getDB(): IDBDatabase {
return db;
}
+export async function startLesson(
+ level: number,
+ layout: KeyboardLayout,
+): Promise {
+ // Clean up previous lesson if any
+ if (lessonCleanup) {
+ lessonCleanup();
+ lessonCleanup = null;
+ }
+
+ showScreen("lesson");
+
+ lessonCleanup = initLessonScreen(db, level, layout, async (completedLevel: number) => {
+ // Update progress after lesson completion
+ const progress = await getProgress(db);
+ if (!progress) return;
+
+ // Add level to completedLevels if not already there
+ if (!progress.completedLevels.includes(completedLevel)) {
+ progress.completedLevels.push(completedLevel);
+ }
+
+ // Advance to next level if this was the current level (LEVL-02, LEVL-03)
+ if (completedLevel === progress.currentLevel) {
+ progress.currentLevel = completedLevel + 1;
+ }
+
+ // Increment session count
+ progress.totalSessions++;
+
+ // Add today's date if not already recorded
+ const today = new Date().toISOString().split("T")[0]!;
+ if (!progress.sessionDates.includes(today)) {
+ progress.sessionDates.push(today);
+ }
+
+ await saveProgress(db, progress);
+
+ // Navigate to forest (per D-07: skip reward in Phase 1)
+ showScreen("forest");
+ });
+}
+
export async function initApp(): Promise {
db = await openDB();
const progress = await getProgress(db);
if (progress && progress.selectedCharacter) {
- // Returning user — skip welcome, go to forest
+ // Returning user -- go to forest
showScreen("forest");
} else {
- // First time — show welcome
+ // First time -- show welcome
initWelcomeScreen(db, showScreen);
showScreen("welcome");
}
diff --git a/src/styles/main.css b/src/styles/main.css
index 96106be..e07486e 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -162,6 +162,59 @@ h3 {
cursor: not-allowed;
}
+/* Lesson screen */
+.lesson {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2rem;
+ padding: 2rem;
+ min-height: 100vh;
+ background: var(--bg-cream);
+}
+
+.lesson__letter-area {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 200px;
+ height: 200px;
+}
+
+.falling-letter {
+ font-family: "Quicksand", sans-serif;
+ font-size: 5rem;
+ font-weight: 700;
+ color: var(--accent-green);
+ animation: letter-float 2s ease-in-out infinite;
+}
+
+@keyframes letter-float {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+}
+
+.falling-letter--dissolve {
+ animation: letter-dissolve 0.3s ease-out forwards;
+}
+
+@keyframes letter-dissolve {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 100% {
+ transform: scale(1.5);
+ opacity: 0;
+ }
+}
+
/* Keyboard */
.keyboard {
display: flex;
diff --git a/src/ui/screens.ts b/src/ui/screens.ts
index a281418..6db614c 100644
--- a/src/ui/screens.ts
+++ b/src/ui/screens.ts
@@ -6,6 +6,12 @@ import type {
} from "../types";
import { companions } from "../companion/characters";
import { saveProgress } from "../storage/db";
+import {
+ createExerciseState,
+ handleKeyPress,
+ getCurrentLetter,
+} from "../game/typing";
+import { renderKeyboard, highlightKey, pressKey } from "../game/keyboard";
export function initWelcomeScreen(
db: IDBDatabase,
@@ -67,3 +73,67 @@ export function initWelcomeScreen(
navigateTo("forest");
});
}
+
+export function initLessonScreen(
+ _db: IDBDatabase,
+ level: number,
+ layout: KeyboardLayout,
+ onComplete: (level: number) => void,
+): () => void {
+ const letterArea = document.getElementById("letter-area")!;
+ const keyboardContainer = document.getElementById("lesson-keyboard")!;
+ const state = createExerciseState(level);
+
+ // Render keyboard
+ renderKeyboard(keyboardContainer, layout, level);
+
+ // Show current letter
+ function showCurrentLetter(): void {
+ const letter = getCurrentLetter(state);
+ if (!letter) return;
+ letterArea.innerHTML = "";
+ const letterEl = document.createElement("div");
+ letterEl.className = "falling-letter";
+ letterEl.textContent = letter === " " ? "\u2423" : letter.toUpperCase();
+ letterArea.appendChild(letterEl);
+ highlightKey(letter);
+ }
+
+ showCurrentLetter();
+
+ // Keyboard handler
+ function onKeyDown(e: KeyboardEvent): void {
+ // Ignore modifier keys, function keys, etc.
+ if (e.key.length > 1 && e.key !== " ") return;
+ e.preventDefault();
+
+ const result = handleKeyPress(state, e.key);
+ pressKey(e.key);
+
+ if (result.correct) {
+ // Correct: dissolve letter with animation
+ const currentEl = letterArea.querySelector(".falling-letter");
+ if (currentEl) {
+ currentEl.classList.add("falling-letter--dissolve");
+ }
+
+ if (result.exerciseComplete) {
+ // Lesson complete
+ document.removeEventListener("keydown", onKeyDown);
+ onComplete(level);
+ } else {
+ // Show next letter after brief delay
+ setTimeout(() => showCurrentLetter(), 300);
+ }
+ }
+ // Wrong: do nothing to letter (per D-03), correct key already highlighted
+ }
+
+ document.addEventListener("keydown", onKeyDown);
+
+ // Return cleanup function
+ return () => {
+ document.removeEventListener("keydown", onKeyDown);
+ letterArea.innerHTML = "";
+ };
+}