This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a static single-page web application for learning any workshops by examples. It's a general-purpose learning platform featuring example-based lessons with audio pronunciation and progress tracking via LocalStorage.
- static: easy deploy on github pages
- open: learn any workshop, lessons with sections, sections with examples, eg. new language, math, theory for driver / boot / pilot license
- multi-language: base language, learn based on your preferred known language
- learning items: track progress by marking items as learned
- audio reading: learning by just listening to examples
- Framework: Vue 3.4+ (Composition API with SFCs)
- Routing: Vue Router 4.6+ (hash-based routing)
- Dependency Management: pnpm
- Build Tool: Vite 5.0
- Styling: Tailwind CSS 3.4
- Testing:
- Vitest 1.0 (unit tests with happy-dom/jsdom)
- Playwright 1.40 (E2E tests)
- Data Format: YAML for lesson content (parsed with js-yaml 4.1)
- Markdown: Marked 17.0 for rendering explanations
- Deployment: GitHub Pages (via GitHub Actions)
open-learn/
├── index.html # Minimal HTML entry point
├── src/
│ ├── main.js # App entry point - creates Vue app with router
│ ├── App.vue # Root component with unified navigation bar
│ ├── style.css # Custom styles (imports Tailwind)
│ ├── router/
│ │ └── index.js # Vue Router configuration
│ ├── views/ # Page components
│ │ ├── Home.vue # Workshop selection page
│ │ ├── LessonsOverview.vue # Lessons grid page
│ │ ├── LessonDetail.vue # Individual lesson page (assessments, audio, progress)
│ │ ├── LearningItems.vue # Learning items browser
│ │ ├── Settings.vue # Settings page (preferences, coach, export/import)
│ │ └── AddSource.vue # Add external workshop page
│ ├── composables/ # Reusable composition functions (singleton pattern)
│ │ ├── useLessons.js # Lesson loading logic with js-yaml + remote sources
│ │ ├── useSettings.js # Settings persistence logic
│ │ ├── useProgress.js # Progress tracking logic (learning items)
│ │ ├── useAssessments.js # Assessment answers, validation, coach queue
│ │ └── useAudio.js # Audio playback system
│ └── utils/
│ └── formatters.js # Display name formatting
├── public/
│ ├── CNAME # Custom domain: open-learn.app
│ ├── default-sources.yaml # Default workshop sources (loaded at startup)
│ └── lessons/ # YAML lesson content (deployed as-is)
│ ├── index.yaml # Root index - lists available interface languages
│ ├── deutsch/ # German interface folder
│ │ ├── workshops.yaml # Lists bundled workshops
│ │ ├── open-learn-guide/ # Bundled: platform tutorial
│ │ └── open-learn-feedback/ # Bundled: feedback workshop
│ ├── english/ # English interface folder
│ │ ├── workshops.yaml
│ │ ├── open-learn-guide/
│ │ └── open-learn-feedback/
│ └── README.md # Lesson system documentation
├── docs/
│ ├── features.md # Complete feature inventory
│ ├── development-plan.md # Current development plan
│ ├── lesson-schema.md # Individual lesson YAML schema documentation
│ ├── yaml-schemas.md # Index YAML schemas (languages/topics/lessons)
│ ├── audio-system.md # Audio playback documentation
│ ├── external-workshop-guide.md # Guide for creating external workshops
│ ├── lesson-plan-template.md # Lesson planning guide
│ └── adr/ # Architecture Decision Records
├── tests/
│ ├── basic.test.js # Basic sanity tests
│ ├── dark-mode.test.js # Dark mode toggle tests
│ ├── assessments.test.js # Assessment validation + persistence tests
│ ├── coach-forwarding.test.js # Coach batch queue + API tests
│ └── e2e/
│ └── app.spec.js # Playwright E2E tests
├── vite.config.js # Vite config (base: '/')
├── tailwind.config.js # Tailwind customization
├── playwright.config.js # Playwright E2E test config
└── package.json # Dependencies and scripts
# Install dependencies (first time setup)
pnpm install
# Development server (http://localhost:5173)
pnpm dev
# Build for production (outputs to dist/)
pnpm build
# Preview production build locally
pnpm preview
# Run unit tests (Vitest)
pnpm test
# Run unit tests with UI
pnpm test:ui
# Run E2E tests (Playwright)
pnpm test:e2eComponent-Based Architecture:
- Uses
.vueSingle File Components (SFCs) - Vue Router for client-side routing
- Composition API with composables for shared logic
- Unified navigation bar in root App component
Main Components:
App.vue- Root component with unified navigation (back button, dynamic title, settings button)Home.vue- Workshop selection page (route:/)LessonsOverview.vue- Lessons grid page (route:/:learning/:workshop/lessons)LessonDetail.vue- Individual lesson page (route:/:learning/:workshop/lesson/:number)LearningItems.vue- Learning items browser (route:/:learning/:workshop/items/:number?)Settings.vue- Settings page (route:/settings)AddSource.vue- Add external workshop (route:/add?source=URL)
Composables (Reusable logic, all use singleton pattern — see docs/adr/005):
useLessons()- Lesson loading with js-yaml parserloadAvailableContent()- Load main lesson index + default + user content sourcesloadWorkshopsForLanguage(lang)- Load workshops for a languageloadAllLessonsForWorkshop(lang, workshop)- Load all lessons for a workshopdefault-sources.yaml- Platform-shipped workshop sources (loaded at startup, non-removable)- Content source management (add/remove external workshops)
- IPFS URL resolution
useSettings()- Settings management- Shared reactive state across all components
- Automatic localStorage persistence via watchers
- Dark mode toggle with DOM class manipulation
- Settings loaded on app initialization in
main.js
useProgress()- Progress tracking- Track learned items per language/workshop combination
- Persisted to localStorage
useAssessments()- Assessment system- Answer validation for input, multiple-choice, select types
- Save/load answers to/from localStorage
- Coach queue: batch answer forwarding to external coach API
flushCoachQueue()(async POST) andflushCoachQueueSync()(sendBeacon for page close)
useAudio()- Audio playback system- Pre-loads MP3 files per lesson
- Media Session API for lock screen controls
- Variable playback speed
Routing:
#/- Home page (workshop selection)#/:learning/:workshop/lessons- Lessons overview grid#/:learning/:workshop/lesson/:number- Lesson detail view#/:learning/:workshop/items/:number?- Learning items#/settings- Settings panel#/add?source=URL- Add external workshop
Uses hash-based routing (createWebHashHistory) for GitHub Pages compatibility.
Navigation Pattern:
- Dynamic Title: Changes based on route
- Home: "🎓 Open Learn"
- Overview: Workshop name (e.g., "Portugiesisch")
- Detail: Lesson title (e.g., "Basic Verbs - Ser and Estar")
- Settings: "⚙️ Settings"
- Back Button: Visible on all pages except home
- Settings Button: Always visible in top-right corner
YAML Loading Flow:
- Load
lessons/index.yaml→ get available interface languages - User selects language → try
lessons/{language}/workshops.yamlfirst, fallback totopics.yaml→ get workshops - User selects workshop → navigate to
/:learning/:workshop/lessons - Load
lessons/{language}/{workshop}/lessons.yaml→ get lesson folder names - Load all lessons for workshop → fetch
{folder}/content.yamlfor each folder and parse with js-yaml
Lessons follow a hierarchical structure: Lesson → Sections → Examples → Related Items
number: 1
title: "Lesson Title"
description: "Brief description"
sections:
- title: "Section Title"
explanation: |
Markdown-formatted explanation text
examples:
- q: "Question/source sentence"
a: "Answer/translation"
labels: ["Futur", "Gerundium"] # Optional labels
rel:
- ["term", "translation", "context"] # First element is unique ID
- ["word", "meaning"]Key Concepts:
- Three-level hierarchy: Language → Workshop → Lesson
- Bundled:
lessons/<language>/<workshop>/<lesson-folder>/ - Remote: loaded from
public/default-sources.yamlor user-added sources - Example:
deutsch/open-learn-guide/01-welcome/= bundled tutorial - Example:
open-learn.app/workshop-portugiesisch/= remote workshop
- Bundled:
- Self-contained lessons: Each lesson folder contains its content and audio files
content.yaml- Lesson contentaudio/- Audio files for pronunciation- Makes lessons portable and distributable (can be hosted on IPFS, CDN, etc.)
- Workshop repos: Most workshops live in dedicated repos under
openlearnapporg- Deployed to
open-learn.app/<repo-name>/via GitHub Pages - Only
open-learn-guideandopen-learn-feedbackare bundled in this repo
- Deployed to
- Labels: Optional categorization (e.g. for grammar, like "Futur", "Passiv")
- Related items (
rel): Vocabulary/concepts with first element as unique identifier - Markdown support: Section explanations support markdown formatting
See docs/lesson-schema.md for individual lesson documentation and docs/yaml-schemas.md for index file schemas.
js-yaml (import yaml from 'js-yaml'):
- Full-featured YAML parser for lesson content
- Handles all YAML spec features (comments, multi-line strings, etc.)
- Used in
useLessons()composable viayaml.load(text)
marked (import { marked } from 'marked'):
- Markdown-to-HTML converter
- Used for rendering section explanations in lesson detail view
- Supports GitHub-flavored markdown
Singleton Pattern: The useSettings() composable uses module-level reactive state, ensuring all components share the same settings instance.
Persistence Flow:
- Settings loaded from
localStorageinmain.jsbefore app mounts - Changes automatically saved via Vue watchers
- Dark mode applied to
<html>element via class toggle - All components access the same reactive settings object
Settings:
showAnswers(boolean): Toggle visibility of answer translations in lessonsshowLearningItems(boolean): Show/hide learning items on lesson cardsshowLabels(boolean): Show/hide grammar labelsdarkMode(boolean): Dark theme toggleaudioSpeed(number): Playback speed (0.6, 0.8, 1.0)readAnswers(boolean): Include answers in audio playbackhideLearnedExamples(boolean): Filter out learned examplesshowDebugOverlay(boolean): Debug info overlaycoachConsent(boolean): Opt-in to forward answers to workshop coachcoachIdentifier(string): Name/email sent with coach submissions
- Choose or create the appropriate workshop folder:
public/lessons/<language>/<workshop>/ - Create a new lesson folder:
public/lessons/<language>/<workshop>/##-lesson-name/ - Create
content.yamlin the lesson folder following the schema (seedocs/lesson-schema.md) - Optionally create an
audio/subfolder for audio files - Add the folder name to
public/lessons/<language>/<workshop>/lessons.yaml:lessons: - 01-basics - 02-your-new-lesson
- Generate audio files using
./generate-audio.sh public/lessons/<language>/<workshop>/02-your-new-lesson/
- Create folder structure:
public/lessons/<language>/<workshop>/ - Add workshop to
public/lessons/<language>/workshops.yaml:workshops: - folder: portugiesisch code: pt-PT - folder: your-new-workshop code: de-DE
- Create
public/lessons/<language>/<workshop>/lessons.yamlwith lesson folder names - Add lesson folders with
content.yamlfiles
- Add language to
public/lessons/index.yaml:languages: - folder: deutsch code: de-DE - folder: your-new-language code: xx-XX
- Create
public/lessons/<language>/workshops.yamlwith workshops list - Create workshop folders with
lessons.yamland lesson folders
See docs/yaml-schemas.md for detailed documentation on all index file schemas.
| Term | Code variable | Meaning |
|---|---|---|
| Language | learning |
Interface language the user knows (e.g. deutsch) |
| Workshop | workshop |
Subject being learned (e.g. portugiesisch) |
| Lesson | lesson |
Single learning unit, numbered (e.g. 01-essential-verbs) |
| Learning Item | rel |
Vocabulary/concept from examples, tracked as learned/unlearned |
| Coach | lesson.coach |
Optional external service receiving assessment answers |
Examples can have a type field for interactive assessments (default is qa which just shows question + answer).
Types: input (text field), multiple-choice (checkboxes), select (radio buttons)
Click-to-Save: No submit buttons. Answers auto-validate on interaction:
input: validates on Enter key or blurselect: validates immediately on radio clickmultiple-choice: live per-option feedback (green/red borders), saves when all correct
Visual feedback: Correct assessments show ✓ before the question and a green card background. Answers are always re-editable (no locked state).
Coach integration: If lesson.coach is defined and user has coachConsent, answers are queued and batch-sent to the coach API.
See docs/features.md for the complete feature inventory and docs/adr/ for architectural decisions.
tests/basic.test.js: Basic sanity checkstests/dark-mode.test.js: Dark mode toggle + persistencetests/assessments.test.js: Assessment validation, save/load, clear (15 tests)tests/coach-forwarding.test.js: Coach queue, batch sending, consent (11 tests)
tests/e2e/app.spec.js: Page load, HTML structure, dark mode
GitHub Actions (.github/workflows/static.yml):
- Triggers on push to
mainbranch - Runs build
- Deploys
dist/to GitHub Pages
Important: Vite is configured with base: '/' for custom domain deployment at open-learn.app.
- LocalStorage: Settings and progress persistence
- Fetch API: Dynamic YAML lesson loading
- Media Session API: Lock screen audio controls
- All components use
.vueSingle File Components (SFCs) - Vue Router handles client-side routing with hash-based URLs for GitHub Pages compatibility
- Tailwind classes are used directly in component templates
- Lessons are loaded dynamically at runtime - no build-time processing
- Dark mode: Tailwind
dark:classes +<html class="dark">toggle via useSettings composable - YAML parsing uses js-yaml library for full spec support
- Markdown rendering uses marked library for section explanations
- Composables pattern for shared logic (useLessons, useSettings, useProgress, useAssessments, useAudio)
- Navigation state is managed by Vue Router - no manual view switching
- Dynamic page titles based on route and content
Maintain CHANGELOG.md in the project root. When creating a PR, add an entry to the changelog under the current date. Group entries by Features, Fixes, and Docs & CI. Include the PR number in parentheses.