From c9643d898ae453431ae397302be4cd46f2cd5f89 Mon Sep 17 00:00:00 2001 From: srfwb <264158739+srfwb@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:35:22 +0200 Subject: [PATCH 01/15] docs(spec): lessons and challenges system design --- .gitignore | 1 + .../specs/2026-04-26-lessons-system-design.md | 298 ++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-26-lessons-system-design.md diff --git a/.gitignore b/.gitignore index 4e69aa4..1436f73 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ dist-ssr .claude.local.md .claude-*/ CLAUDE.md +.superpowers/ diff --git a/docs/superpowers/specs/2026-04-26-lessons-system-design.md b/docs/superpowers/specs/2026-04-26-lessons-system-design.md new file mode 100644 index 0000000..e75ac78 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-lessons-system-design.md @@ -0,0 +1,298 @@ +# Lessons & Challenges System — Design Spec + +## Context + +WeCode is a desktop IDE that teaches HTML and CSS. The app currently has two views: Home (project picker) and IDE (editor + preview). This spec adds a third pillar: **structured learning** through guided lessons and free-form challenges, both reusing the existing IDE shell. + +The system ships as milestone 3. Scope for v1: the full engine (navigation, validation, progression, dock UI) plus **1 lesson + 1 challenge** as proof of concept. + +## Architecture + +### View routing + +``` +useIdeStore.view: "home" | "ide" | "lesson" +``` + +- `"home"` — Home shell with rail-routed sub-views +- `"ide"` — Free project editing (unchanged) +- `"lesson"` — IDE shell wrapped in a `LessonProvider` context + +### Home sub-views (rail as router) + +The Home rail gains clickable items that swap the main content zone: + +``` +useHomeStore.tab: "accueil" | "lessons" | "challenges" +``` + +| Rail item | Tab value | What the main zone shows | +|-----------|-----------|--------------------------| +| Accueil | `"accueil"` | Current home content (search, continue card, recent projects, templates) | +| Leçons | `"lessons"` | Grid of all lessons with progress indicators | +| Challenges | `"challenges"` | Grid of all challenges with completion status | + +Items not yet wired (Projets, Cheatsheets, Settings) remain visible but disabled. + +The rail shows a progress counter next to Leçons: `3 / 12` (completed / total). + +### LessonProvider (React Context) + +When `view === "lesson"`, `App.tsx` renders: + +```tsx + + + +``` + +The context exposes: + +```ts +interface LessonContextValue { + lesson: LessonData; + checkpoints: CheckpointState[]; // { id, status: "todo"|"active"|"done" } + currentStepIndex: number; + fileOpsLocked: boolean; // true for lessons, false for challenges + progress: number; // 0–1 ratio + validateNow: () => void; + exitLesson: () => void; +} +``` + +Components that adapt their behavior in lesson mode: +- **FileTree** — hides create/delete/rename buttons when `fileOpsLocked` +- **Toolbar** — brand click calls `exitLesson()` instead of `setView("home")` +- **LessonDock** — reads `lesson`, `checkpoints`, `currentStepIndex` from context +- **StatusBar** — shows checkpoint progress instead of version chip + +When the context is absent (free project mode), all components behave as today. + +### Stores + +**`useLessonStore`** (new Zustand store): +- `activeLessonId: string | null` +- `checkpointStates: Record` +- `startLesson(id): void` — hydrates VFS with starter files, sets view to `"lesson"` +- `completeCheckpoint(id): void` +- `resetLesson(id): void` + +**`useHomeStore`** (new Zustand store): +- `tab: "accueil" | "lessons" | "challenges"` +- `setTab(tab): void` + +**Global progression** — persisted via `tauri-plugin-store` as `wecode.progress.json`: +```ts +interface ProgressStore { + version: 1; + completed: Record; // lessonId → timestamp + checkpoints: Record; // lessonId → completed checkpoint ids +} +``` + +## Lesson data format + +Each lesson/challenge is a JSON file in `src/lessons/data/`: + +```ts +interface LessonData { + id: string; + type: "lesson" | "challenge"; + title: string; + description: string; + difficulty: "débutant" | "intermédiaire" | "avancé"; + estimatedMinutes: number; + tags: string[]; + allowFileOps: boolean; // false for lessons, true for challenges + starterFiles: Record; // VFS path → content + steps: LessonStep[]; +} + +interface LessonStep { + heading: string; + paragraphs: LessonParagraph[]; + checkpoints: CheckpointDef[]; +} + +interface LessonParagraph { + kind: "text" | "code"; + content: string; // text may contain `backtick` for inline code +} + +interface CheckpointDef { + id: string; + label: string; + rule: ValidationRule; +} +``` + +## Validation engine + +### Rule types + +| Type | Fields | What it checks | +|------|--------|----------------| +| `element-exists` | `selector`, `file` | `querySelector(selector)` returns ≥1 match | +| `element-count` | `selector`, `file`, `min?`, `max?` | Match count within range | +| `element-text` | `selector`, `file`, `text`, `match` | Element text content matches (`contains`, `exact`, `regex`, `not-empty`) | +| `attribute-exists` | `selector`, `file`, `attribute` | Element has the attribute | +| `attribute-value` | `selector`, `file`, `attribute`, `value?`, `match` | Attribute value check | +| `attribute-count` | `selector`, `file`, `minAttributes` | Element has ≥N attributes | +| `css-property` | `selector`, `file`, `property`, `match` | CSS property exists on selector | +| `css-property-value` | `selector`, `file`, `property`, `value`, `match` | CSS property value check (`exact`, `contains`, `gte`, `lte`) | +| `file-contains` | `file`, `text` | Raw source contains string | +| `file-not-contains` | `file`, `text` | Raw source does NOT contain string | +| `file-regex` | `file`, `pattern` | Raw source matches regex | +| `nesting` | `parent`, `child`, `direct?`, `file` | Child is descendant (or direct child) of parent | +| `element-order` | `selectors[]`, `within`, `file` | Elements appear in document order | +| `sibling` | `first`, `then`, `file` | Two elements are adjacent siblings | +| `indent-style` | `file`, `style` (`spaces`\|`tabs`), `size?` | Source follows indentation convention | +| `composite` | `operator` (`and`\|`or`), `rules[]` | Combines multiple rules | + +### Match modes + +`exact`, `contains`, `starts-with`, `ends-with`, `regex`, `not-empty`, `exists`, `gte`, `lte`. + +### Execution + +- **Trigger**: VFS `change` event, debounced 300 ms. +- **Method**: `DOMParser` parses the HTML source into a DOM. `querySelector` / `querySelectorAll` run the selectors. CSS rules are checked by parsing `