
Codebase To Course
- 4 installs
- 7 repo stars
- Updated August 2, 2026
- practicalswan/agent-skills
codebase-to-course is a Claude Code skill for ai & agent building.
About
Generates a single-page HTML course from a codebase with scroll navigation, animated visualizations, quizzes, and side-by-side code-to-plain-English translations. A developer uses it to create an interactive tutorial or walkthrough explaining a project.
- Self-contained HTML with scroll-based navigation and quizzes
- Code-with-plain-English side-by-side translations
Codebase To Course by the numbers
- 4 all-time installs (skills.sh)
- Ranked #1,241 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/practicalswan/agent-skills --skill codebase-to-courseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 7 |
| Last updated | August 2, 2026 |
| Repository | practicalswan/agent-skills ↗ |
How do I helps with ai & agent building tasks.?
Turns a codebase into a self-contained interactive HTML course that teaches how the code works to non-technical people.
Who is it for?
A solo builder working on ai & agent building tasks who needs structured help with codebase to course.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks., or when codebase-to-course is a claude code skill for ai & agent building.
What you get
Structured output aligned to codebase-to-course: codebase-to-course, AI & Agent Building.
Files
Codebase-to-Course
Transform any codebase into a stunning, interactive course. The output is a directory containing a pre-built styles.css, main.js, per-module HTML files, and an assembled index.html — open it directly in the browser with no setup required (only external dependency: Google Fonts CDN). The course teaches how the code works through scroll-based modules, animated visualizations, embedded quizzes, and plain-English translations of code.
- Leverage native parallel subagent dispatch and 200k+ context windows where available.
First-Run Welcome
When the skill is first triggered and the user hasn't specified a codebase yet, introduce yourself and explain what you do:
I can turn any codebase into an interactive course that teaches how it works — no coding knowledge required.
>
Just point me at a project:
- A local folder — e.g., "turn ./my-project into a course"
- A GitHub link — e.g., "make a course from https://github.com/user/repo"
- The current project — if you're already in a codebase, just say "turn this into a course"
>
I'll read through the code, figure out how everything fits together, and generate a beautiful single-page HTML course with animated diagrams, plain-English code explanations, and interactive quizzes. The whole thing runs in your browser — no setup needed.
If the user provides a GitHub link, clone the repo first (git clone <url> /tmp/<repo-name>) before starting the analysis. If they say "this codebase" or similar, use the current working directory.
Who This Is For
The target learner is a "vibe coder" — someone who builds software by instructing AI coding tools in natural language, without a traditional CS education. They may have built this project themselves (without looking at the code), or they may have found an interesting open-source project on GitHub and want to understand how it's built. Either way, they don't yet understand what's happening under the hood.
Assume zero technical background. Every CS concept — from variables to APIs to databases — needs to be explained in plain language as if the learner has never encountered it. No jargon without definition. No "as you probably know." The tone should be like a smart friend explaining things, not a professor lecturing.
Their goals are practical, not academic:
- Have enough technical knowledge to effectively steer AI coding tools — make better architectural and tech stack decisions
- Detect when AI is wrong — spot hallucinations, catch bad patterns, know when something smells off
- Intervene when AI gets stuck — break out of bug loops, debug issues, unblock themselves
- Build more advanced software with production-level quality and reliability
- Be technically fluent enough to discuss decisions with engineers confidently
- Acquire the vocabulary of software — learn the precise technical terms so they can describe requirements clearly and unambiguously to AI coding agents (e.g., knowing to say "namespace package" instead of "shared folder thing")
They are NOT trying to become software engineers. They want coding as a superpower that amplifies what they're already good at. They don't need to write code from scratch — they need to read it, understand it, and direct it.
Why This Approach Works
This skill inverts traditional CS education. The old model is: memorize concepts for years → eventually build something → finally see the point (most people quit before step 3). This model is: build something first → experience it working → now understand how it works.
The learner already has context that traditional students don't — they've used the app, they know what it does, they may have even described its features in natural language. The course meets them where they are: "You know that button you click? Here's what happens under the hood when you click it."
Every module answers "why should I care?" before "how does it work?" The answer to "why should I care?" is always practical: because this knowledge helps you steer AI better, debug faster, or make smarter architectural decisions.
The directory-based output is intentional: separating CSS/JS from content means AI never regenerates boilerplate, each module is written independently (keeping output size small and quality high), and the assembled index.html works offline with zero setup.
---
The Process
Phase 1: Codebase Analysis
Before writing course HTML, deeply understand the codebase. Read all the key files, trace the data flows, identify the "cast of characters" (main components/modules), and map how they communicate. Thoroughness here pays off — the more you understand, the better the course.
What to extract:
- The main "actors" (components, services, modules) and their responsibilities
- The primary user journey (what happens when someone uses the app end-to-end)
- Key APIs, data flows, and communication patterns
- Clever engineering patterns (caching, lazy loading, error handling, etc.)
- Real bugs or gotchas (if visible in git history or comments)
- The tech stack and why each piece was chosen
Figure out what the app does yourself by reading the README, the main entry points, and the UI code. Don't ask the user to explain the product — they may not be familiar with it either. The course should open by explaining what the app does in plain language (a brief "here's what this thing does and why it's interesting") before diving into how it works. The first module should start with a concrete user action — "imagine you paste a YouTube URL and click Analyze — here's what happens under the hood."
Phase 2: Curriculum Design
Structure the course as 4-6 modules. Most courses need 4-6. Only go to 7-8 if the codebase genuinely has that many distinct concepts worth teaching. Fewer, better modules beat more, thinner ones.
The arc always starts from what the learner already knows (the user-facing behavior) and moves toward what they don't (the code underneath). Think of it as zooming in: start wide with the experience, then progressively peel back layers.
| Module Position | Purpose | Why it matters for a vibe coder |
|---|---|---|
| 1 | "Here's what this app does — and what happens when you use it" | Start with the product (what it does, why it's interesting), then trace a core user action into the code. Grounds everything in something concrete. |
| 2 | Meet the actors | Know which components exist so you can tell AI "put this logic in X, not Y" |
| 3 | How the pieces talk | Understand data flow so you can debug "it's not showing up" problems |
| 4 | The outside world (APIs, databases) | Know what's external so you can evaluate costs, rate limits, and failure modes |
| 5 | The clever tricks | Learn patterns (caching, chunking, error handling) so you can request them from AI |
| 6 | When things break | Build debugging intuition so you can escape AI bug loops |
| 7 | The big picture | See the full architecture so you can make better decisions about what to build next |
This is a menu, not a checklist. Pick the modules that serve the codebase — a simple CLI tool needs 4, not 7. Adapt the arc to the codebase's complexity.
The key principle: Every module should connect back to a practical skill — steering AI, debugging, making decisions. If a module doesn't help the learner DO something better, cut it or reframe it until it does.
Each module should contain:
- 3-6 screens (sub-sections that flow within the module)
- At least one code-with-English translation
- At least one interactive element (quiz, visualization, or animation)
- One or two "aha!" callout boxes with universal CS insights
- A metaphor that grounds the technical concept in everyday life — but NEVER reuse the same metaphor across modules, and NEVER default to the "restaurant" metaphor (it's overused). Pick metaphors that organically fit the specific concept. The best metaphors feel inevitable for the concept, not forced.
Mandatory interactive elements (every course must include ALL of these):
- Group Chat Animation — at least one across the course. These are the iMessage/WeChat-style conversations between components. They're one of the most engaging elements and must always appear, even if you have to creatively frame a module's concept as a conversation between actors.
- Message Flow / Data Flow Animation — at least one across the course. The step-by-step packet animation between actors. If the codebase has any kind of request/response, data pipeline, or multi-step process, animate it. Every codebase has data flowing somewhere — find it.
- Code ↔ English Translation Blocks — at least one per module (already required above, but reiterating: this is non-negotiable).
- Quizzes — at least one per module (multiple-choice, scenario, drag-and-drop, or spot-the-bug — any quiz type counts).
- Glossary Tooltips — on every technical term, first use per module.
These five element types are the backbone of every course. Other interactive elements (architecture diagrams, layer toggles, pattern cards, etc.) are optional and should be added when they fit. But the five above must ALWAYS be present — no exceptions.
Do NOT present the curriculum for approval — just build it. The user wants a course, not a planning document. Design the curriculum internally, then go straight to building. If they want changes, they'll tell you after seeing the result.
After designing the curriculum, decide which build path to use:
- Simple codebase (single-purpose CLI, small web app, library, one clear entry point, 5 or fewer modules) → go directly to Phase 3 Sequential.
- Complex codebase (full-stack app, multiple services, content-heavy site, monorepo, or 6+ modules) → go to Phase 2.5 first, then Phase 3 Parallel.
Phase 2.5: Module Briefs (complex codebases only)
For complex codebases, write a brief for each module before writing any HTML. This is the critical step that enables parallel writing — each brief gives an agent everything it needs without re-reading the codebase.
Read references/module-brief-template.md for the template structure. Read references/content-philosophy.md for the content rules that should guide brief writing.
For each module, write a brief to `course-name/briefs/0N-slug.md` containing:
- Teaching arc (metaphor, opening hook, key insight)
- Pre-extracted code snippets (copy-pasted from the codebase with file paths and line numbers)
- Interactive elements checklist with enough detail to build them
- Which sections of which reference files the writing agent needs
- What the previous and next modules cover (for transitions)
The code snippets are the critical token-saving step. By pre-extracting them into the brief, writing agents never need to read the codebase at all.
Phase 3: Build the Course
The course output is a directory, not a single file. All CSS and JS are pre-built reference files — never regenerate them. Your job is to write only the HTML content.
Output structure:
course-name/
styles.css ← copied verbatim from references/styles.css
main.js ← copied verbatim from references/main.js
_base.html ← customized shell (title, accent color, nav dots)
_footer.html ← copied verbatim from references/_footer.html
build.sh ← copied verbatim from references/build.sh
briefs/ ← module briefs (complex codebases only, can delete after build)
modules/
01-intro.html
02-actors.html
...
index.html ← assembled by build.sh (do not write manually)Step 1 (both paths): Setup — Create the course directory. Copy these four files verbatim using Read + Write (do not regenerate their contents):
references/styles.css→course-name/styles.cssreferences/main.js→course-name/main.jsreferences/_footer.html→course-name/_footer.htmlreferences/build.sh→course-name/build.sh
Step 2 (both paths): Customize `_base.html` — Read references/_base.html, then write it to course-name/_base.html with exactly three substitutions:
- Both instances of
COURSE_TITLE→ the actual course title - The four
ACCENT_*placeholders → the chosen accent color values (pick one palette from the comments in_base.html) NAV_DOTS→ one<button class="nav-dot" ...>per module
Step 3: Write modules — This is where the paths diverge.
Sequential path (simple codebases)
Read references/content-philosophy.md and references/gotchas.md. Then write modules one at a time. For each module, write course-name/modules/0N-slug.html containing only the <section class="module" id="module-N"> block and its contents. Do not include <html>, <head>, <body>, <style>, or <script> tags.
Read references/interactive-elements.md for HTML patterns for each interactive element type. Read references/design-system.md for visual conventions.
Parallel path (complex codebases)
When the current host supports delegated agents and the user has allowed parallel agent work, dispatch modules to subagents in batches of up to 3. Each agent receives:
- Its module brief (from
course-name/briefs/) references/content-philosophy.mdandreferences/gotchas.md- Only the sections of
references/interactive-elements.mdandreferences/design-system.mdlisted in the brief
Each agent writes its module file(s) to course-name/modules/. Short modules (3 screens, one quiz) can be paired — two briefs given to one agent.
What agents do NOT receive: the full codebase (snippets are in the brief), SKILL.md, other modules' briefs, or unneeded reference file sections.
After all agents finish, do a quick consistency check in the main context: nav dots match modules, transitions between modules are coherent, no obvious tone shifts.
Step 4 (both paths): Assemble — Run build.sh from the course directory:
cd course-name && bash build.shThis produces index.html. Open it in the browser.
Critical rules:
- Never regenerate
styles.cssormain.js— always copy from references - Module files contain only
<section>content — no boilerplate - Use CSS
scroll-snap-type: y proximity(NOTmandatory) - Use
min-height: 100dvhwith100vhfallback on.module - Interactive element JS is in
main.js; wire up viadata-*attributes and CSS class names as shown inreferences/interactive-elements.md - Chat containers need
idattributes; flow animations needdata-steps='[...]'JSON on.flow-animation
Phase 4: Review and Open
After running build.sh, open index.html in the browser. Walk the user through what was built and ask for feedback on content, design, and interactivity.
---
Design Identity
The visual design should feel like a beautiful developer notebook — warm, inviting, and distinctive. Read references/design-system.md for the full token system, but here are the non-negotiable principles:
- Warm palette: Off-white backgrounds (like aged paper), warm grays, NO cold whites or blues
- Bold accent: One confident accent color (vermillion, coral, teal — NOT purple gradients)
- Distinctive typography: Display font with personality for headings (Bricolage Grotesque, or similar bold geometric face — NEVER Inter, Roboto, Arial, or Space Grotesk). Clean sans-serif for body (DM Sans or similar). JetBrains Mono for code.
- Generous whitespace: Modules breathe. Max 3-4 short paragraphs per screen.
- Alternating backgrounds: Even/odd modules alternate between two warm background tones for visual rhythm
- Dark code blocks: IDE-style with Catppuccin-inspired syntax highlighting on deep indigo-charcoal (#1E1E2E)
- Depth without harshness: Subtle warm shadows, never black drop shadows
---
Reference Files
The references/ directory contains detailed specs. Read them only when you reach the relevant phase — not upfront. This keeps context lean.
- `references/content-philosophy.md` — Visual density rules, metaphor guidelines, quiz design, tooltip rules, code translation guidance. Read during Phase 2.5 (briefs) and Phase 3 (writing modules).
- `references/gotchas.md` — Common failure points checklist. Read during Phase 3 and Phase 4 (review).
- `references/module-brief-template.md` — Template for Phase 2.5 module briefs. Read only for complex codebases using the parallel path.
- `references/design-system.md` — Complete CSS custom properties, color palette, typography scale, spacing system, shadows, animations, scrollbar styling. Read during Phase 3 when writing module HTML.
- `references/interactive-elements.md` — Implementation patterns for every interactive element: drag-and-drop quizzes, multiple-choice quizzes, code↔English translations, group chat animations, message flow visualizations, architecture diagrams, pattern cards, callout boxes. Read the relevant sections during Phase 3.
Anti-Patterns
- Writing for the author instead of the reader: It bakes in unstated context and leaves the actual audience unsure what to do next.
- Skipping concrete examples or commands: Abstract guidance is easy to approve and hard to apply correctly.
- Letting links, screenshots, or versions drift: Polished formatting does not help if the instructions are no longer true.
<!-- PORTABILITY:START -->
Verification Protocol
Before claiming "skill applied successfully":
1. Pass/fail: The Codebase To Course output identifies audience, purpose, source of truth, and freshness requirements. 2. Pass/fail: Shared documentation-stack guidance is referenced instead of duplicating another documentation skill. 3. Pass/fail: Claims, links, commands, examples, and screenshots are verified or explicitly marked unverified. 4. Pressure-test scenario: Apply the skill to a doc request with a stale command, missing owner, and conflicting audience. 5. Success metric: Zero undocumented assumptions; every reader-facing claim is sourced or scoped.
Cross-Client Portability
This skill is written to stay usable across GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- GitHub Copilot: keep the folder in a Copilot-visible skill or plugin path, or wrap the workflow as project instructions if the host does not support portable skill folders directly.
- Claude Code: keep the folder in a local skills directory or a compatible plugin or marketplace source.
- Codex: install or sync the folder into
$CODEX_HOME/skills/<skill-name>and restart Codex after major changes. - Gemini CLI: this repository generates a project command named
/skills:codebase-to-coursefrom this skill. Rebuild commands withpython scripts/export-gemini-skill.py codebase-to-courseand then run/commands reloadinside Gemini CLI.
<!-- PORTABILITY:END -->
<!-- MCP:START -->
MCP Availability And Fallback
Preferred MCP Server: None required
- Fallback prompt: "Use the Codebase-to-Course skill without MCP. Rely on the local
SKILL.md, bundled references or scripts, and manual verification. Show the exact commands, evidence, and final checks you used before concluding." - If the current host does not expose a matching server, use the bundled references, scripts, native toolchain, and manual workflow already described in this skill.
- Treat direct local verification, rendered output, logs, tests, or screenshots as the fallback evidence path before completion.
<!-- MCP:END -->
Related Skills
- documentation-authoring: Use it when the workflow also needs drafting structured technical or product documents.
- documentation-patterns: Use it when the workflow also needs reusable documentation structures and templates.
- documentation-quality: Use it when the workflow also needs documentation review standards and quality gates.
- documentation-verification: Use it when the workflow also needs final documentation validation before publishing.
Changelog
[2026-04-25] - Version 1.2 Verification Protocol Refresh
Added
- Added a
Verification Protocolsection with skill-specific pass/fail checks, one pressure-test scenario, and a measurable success metric. - Added guidance to leverage native parallel subagent dispatch and 200k+ context windows where available.
Changed
- Updated
SKILL.mdfrontmatter toversion: "1.2"andlast_updated: 2026-04-25. - Reframed activation guidance toward symptom -> action triggers and standardized two-stage review wording where applicable.
[2026-04-24] - Version 1.1 Refresh
Changed
- Updated the SKILL frontmatter version to
1.1for the 2026-04-24 catalog refresh.
All notable changes to the codebase-to-course skill will be documented in this file.
[2026-04-24] - Skill Refresh
Changed
- Standardized the SKILL frontmatter with version metadata, last-updated date, tags, and a concise catalog description.
- Reformatted the portability and MCP guidance with a preferred server line, a copy-paste fallback prompt, and consistent bullet lists.
- Added a catalog-standard Anti-Patterns section and refreshed the Related Skills links at the end of the skill.
[2026-04-24] - Initial Import and Portability Upgrade
Added
- Imported the skill from
https://github.com/zarazhangrui/codebase-to-courseatff8837ecf8e9f6ce9874ffa42e42633394a52a00. - Imported the upstream
references/course template assets used by the skill. - Added cross-client portability guidance for GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- Added the repo-standard no-MCP fallback guidance for this skill.
Changed
- Clarified that parallel delegated-agent module writing depends on host support and user approval.
Tested
- Validated
SKILL.mdfrontmatter and Gemini command export readiness withpython scripts/validate-skills.py.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>COURSE_TITLE</title>
<!-- Google Fonts: Bricolage Grotesque · DM Sans · JetBrains Mono -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400;1,9..40,500&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<!--
CUSTOMIZE PER COURSE — change only the three accent variables below.
Options:
vermillion #D94F30 / #C4432A / #FDEEE9 / #E8836C (default)
coral #E06B56 / #C85A47 / #FDECEA / #E89585
teal #2A7B9B / #1F6280 / #E4F2F7 / #5A9DB8
amber #D4A843 / #BF9530 / #FDF5E0 / #E0C070
forest #2D8B55 / #226B41 / #E8F5EE / #5AAD7A
-->
<style>
:root {
--color-accent: ACCENT_COLOR;
--color-accent-hover: ACCENT_HOVER;
--color-accent-light: ACCENT_LIGHT;
--color-accent-muted: ACCENT_MUTED;
}
</style>
<script src="main.js" defer></script>
</head>
<body>
<nav class="nav" id="nav">
<div class="progress-bar" id="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<div class="nav-inner">
<span class="nav-title">COURSE_TITLE</span>
<div class="nav-dots" id="nav-dots" role="tablist">
<!--
One <button> per module. Replace MODULE_N_NAME with the actual module title.
<button class="nav-dot" data-target="module-1" data-tooltip="MODULE_1_NAME" role="tab" aria-label="Module 1: MODULE_1_NAME"></button>
<button class="nav-dot" data-target="module-2" data-tooltip="MODULE_2_NAME" role="tab" aria-label="Module 2: MODULE_2_NAME"></button>
...
-->
NAV_DOTS
</div>
</div>
</nav>
<main id="main">
<!-- modules/*.html content is assembled here by build.sh -->
</main>
</body>
</html>
#!/bin/bash
# Assembles the course from parts.
# Run from the course directory: bash build.sh
set -e
cat _base.html modules/*.html _footer.html > index.html
echo "Built index.html — open it in your browser."
Content Philosophy
When to read this: During Phase 2.5 (writing module briefs) and Phase 3 (writing module HTML). These principles guide every content decision — what to show, how to explain it, and how to test understanding.
These principles are what separate a great course from a generic tutorial. They should guide every content decision:
Show, Don't Tell — Aggressively Visual
People's eyes glaze over text blocks. The course should feel closer to an infographic than a textbook. Follow these hard rules:
Text limits:
- Max 2-3 sentences per text block. If you're writing a fourth sentence, stop and convert it into a visual instead.
- No text block should ever be wider than the content width AND taller than ~4 lines. If it is, break it up with a visual element.
- Every screen must be at least 50% visual (diagrams, code blocks, cards, animations, badges — anything that isn't a paragraph).
Convert text to visuals:
- A list of 3+ items → cards with icons (pattern cards, feature cards)
- A sequence of steps → flow diagram with arrows or numbered step cards
- "Component A talks to Component B" → animated data flow or group chat visualization
- "This file does X, that file does Y" → visual file tree with annotations or icon + one-liner badges
- Explaining what code does → code↔English translation block (not a paragraph about the code)
- Comparing two approaches → side-by-side columns with visual contrast
Visual breathing room:
- Use generous spacing between elements (
--space-8to--space-12between sections) - Alternate between full-width visuals and narrow text blocks to create rhythm
- Every module should have at least one "hero visual" — a diagram, animation, or interactive element that dominates the screen and teaches the core concept at a glance
Code ↔ English Translations
Every code snippet gets a side-by-side plain English translation. Left panel: real code from the project with syntax highlighting. Right panel: line-by-line plain English explaining what each line does. This is the single most valuable teaching tool for non-technical learners.
Critical: No horizontal scrollbars on code. All code must use white-space: pre-wrap so it wraps instead of scrolling. This is a course for non-technical people, not an IDE — readability beats preserving indentation structure.
Critical: Use original code exactly as-is. Never modify, simplify, or trim code snippets from the codebase. The learner should be able to open the real file and see the exact same code they learned from — that builds trust. Instead of editing code to make it shorter, choose naturally short, punchy snippets (5-10 lines) from the codebase that illustrate the concept well. Every codebase has compact, self-contained moments — find those rather than butchering longer functions.
One Concept Per Screen
No walls of text. Each screen within a module teaches exactly one idea. If you need more space, add another screen — don't cram.
Metaphors First, Then Reality
Introduce every new concept with a metaphor from everyday life. Then immediately ground it: "In our code, this looks like..." The metaphor builds intuition; the code grounds it in reality.
Critical: No recycled metaphors. Do NOT default to "restaurant" for everything — that's the #1 crutch. Each concept deserves its own metaphor that feels natural to that specific idea. A database is a library with a card catalog. Auth is a bouncer checking IDs. An event loop is an air traffic controller. Message passing is a postal system. API rate limiting is a nightclub with a capacity limit. Pick the metaphor that makes the concept click, not the one that's easiest to reach for. If you catch yourself using "restaurant" or "kitchen" more than once in a course, stop and rethink.
Learn by Tracing
Follow what actually happens when the learner does something they already do every day in the app — trace the data flow end-to-end. "You know that button you click? Here's the journey your data takes after you click it..." This works because the learner has already experienced the result — now they're seeing the machinery behind it. It's like watching a behind-the-scenes documentary of a movie you loved.
Make It Memorable
Use "aha!" callout boxes for universal CS insights. Use humor where natural (not forced). Give components personality — they're "characters" in a story, not abstract boxes on a diagram.
Glossary Tooltips — No Term Left Behind
Every technical term (API, DOM, callback, middleware, etc.) gets a dashed-underline tooltip on first use in each module. Hover on desktop or tap on mobile to see a 1-2 sentence plain-English definition. The learner should never have to leave the page to Google a term. This is the difference between a course that says it's for non-technical people and one that actually is.
Be extremely aggressive with tooltips. If there is even a 1% chance a non-technical person doesn't know a word, tooltip it. This includes:
- Software names they might not know (Blender, GIMP, Audacity, etc.)
- Everyday developer terms (REPL, JSON, flag, CLI, API, SDK, etc.)
- Programming concepts (function, variable, dictionary, class, module, etc.)
- Infrastructure terms (PATH, pip, namespace, entry point, etc.)
- Acronyms — ALWAYS tooltip acronyms on first use
The vocabulary IS the learning. One of the key goals is for learners to acquire the precise technical vocabulary they need to communicate with AI coding agents. Each tooltip should teach the term in a way that helps the learner USE it in their own instructions — e.g., "A flag is an option you add to a command to change its behavior — like adding '--json' to get structured data instead of plain text. When talking to AI, you'd say 'add a flag for verbose output.'"
Cursor: Use cursor: pointer on terms (not cursor: help). The question-mark cursor feels clinical — a pointer feels clickable and inviting.
Tooltip overflow fix: Translation blocks and other containers with overflow: hidden will clip tooltips. To fix this, the tooltip JS must use position: fixed and calculate coordinates from getBoundingClientRect() instead of relying on CSS position: absolute within the container. Append tooltips to document.body rather than inside the term element. This ensures tooltips are never clipped by any ancestor's overflow.
Quizzes That Test Application, Not Memory
The goal of learning is practical application — being able to do something with what you learned. Quizzes should test whether the learner can use their knowledge to solve a new problem, not whether they can regurgitate a definition.
What to quiz (in order of value): 1. "What would you do?" scenarios — Present a new situation the learner hasn't seen and ask them to apply what they learned. e.g., "You want to add a 'save to favorites' feature. Which files would you need to change?" This is the gold standard. 2. Debugging scenarios — "A user reports X is broken. Based on what you learned, where would you look first?" This tests whether they understood the architecture, not just memorized file names. 3. Architecture decisions — "You're building a similar app from scratch. Would you put this logic in the frontend or backend? Why?" Tests whether they understood the reasoning behind design choices. 4. Tracing exercises — "When a user does X, trace the path the data takes." Tests whether they can follow the flow.
What NOT to quiz:
- Definitions ("What does API stand for?") — that's what the glossary tooltips are for
- File name recall ("Which file handles X?") — nobody memorizes file names
- Syntax details ("What's the correct way to write a fetch call?") — this isn't a coding bootcamp
- Anything that can be answered by scrolling up and copying — that tests scrolling, not understanding
Quiz tone:
- Wrong answers get encouraging, non-judgmental explanations ("Not quite — here's why...")
- Correct answers get brief reinforcement of the underlying principle ("Exactly! This works because...")
- Never punitive, never score-focused. No "You got 3/5!" — the quiz is a thinking exercise, not an exam
- Wrong answer explanations should teach something new, not just say "wrong, the answer was B"
How many quizzes: One per module, placed at the end after the learner has seen all the content. 3-5 questions per quiz. Each question should make the learner pause and think, not just pick the obvious answer.
Deciding what concepts are worth quizzing: Quiz the things that would actually help someone in practice — architecture understanding ("where does this logic live and why?"), debugging intuition ("what would cause this symptom?"), and decision-making ("what's the tradeoff here?"). If a concept won't help someone debug a problem, steer an AI assistant, or make an architectural decision, it's not worth quizzing.
Design System Reference
Complete CSS design tokens for the course. Copy this entire :root block into the course HTML and adapt the accent color to suit the project's personality.
Table of Contents
1. Color Palette 2. Typography 3. Spacing & Layout 4. Shadows & Depth 5. Animations & Transitions 6. Navigation & Progress 7. Module Structure 8. Responsive Breakpoints 9. Scrollbar & Background
---
Color Palette
:root {
/* --- BACKGROUNDS --- */
--color-bg: #FAF7F2; /* warm off-white, like aged paper */
--color-bg-warm: #F5F0E8; /* slightly warmer for alternating modules */
--color-bg-code: #1E1E2E; /* deep indigo-charcoal for code blocks */
--color-text: #2C2A28; /* dark charcoal, easy on eyes */
--color-text-secondary: #6B6560; /* warm gray for secondary text */
--color-text-muted: #9E9790; /* muted for timestamps, labels */
--color-border: #E5DFD6; /* subtle warm border */
--color-border-light: #EEEBE5; /* even lighter border */
--color-surface: #FFFFFF; /* card surfaces */
--color-surface-warm: #FDF9F3; /* warm card surface */
/* --- ACCENT (adapt per project — pick ONE bold color) ---
Default: vermillion. Alternatives: coral (#E06B56), teal (#2A7B9B),
amber (#D4A843), forest (#2D8B55). Avoid purple gradients. */
--color-accent: #D94F30;
--color-accent-hover: #C4432A;
--color-accent-light: #FDEEE9;
--color-accent-muted: #E8836C;
/* --- SEMANTIC --- */
--color-success: #2D8B55;
--color-success-light: #E8F5EE;
--color-error: #C93B3B;
--color-error-light: #FDE8E8;
--color-info: #2A7B9B;
--color-info-light: #E4F2F7;
/* --- ACTOR COLORS (assign to main components) ---
Each major "character" in the codebase gets a distinct color
for chat bubbles, diagrams, and highlights */
--color-actor-1: #D94F30; /* vermillion */
--color-actor-2: #2A7B9B; /* teal */
--color-actor-3: #7B6DAA; /* muted plum */
--color-actor-4: #D4A843; /* golden */
--color-actor-5: #2D8B55; /* forest */
}Rules:
- Even-numbered modules use
--color-bg, odd-numbered use--color-bg-warm(alternating backgrounds create visual rhythm) - Actor colors should be visually distinct from each other and from the accent
- Code blocks always use
--color-bg-codewith light text
---
Typography
:root {
/* --- FONTS ---
Display: bold, geometric, personality-driven. NOT Inter/Roboto/Arial.
Body: readable with character. NOT system fonts.
Mono: developer-friendly with clear character distinction. */
--font-display: 'Bricolage Grotesque', Georgia, serif;
--font-body: 'DM Sans', -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
/* --- TYPE SCALE (1.25 ratio) --- */
--text-xs: 0.75rem; /* 12px — labels, badges */
--text-sm: 0.875rem; /* 14px — secondary text, code */
--text-base: 1rem; /* 16px — body text */
--text-lg: 1.125rem; /* 18px — lead paragraphs */
--text-xl: 1.25rem; /* 20px — screen headings */
--text-2xl: 1.5rem; /* 24px — sub-module titles */
--text-3xl: 1.875rem; /* 30px — module subtitles */
--text-4xl: 2.25rem; /* 36px — module titles */
--text-5xl: 3rem; /* 48px — hero text */
--text-6xl: 3.75rem; /* 60px — module numbers */
/* --- LINE HEIGHTS --- */
--leading-tight: 1.15; /* headings */
--leading-snug: 1.3; /* subheadings */
--leading-normal: 1.6; /* body text */
--leading-loose: 1.8; /* relaxed reading */
}Google Fonts link (put in `<head>`):
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400;1,9..40,500&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">Rules:
- Module numbers:
--text-6xl, font-display, weight 800,--color-accentwith 15% opacity - Module titles:
--text-4xl, font-display, weight 700 - Screen headings:
--text-xlor--text-2xl, font-display, weight 600 - Body text:
--text-baseor--text-lg, font-body,--leading-normal - Code:
--text-sm, font-mono - Labels/badges:
--text-xs, font-mono, uppercase, letter-spacing 0.05em
---
Spacing & Layout
:root {
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
--space-24: 6rem; /* 96px */
--content-width: 800px; /* standard reading width */
--content-width-wide: 1000px; /* for side-by-side layouts */
--nav-height: 50px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-full: 9999px;
}Module layout:
.module {
min-height: 100dvh; /* fallback: 100vh */
scroll-snap-align: start;
padding: var(--space-16) var(--space-6);
padding-top: calc(var(--nav-height) + var(--space-12));
}
.module-content {
max-width: var(--content-width);
margin: 0 auto;
}---
Shadows & Depth
:root {
--shadow-sm: 0 1px 2px rgba(44, 42, 40, 0.05);
--shadow-md: 0 4px 12px rgba(44, 42, 40, 0.08);
--shadow-lg: 0 8px 24px rgba(44, 42, 40, 0.1);
--shadow-xl: 0 16px 48px rgba(44, 42, 40, 0.12);
}Use warm-tinted RGBA (44, 42, 40) — never pure black shadows.
---
Animations & Transitions
:root {
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
--stagger-delay: 120ms;
}Scroll-triggered reveal pattern:
.animate-in {
opacity: 0;
transform: translateY(20px);
transition: opacity var(--duration-slow) var(--ease-out),
transform var(--duration-slow) var(--ease-out);
}
.animate-in.visible {
opacity: 1;
transform: translateY(0);
}
/* Stagger children */
.stagger-children > .animate-in {
transition-delay: calc(var(--stagger-index, 0) * var(--stagger-delay));
}JS setup for stagger:
document.querySelectorAll('.stagger-children').forEach(parent => {
Array.from(parent.children).forEach((child, i) => {
child.style.setProperty('--stagger-index', i);
});
});Intersection Observer (trigger reveals):
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target); // animate only once
}
});
}, { rootMargin: '0px 0px -10% 0px', threshold: 0.1 });
document.querySelectorAll('.animate-in').forEach(el => observer.observe(el));---
Navigation & Progress
HTML structure:
<nav class="nav">
<div class="progress-bar" role="progressbar" aria-valuenow="0"></div>
<div class="nav-inner">
<span class="nav-title">Course Title</span>
<div class="nav-dots">
<button class="nav-dot" data-target="module-1" data-tooltip="Module 1 Name"
role="tab" aria-label="Module 1"></button>
<!-- one per module -->
</div>
</div>
</nav>Progress bar (CSS-only where possible, JS fallback):
function updateProgressBar() {
const scrollTop = window.scrollY;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (scrollTop / scrollHeight) * 100;
progressBar.style.width = progress + '%';
}
window.addEventListener('scroll', () => {
requestAnimationFrame(updateProgressBar);
}, { passive: true });Nav dot states:
- Default:
border: 2px solid var(--color-text-muted), empty center - Current:
border-color: var(--color-accent), filled center, subtle glow shadow - Visited:
background: var(--color-accent), filled solid
Keyboard navigation:
document.addEventListener('keydown', (e) => {
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName)) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { nextModule(); e.preventDefault(); }
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { prevModule(); e.preventDefault(); }
});---
Module Structure
HTML template for each module:
<section class="module" id="module-N" style="background: var(--color-bg or --color-bg-warm)">
<div class="module-content">
<header class="module-header animate-in">
<span class="module-number">0N</span>
<h1 class="module-title">Module Title</h1>
<p class="module-subtitle">One-line description of what this module teaches</p>
</header>
<div class="module-body">
<section class="screen animate-in">
<h2 class="screen-heading">Screen Title</h2>
<p>Content...</p>
<!-- Interactive elements, code translations, etc. -->
</section>
<section class="screen animate-in">
<!-- Next screen -->
</section>
</div>
</div>
</section>---
Responsive Breakpoints
/* Tablet */
@media (max-width: 768px) {
:root {
--text-4xl: 1.875rem;
--text-5xl: 2.25rem;
--text-6xl: 3rem;
}
.translation-block { grid-template-columns: 1fr; } /* stack code/english */
.pattern-cards { grid-template-columns: 1fr 1fr; }
}
/* Mobile */
@media (max-width: 480px) {
:root {
--text-4xl: 1.5rem;
--text-5xl: 1.875rem;
--text-6xl: 2.25rem;
}
.module { padding: var(--space-8) var(--space-4); }
.pattern-cards { grid-template-columns: 1fr; }
.flow-steps { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); }
}---
Scrollbar & Background
/* Custom scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: var(--radius-full);
}
/* Subtle atmospheric background */
body {
background: var(--color-bg);
background-image: radial-gradient(
ellipse at 20% 50%,
rgba(217, 79, 48, 0.03) 0%,
transparent 50%
);
}
/* Page scroll setup */
html {
scroll-snap-type: y proximity;
scroll-behavior: smooth;
}---
Code Block Globals
All code blocks in the course — whether inside translation blocks, standalone snippets, or quiz challenges — must wrap text and never show a horizontal scrollbar. This is a teaching tool, not an IDE.
pre, code {
white-space: pre-wrap; /* wrap long lines */
word-break: break-word; /* break mid-word if absolutely needed */
overflow-x: hidden; /* no horizontal scrollbar — ever */
}
/* Hide scrollbars on code containers */
.translation-code::-webkit-scrollbar,
pre::-webkit-scrollbar {
display: none;
}Code snippets must be exact copies from the real codebase — never modified, trimmed, or simplified. Instead, choose naturally short (5-10 line) sections from the code that illustrate the concept well. If a longer block is needed, show it all — the wrapping CSS will handle readability.
---
Syntax Highlighting (Catppuccin-inspired)
For code blocks on the dark --color-bg-code background:
.code-keyword { color: #CBA6F7; } /* purple — if, else, return, function */
.code-string { color: #A6E3A1; } /* green — "strings" */
.code-function { color: #89B4FA; } /* blue — function names */
.code-comment { color: #6C7086; } /* muted gray — // comments */
.code-number { color: #FAB387; } /* peach — numbers */
.code-property { color: #F9E2AF; } /* yellow — object keys */
.code-operator { color: #94E2D5; } /* teal — =, =>, +, etc. */
.code-tag { color: #F38BA8; } /* pink — HTML tags */
.code-attr { color: #F9E2AF; } /* yellow — HTML attributes */
.code-value { color: #A6E3A1; } /* green — attribute values */Gotchas — Common Failure Points
When to read this: During Phase 3 (writing module HTML) and Phase 4 (review). Check every one of these before considering a course complete.
These are real problems encountered when building courses. Check every one before considering a course complete.
Tooltip Clipping
Translation blocks use overflow: hidden for code wrapping. If tooltips use position: absolute inside the term element, they get clipped by the container. Fix: Tooltips must use position: fixed and be appended to document.body. Calculate position from getBoundingClientRect(). This is already handled by main.js but is the #1 bug that appears in every build.
Not Enough Tooltips
The most common failure is under-tooltipping. Non-technical learners don't know terms like REPL, JSON, flag, entry point, PATH, pip, namespace, function, class, module, PR, E2E, or even software names like Blender/GIMP. Rule of thumb: if a term wouldn't appear in everyday conversation with a non-technical friend, tooltip it. Err heavily on the side of too many. BUT: don't tooltip terms the user already knows well from their domain (e.g., AI/ML concepts for someone in AI).
Walls of Text
The course looks like a textbook instead of an infographic. This happens when you write more than 2-3 sentences in a row without a visual break. Every screen must be at least 50% visual. Convert any list of 3+ items into cards, any sequence into step cards or flow diagrams, any code explanation into a code↔English translation block.
Recycled Metaphors
Using "restaurant" or "kitchen" for everything. Every module needs its own metaphor that feels inevitable for that specific concept. If you catch yourself reaching for the same metaphor twice, stop and find one that fits the concept organically.
Code Modifications
Trimming, simplifying, or "cleaning up" code snippets from the codebase. The learner should be able to open the real file and see the exact same code. Instead of editing code to be shorter, choose naturally short snippets (5-10 lines) from the codebase that illustrate the point.
Quiz Questions That Test Memory
Asking "What does API stand for?" or "Which file handles X?" — those test recall, not understanding. Every quiz question should present a new scenario the learner hasn't seen and ask them to apply what they learned.
Scroll-Snap Mandatory
Using scroll-snap-type: y mandatory traps users inside long modules. Always use proximity.
Module Quality Degradation
Trying to write all modules in one pass causes later modules to be thin and rushed. Build one module at a time and verify each before moving on. For complex codebases, use the parallel path with module briefs.
Missing Interactive Elements
A module with only text and code blocks, no interactivity. Every module needs at least one of: quiz, data flow animation, group chat, architecture diagram, drag-and-drop. These aren't decorations — they're how non-technical learners actually process information.
Interactive Elements Reference
Implementation patterns for every interactive element type used in courses. Pick the elements that best serve each module's teaching goal.
Architecture note: All CSS and JavaScript for these elements live inreferences/styles.cssandreferences/main.js, which are copied verbatim into every course directory. When writing module HTML files, use only the HTML patterns below — do not inline<style>or<script>tags for these elements. The engines inmain.jsauto-initialize on page load by scanning for the relevant class names anddata-*attributes described here.
Table of Contents
1. Code ↔ English Translation Blocks 2. Multiple-Choice Quizzes 3. Drag-and-Drop Matching 4. Group Chat Animation 5. Message Flow / Data Flow Animation 6. Interactive Architecture Diagram 7. Layer Toggle Demo 8. "Spot the Bug" Challenge 9. Scenario Quiz 10. Callout Boxes 11. Pattern/Feature Cards 12. Flow Diagrams 13. Permission/Config Badges 14. Glossary Tooltips 15. Visual File Tree 16. Icon-Label Rows 17. Numbered Step Cards
---
Code ↔ English Translation Blocks
The most important teaching element. Shows real code from the project on the left and a plain English translation on the right, line by line.
HTML:
<div class="translation-block animate-in">
<div class="translation-code">
<span class="translation-label">CODE</span>
<pre><code>
<span class="code-line"><span class="code-keyword">const</span> response = <span class="code-keyword">await</span> <span class="code-function">fetch</span>(url, {</span>
<span class="code-line"> <span class="code-property">method</span>: <span class="code-string">'POST'</span>,</span>
<span class="code-line"> <span class="code-property">headers</span>: { <span class="code-string">'Authorization'</span>: apiKey }</span>
<span class="code-line">});</span>
</code></pre>
</div>
<div class="translation-english">
<span class="translation-label">PLAIN ENGLISH</span>
<div class="translation-lines">
<p class="tl">Send a request to the URL and wait for a response...</p>
<p class="tl">We're sending data (POST), not just asking for it (GET)...</p>
<p class="tl">Include our API key so the server knows who we are...</p>
<p class="tl">End of the request setup.</p>
</div>
</div>
</div>CSS:
.translation-block {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-md);
margin: var(--space-8) 0;
}
.translation-code {
background: var(--color-bg-code);
color: #CDD6F4;
padding: var(--space-6);
font-family: var(--font-mono);
font-size: var(--text-sm);
line-height: 1.7;
position: relative;
overflow-x: hidden; /* NO horizontal scrollbar — ever */
}
.translation-code pre,
.translation-code code {
white-space: pre-wrap; /* wrap long lines instead of scrolling */
word-break: break-word; /* break mid-word if needed */
overflow-x: hidden;
}
.translation-english {
background: var(--color-surface-warm);
padding: var(--space-6);
font-size: var(--text-sm);
line-height: 1.7;
border-left: 3px solid var(--color-accent);
}
.translation-label {
position: absolute;
top: var(--space-2);
right: var(--space-3);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.1em;
opacity: 0.5;
}
.translation-english .translation-label {
color: var(--color-text-muted);
}
/* Responsive: stack vertically on mobile */
@media (max-width: 768px) {
.translation-block { grid-template-columns: 1fr; }
.translation-english { border-left: none; border-top: 3px solid var(--color-accent); }
}Rules:
- Each English line should correspond to 1-2 code lines
- Use conversational language, not technical jargon
- Highlight the "why" not just the "what" — e.g., "Include our API key so the server knows who we are" not "Set the Authorization header"
---
Multiple-Choice Quizzes
For testing understanding with instant feedback. Each question has options, one correct answer, and per-question explanations.
Wiring: main.js exposes window.selectOption(btn), window.checkQuiz(containerId), and window.resetQuiz(containerId). Call them via onclick. Per-question explanations go in data-explanation-right and data-explanation-wrong on the .quiz-question-block.
HTML:
<div class="quiz-container" id="quiz-module3">
<div class="quiz-question-block"
data-correct="option-b"
data-explanation-right="Exactly — because X is responsible for Y in this architecture."
data-explanation-wrong="Not quite. Think about where Y lives in the codebase...">
<h3 class="quiz-question">Question text here?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div>
<span>Answer A</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div>
<span>Answer B (correct)</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div>
<span>Answer C</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-module3')">Check Answers</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-module3')">Try Again</button>
</div>CSS for quiz states:
.quiz-option {
display: flex; align-items: center; gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
cursor: pointer; width: 100%;
transition: border-color var(--duration-fast), background var(--duration-fast);
}
.quiz-option:hover { border-color: var(--color-accent-muted); }
.quiz-option.selected { border-color: var(--color-accent); background: var(--color-accent-light); }
.quiz-option.correct { border-color: var(--color-success); background: var(--color-success-light); }
.quiz-option.incorrect { border-color: var(--color-error); background: var(--color-error-light); }
.quiz-option-radio {
width: 18px; height: 18px; border-radius: 50%;
border: 2px solid var(--color-border);
transition: all var(--duration-fast);
}
.quiz-option.selected .quiz-option-radio {
border-color: var(--color-accent);
background: var(--color-accent);
box-shadow: inset 0 0 0 3px white;
}
.quiz-feedback {
max-height: 0; overflow: hidden; opacity: 0;
transition: max-height var(--duration-normal), opacity var(--duration-normal);
}
.quiz-feedback.show { max-height: 200px; opacity: 1; padding: var(--space-3); margin-top: var(--space-2); border-radius: var(--radius-sm); }
.quiz-feedback.success { background: var(--color-success-light); color: var(--color-success); }
.quiz-feedback.error { background: var(--color-error-light); color: var(--color-error); }---
Drag-and-Drop Matching
For matching concepts to descriptions. Supports both mouse (HTML5 Drag API) and touch.
HTML:
<div class="dnd-container">
<div class="dnd-chips">
<div class="dnd-chip" draggable="true" data-answer="actor-a">Actor A</div>
<div class="dnd-chip" draggable="true" data-answer="actor-b">Actor B</div>
<div class="dnd-chip" draggable="true" data-answer="actor-c">Actor C</div>
</div>
<div class="dnd-zones">
<div class="dnd-zone" data-correct="actor-a">
<p class="dnd-zone-label">Description for Actor A</p>
<div class="dnd-zone-target">Drop here</div>
</div>
<!-- more zones -->
</div>
<button onclick="checkDnD()">Check Matches</button>
<button onclick="resetDnD()">Reset</button>
</div>JS (mouse + touch):
// MOUSE: HTML5 Drag API
chips.forEach(chip => {
chip.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', chip.dataset.answer);
chip.classList.add('dragging');
});
chip.addEventListener('dragend', () => chip.classList.remove('dragging'));
});
zones.forEach(zone => {
const target = zone.querySelector('.dnd-zone-target');
target.addEventListener('dragover', (e) => { e.preventDefault(); target.classList.add('drag-over'); });
target.addEventListener('dragleave', () => target.classList.remove('drag-over'));
target.addEventListener('drop', (e) => {
e.preventDefault();
target.classList.remove('drag-over');
const answer = e.dataTransfer.getData('text/plain');
const chip = document.querySelector(`[data-answer="${answer}"]`);
target.textContent = chip.textContent;
target.dataset.placed = answer;
chip.classList.add('placed');
});
});
// TOUCH: Custom implementation (HTML5 drag doesn't work on mobile)
chips.forEach(chip => {
chip.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
const clone = chip.cloneNode(true);
clone.classList.add('touch-ghost');
clone.style.cssText = `position:fixed; z-index:1000; pointer-events:none;
left:${touch.clientX - 40}px; top:${touch.clientY - 20}px;`;
document.body.appendChild(clone);
chip._ghost = clone;
chip._answer = chip.dataset.answer;
}, { passive: false });
chip.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
if (chip._ghost) {
chip._ghost.style.left = (touch.clientX - 40) + 'px';
chip._ghost.style.top = (touch.clientY - 20) + 'px';
}
// Highlight zone under finger
const el = document.elementFromPoint(touch.clientX, touch.clientY);
zones.forEach(z => z.querySelector('.dnd-zone-target').classList.remove('drag-over'));
if (el && el.closest('.dnd-zone-target')) {
el.closest('.dnd-zone-target').classList.add('drag-over');
}
}, { passive: false });
chip.addEventListener('touchend', (e) => {
if (chip._ghost) { chip._ghost.remove(); chip._ghost = null; }
const touch = e.changedTouches[0];
const el = document.elementFromPoint(touch.clientX, touch.clientY);
if (el && el.closest('.dnd-zone-target')) {
const target = el.closest('.dnd-zone-target');
target.textContent = chip.textContent;
target.dataset.placed = chip._answer;
chip.classList.add('placed');
}
});
});---
Group Chat Animation
iMessage/WeChat-style chat showing components "talking" to each other. Messages appear one by one with typing indicators.
Wiring: main.js auto-initializes every .chat-window on page load. Give each chat window a unique id. Control buttons need these classes: .chat-next-btn, .chat-all-btn, .chat-reset-btn. The typing indicator avatar element should have id="{chatWindowId}-typing-avatar" or simply be the first .chat-avatar inside .chat-typing.
HTML:
<div class="chat-window" id="chat-module2">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="actor-a" style="display:none">
<div class="chat-avatar" style="background: var(--color-actor-1)">A</div>
<div class="chat-bubble">
<span class="chat-sender" style="color: var(--color-actor-1)">Actor A</span>
<p>Hey Background, I need the data for this item.</p>
</div>
</div>
<!-- more messages... -->
</div>
<div class="chat-typing" id="chat-typing" style="display:none">
<div class="chat-avatar" id="typing-avatar">?</div>
<div class="chat-typing-dots">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</div>
</div>
<div class="chat-controls">
<button class="btn chat-next-btn">Next Message</button>
<button class="btn chat-all-btn">Play All</button>
<button class="btn chat-reset-btn">Replay</button>
<span class="chat-progress"></span>
</div>
</div>CSS for typing dots:
.typing-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--color-text-muted);
animation: typingBounce 1.4s infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingBounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-6px); }
}---
Message Flow / Data Flow Animation
Step-by-step visualization of data moving between components. User clicks "Next Step" to advance.
Wiring: main.js auto-initializes every .flow-animation on page load. Pass steps as JSON in data-steps. Each step object: { highlight: "flow-actor-id", label: "description", packet: true, from: "actor-id-suffix", to: "actor-id-suffix" }. Actor element IDs must be flow-actor-1, flow-actor-2, etc. Control buttons need classes .flow-next-btn and .flow-reset-btn.
⚠️ Single quotes in step labels will break parsing. Thedata-stepsattribute is delimited by single quotes (data-steps='[...]'), so any single quote inside a label (e.g."the user's request") will terminate the attribute early and causeJSON.parseto fail silently — the entire animation will stop working. Either avoid apostrophes in labels, replace them with', or rewrite the attribute using double-quote delimiters with escaped inner quotes (data-steps="[{\"label\":\"...\"}]").
HTML:
<div class="flow-animation" data-steps='[
{"highlight":"flow-actor-1","label":"User clicks the button"},
{"highlight":"flow-actor-1","label":"Frontend sends request","packet":true,"from":"actor-1","to":"actor-2"},
{"highlight":"flow-actor-2","label":"Backend calls the database","packet":true,"from":"actor-2","to":"actor-3"}
]'>
<div class="flow-actors">
<div class="flow-actor" id="flow-actor-1">
<div class="flow-actor-icon">A</div>
<span>Actor 1</span>
</div>
<div class="flow-actor" id="flow-actor-2">
<div class="flow-actor-icon">B</div>
<span>Actor 2</span>
</div>
<div class="flow-actor" id="flow-actor-3">
<div class="flow-actor-icon">C</div>
<span>Actor 3</span>
</div>
</div>
<div class="flow-packet" id="flow-packet"></div>
<div class="flow-step-label" id="flow-label">Click "Next Step" to begin</div>
<div class="flow-controls">
<button class="btn flow-next-btn">Next Step</button>
<button class="btn flow-reset-btn">Restart</button>
<span class="flow-progress"></span>
</div>
</div>CSS for active actor glow:
.flow-actor.active {
box-shadow: 0 0 0 3px var(--color-accent), 0 0 20px rgba(217, 79, 48, 0.2);
transform: scale(1.05);
transition: all var(--duration-normal) var(--ease-out);
}---
Interactive Architecture Diagram
Full-system diagram where hovering/clicking a component shows a description tooltip.
HTML:
<div class="arch-diagram">
<div class="arch-zone arch-zone-browser">
<h4 class="arch-zone-label">Browser</h4>
<div class="arch-component" data-desc="Injects UI into the web page, reads DOM, captures user actions"
onclick="showArchDesc(this)">
<div class="arch-icon">📄</div>
<span>Component A</span>
</div>
<!-- more components -->
</div>
<div class="arch-zone arch-zone-external">
<h4 class="arch-zone-label">External Services</h4>
<!-- API cards -->
</div>
<div class="arch-description" id="arch-desc">Click any component to learn what it does</div>
</div>---
Layer Toggle Demo
Shows how different layers (e.g., HTML/CSS/JS, or data/logic/UI) build on each other. Three tabs switch between views.
HTML:
<div class="layer-demo">
<div class="layer-tabs">
<button class="layer-tab active" onclick="showLayer('html')">HTML</button>
<button class="layer-tab" onclick="showLayer('css')">+ CSS</button>
<button class="layer-tab" onclick="showLayer('js')">+ JS</button>
</div>
<div class="layer-viewport">
<div class="layer" id="layer-html" style="display:block">
<!-- Raw unstyled version -->
</div>
<div class="layer" id="layer-css" style="display:none">
<!-- Styled version -->
</div>
<div class="layer" id="layer-js" style="display:none">
<!-- Interactive version -->
</div>
</div>
<p class="layer-description" id="layer-desc">This is the raw HTML...</p>
</div>---
"Spot the Bug" Challenge
Show code with a deliberate bug. User clicks the buggy line. Reveal explains the issue.
HTML:
<div class="bug-challenge">
<h3>Find the bug in this code:</h3>
<div class="bug-code">
<div class="bug-line" data-line="1" onclick="checkBugLine(this, false)">
<span class="line-num">1</span>
<code>chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {</code>
</div>
<div class="bug-line" data-line="2" onclick="checkBugLine(this, false)">
<span class="line-num">2</span>
<code> if (msg.action === 'fetchData') {</code>
</div>
<div class="bug-line bug-target" data-line="3" onclick="checkBugLine(this, true)">
<span class="line-num">3</span>
<code> fetch(url).then(r => r.json()).then(data => sendResponse(data));</code>
</div>
<div class="bug-line" data-line="4" onclick="checkBugLine(this, false)">
<span class="line-num">4</span>
<code> }</code>
</div>
<div class="bug-line" data-line="5" onclick="checkBugLine(this, false)">
<span class="line-num">5</span>
<code>});</code>
</div>
</div>
<div class="bug-feedback" id="bug-feedback"></div>
</div>JS:
window.checkBugLine = function(el, isCorrect) {
const feedback = el.closest('.bug-challenge').querySelector('.bug-feedback');
if (isCorrect) {
el.classList.add('correct');
feedback.innerHTML = '<strong>Found it!</strong> The listener uses an async operation (fetch) but doesn\'t return true. Chrome closes the message channel before the response can be sent. Fix: add <code>return true;</code> at the end.';
feedback.className = 'bug-feedback show success';
} else {
el.classList.add('incorrect');
feedback.innerHTML = 'Not this line — look for where the async timing might cause problems...';
feedback.className = 'bug-feedback show error';
setTimeout(() => { el.classList.remove('incorrect'); feedback.className = 'bug-feedback'; }, 2000);
}
};---
Scenario Quiz
"What would a senior engineer do?" — situational questions with explanations.
Same HTML/CSS/JS pattern as Multiple-Choice Quizzes, but with longer scenario descriptions and more detailed explanations. Wrap each question in a scenario context block:
<div class="scenario-block">
<div class="scenario-context">
<span class="scenario-label">Scenario</span>
<p>Your app processes a 3-hour podcast transcript. The API has a 16,000 token limit. What do you do?</p>
</div>
<!-- quiz-options here -->
</div>---
Callout Boxes
"Aha!" moments — universal CS insights. Max 2 per module.
<div class="callout callout-accent">
<div class="callout-icon">💡</div>
<div class="callout-content">
<strong class="callout-title">Key Insight</strong>
<p>This pattern — splitting responsibilities into focused roles — is one of the most important ideas in software engineering. Engineers call it "separation of concerns."</p>
</div>
</div>Variants:
callout-accent: vermillion left border, light accent background (for CS insights)callout-info: teal left border, light info background (for "good to know")callout-warning: red left border, light error background (for common mistakes)
---
Pattern/Feature Cards
Grid of cards highlighting engineering patterns, tech stack components, or key concepts.
<div class="pattern-cards">
<div class="pattern-card" style="border-top: 3px solid var(--color-actor-1)">
<div class="pattern-icon" style="background: var(--color-actor-1)">🔄</div>
<h4 class="pattern-title">Caching</h4>
<p class="pattern-desc">Store results to avoid redundant work — like keeping leftovers instead of cooking a new meal every time.</p>
</div>
<!-- more cards -->
</div>.pattern-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-4);
}
.pattern-card {
background: var(--color-surface);
border-radius: var(--radius-md);
padding: var(--space-6);
box-shadow: var(--shadow-sm);
transition: transform var(--duration-normal) var(--ease-out), box-shadow var(--duration-normal);
}
.pattern-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
}---
Flow Diagrams
Horizontal flow (desktop):
<div class="flow-steps">
<div class="flow-step">
<div class="flow-step-num">1</div>
<p>User clicks button</p>
</div>
<div class="flow-arrow">→</div>
<div class="flow-step">
<div class="flow-step-num">2</div>
<p>Component A detects click</p>
</div>
<div class="flow-arrow">→</div>
<!-- more steps -->
</div>Arrows rotate to ↓ on mobile via CSS transform.
---
Permission/Config Badges
For annotating config files, permissions, or settings:
<div class="badge-list">
<div class="badge-item">
<code class="badge-code">storage</code>
<span class="badge-desc">Save data between sessions (like browser bookmarks)</span>
</div>
<div class="badge-item">
<code class="badge-code">activeTab</code>
<span class="badge-desc">Access the currently open tab (only when the user clicks)</span>
</div>
</div>.badge-item {
display: flex; align-items: center; gap: var(--space-4);
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-sm);
transition: border-color var(--duration-fast);
}
.badge-item:hover { border-color: var(--color-accent-muted); }
.badge-code {
font-family: var(--font-mono);
font-size: var(--text-sm);
background: var(--color-bg-code);
color: #CBA6F7;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-sm);
white-space: nowrap;
}---
Glossary Tooltips
The most important accessibility feature for non-technical learners. Any technical term in the course text should be wrapped in a tooltip that shows a plain-English definition on hover (desktop) or tap (mobile). The learner never has to leave the page or Google anything.
HTML — mark up terms inline:
<p>The extension uses a
<span class="term" data-definition="A service worker is a background script that runs independently of the web page — like a behind-the-scenes assistant that's always on, even when you're not looking at the page.">service worker</span>
to handle API calls.
</p>CSS:
.term {
border-bottom: 1.5px dashed var(--color-accent-muted);
cursor: pointer; /* NOT cursor: help — pointer feels clickable and inviting */
position: relative;
}
.term:hover, .term.active {
border-bottom-color: var(--color-accent);
color: var(--color-accent);
}
/* The tooltip bubble — uses position: fixed and is appended to document.body
via JS so it is NEVER clipped by ancestor overflow: hidden containers
(like translation blocks). See JS section below for positioning logic. */
.term-tooltip {
position: fixed; /* CRITICAL: fixed, not absolute — prevents clipping */
background: var(--color-bg-code);
color: #CDD6F4;
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-sm);
font-size: var(--text-sm);
font-family: var(--font-body);
line-height: var(--leading-normal);
width: max(200px, min(320px, 80vw));
box-shadow: var(--shadow-lg);
pointer-events: none;
opacity: 0;
transition: opacity var(--duration-fast);
z-index: 10000; /* Above everything, including nav */
}
/* Arrow pointing down */
.term-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--color-bg-code);
}
.term-tooltip.visible {
opacity: 1;
}
/* If tooltip goes off-screen top, flip to below */
.term-tooltip.flip {
bottom: auto;
top: calc(100% + 8px);
}
.term-tooltip.flip::after {
top: auto;
bottom: 100%;
border-top-color: transparent;
border-bottom-color: var(--color-bg-code);
}JS — position: fixed tooltips appended to body (never clipped by overflow):
// Tooltip container — appended to body so it's never clipped
let activeTooltip = null;
function positionTooltip(term, tip) {
const rect = term.getBoundingClientRect();
const tipWidth = 300; // approximate
let left = rect.left + rect.width / 2 - tipWidth / 2;
// Clamp to viewport
left = Math.max(8, Math.min(left, window.innerWidth - tipWidth - 8));
// Try above first
let top = rect.top - 8;
tip.style.left = left + 'px';
// Position above by default, flip below if no room
document.body.appendChild(tip);
const tipHeight = tip.offsetHeight;
if (rect.top - tipHeight - 8 < 0) {
// Flip below
tip.style.top = (rect.bottom + 8) + 'px';
tip.classList.add('flip');
} else {
tip.style.top = (rect.top - tipHeight - 8) + 'px';
tip.classList.remove('flip');
}
}
document.querySelectorAll('.term').forEach(term => {
const tip = document.createElement('span');
tip.className = 'term-tooltip';
tip.textContent = term.dataset.definition;
// Hover for desktop
term.addEventListener('mouseenter', () => {
if (activeTooltip && activeTooltip !== tip) {
activeTooltip.classList.remove('visible');
activeTooltip.remove();
}
positionTooltip(term, tip);
requestAnimationFrame(() => tip.classList.add('visible'));
activeTooltip = tip;
});
term.addEventListener('mouseleave', () => {
tip.classList.remove('visible');
setTimeout(() => { if (!tip.classList.contains('visible')) tip.remove(); }, 150);
activeTooltip = null;
});
// Tap for mobile
term.addEventListener('click', (e) => {
e.stopPropagation();
if (activeTooltip && activeTooltip !== tip) {
activeTooltip.classList.remove('visible');
activeTooltip.remove();
}
if (tip.classList.contains('visible')) {
tip.classList.remove('visible');
tip.remove();
activeTooltip = null;
} else {
positionTooltip(term, tip);
requestAnimationFrame(() => tip.classList.add('visible'));
activeTooltip = tip;
}
});
});
// Close tooltips when clicking elsewhere
document.addEventListener('click', () => {
if (activeTooltip) {
activeTooltip.classList.remove('visible');
activeTooltip.remove();
activeTooltip = null;
}
});Rules:
- Mark up EVERY technical term on first use in each module (API, DOM, callback, async, endpoint, middleware, etc.)
- Keep definitions to 1-2 sentences max, in everyday language
- Use a metaphor in the definition when it helps — e.g., "A callback is like leaving your phone number at a restaurant so they can call you when your table is ready"
- Don't mark the same term twice within the same screen — only on first appearance per module
- The dashed underline should be subtle enough not to distract but visible enough that curious learners discover it
---
Visual File Tree
Use instead of paragraphs listing "this folder does X, that folder does Y." Much easier to scan.
<div class="file-tree">
<div class="ft-folder open">
<span class="ft-name">app/</span>
<span class="ft-desc">Pages and API routes</span>
<div class="ft-children">
<div class="ft-folder">
<span class="ft-name">api/</span>
<span class="ft-desc">Backend endpoints the frontend calls</span>
</div>
<div class="ft-file">
<span class="ft-name">layout.tsx</span>
<span class="ft-desc">The shell that wraps every page</span>
</div>
</div>
</div>
<div class="ft-folder">
<span class="ft-name">components/</span>
<span class="ft-desc">Reusable UI building blocks</span>
</div>
<div class="ft-folder">
<span class="ft-name">lib/</span>
<span class="ft-desc">Shared logic and utilities</span>
</div>
</div>.file-tree { font-family: var(--font-mono); font-size: var(--text-sm); }
.ft-folder, .ft-file {
padding: var(--space-2) var(--space-3);
border-left: 2px solid var(--color-border-light);
margin-left: var(--space-4);
}
.ft-folder > .ft-name { color: var(--color-accent); font-weight: 600; }
.ft-folder > .ft-name::before { content: '📁 '; }
.ft-file > .ft-name::before { content: '📄 '; }
.ft-desc {
color: var(--color-text-secondary);
font-family: var(--font-body);
margin-left: var(--space-2);
font-size: var(--text-xs);
}
.ft-children { margin-left: var(--space-4); }---
Icon-Label Rows
For listing components, features, or concepts visually. Replaces bullet-point paragraphs.
<div class="icon-rows">
<div class="icon-row">
<div class="icon-circle" style="background: var(--color-actor-1)">🖥️</div>
<div>
<strong>Frontend (Next.js)</strong>
<p>What the user sees and interacts with</p>
</div>
</div>
<div class="icon-row">
<div class="icon-circle" style="background: var(--color-actor-2)">⚡</div>
<div>
<strong>API Routes</strong>
<p>Backend logic that runs on the server</p>
</div>
</div>
<div class="icon-row">
<div class="icon-circle" style="background: var(--color-actor-3)">🗄️</div>
<div>
<strong>Database (Supabase)</strong>
<p>Where all the data is stored permanently</p>
</div>
</div>
</div>.icon-rows { display: flex; flex-direction: column; gap: var(--space-4); }
.icon-row {
display: flex; align-items: center; gap: var(--space-4);
padding: var(--space-4);
background: var(--color-surface);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.icon-row p { margin: 0; color: var(--color-text-secondary); font-size: var(--text-sm); }
.icon-circle {
width: 48px; height: 48px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1.25rem; flex-shrink: 0;
}---
Numbered Step Cards
For sequences that would otherwise be a numbered paragraph list. Visual, scannable, and each step stands alone.
<div class="step-cards">
<div class="step-card">
<div class="step-num">1</div>
<div class="step-body">
<strong>User pastes a YouTube URL</strong>
<p>The frontend captures the URL and extracts the video ID</p>
</div>
</div>
<div class="step-card">
<div class="step-num">2</div>
<div class="step-body">
<strong>API fetches the transcript</strong>
<p>A server-side route calls an external service to get the video's text</p>
</div>
</div>
<div class="step-card">
<div class="step-num">3</div>
<div class="step-body">
<strong>AI analyzes the content</strong>
<p>The transcript is sent to an AI model that extracts key moments</p>
</div>
</div>
</div>.step-cards { display: flex; flex-direction: column; gap: var(--space-3); }
.step-card {
display: flex; align-items: flex-start; gap: var(--space-4);
padding: var(--space-4) var(--space-5);
background: var(--color-surface);
border-radius: var(--radius-md);
border-left: 3px solid var(--color-accent);
box-shadow: var(--shadow-sm);
}
.step-num {
width: 32px; height: 32px; border-radius: 50%;
background: var(--color-accent);
color: white; font-weight: 700;
display: flex; align-items: center; justify-content: center;
font-family: var(--font-display);
flex-shrink: 0;
}
.step-body p { margin: var(--space-1) 0 0; color: var(--color-text-secondary); font-size: var(--text-sm); }/**
* CODEBASE-TO-COURSE — COMPLETE JS ENGINE
* Copy this file verbatim into the course output directory.
* Never regenerate it. It handles all interactivity generically.
*
* Engines included:
* - Navigation & progress bar
* - Scroll-triggered reveal animations
* - Keyboard navigation
* - Glossary tooltips
* - Quiz (multiple-choice & scenario)
* - Drag-and-drop matching
* - Group chat animation
* - Data flow / message flow animation
* - Architecture diagram
* - "Spot the bug" challenge
* - Layer toggle
*/
(function () {
'use strict';
/* ── HELPERS ──────────────────────────────────────────────── */
function $(sel, ctx) { return (ctx || document).querySelector(sel); }
function $$(sel, ctx) { return Array.from((ctx || document).querySelectorAll(sel)); }
/* ── NAVIGATION & PROGRESS BAR ────────────────────────────── */
const progressBar = $('#progress-bar');
const navDots = $$('.nav-dot');
const modules = $$('.module');
function updateProgress() {
if (!progressBar) return;
const scrollTop = window.scrollY;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const pct = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
progressBar.style.width = pct + '%';
progressBar.setAttribute('aria-valuenow', Math.round(pct));
updateNavDots();
}
function updateNavDots() {
const scrollMid = window.scrollY + window.innerHeight / 2;
modules.forEach((mod, i) => {
const dot = navDots[i];
if (!dot) return;
const top = mod.offsetTop;
const bottom = top + mod.offsetHeight;
if (scrollMid >= top && scrollMid < bottom) {
dot.classList.add('active');
dot.classList.remove('visited');
} else if (window.scrollY + window.innerHeight > top) {
dot.classList.remove('active');
dot.classList.add('visited');
} else {
dot.classList.remove('active', 'visited');
}
});
}
window.addEventListener('scroll', () => requestAnimationFrame(updateProgress), { passive: true });
updateProgress();
// Nav dot click → scroll to module
navDots.forEach(dot => {
dot.addEventListener('click', () => {
const target = $('#' + dot.dataset.target);
if (target) target.scrollIntoView({ behavior: 'smooth' });
});
});
/* ── KEYBOARD NAVIGATION ───────────────────────────────────── */
function currentModuleIndex() {
const scrollMid = window.scrollY + window.innerHeight / 2;
for (let i = 0; i < modules.length; i++) {
const top = modules[i].offsetTop;
const bottom = top + modules[i].offsetHeight;
if (scrollMid >= top && scrollMid < bottom) return i;
}
return 0;
}
document.addEventListener('keydown', e => {
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
const next = modules[currentModuleIndex() + 1];
if (next) { next.scrollIntoView({ behavior: 'smooth' }); e.preventDefault(); }
}
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
const prev = modules[currentModuleIndex() - 1];
if (prev) { prev.scrollIntoView({ behavior: 'smooth' }); e.preventDefault(); }
}
});
/* ── SCROLL-TRIGGERED REVEAL ───────────────────────────────── */
const revealObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
revealObserver.unobserve(entry.target);
}
});
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 });
$$('.animate-in').forEach(el => revealObserver.observe(el));
// Stagger children
$$('.stagger-children').forEach(parent => {
Array.from(parent.children).forEach((child, i) => {
child.style.setProperty('--stagger-index', i);
});
});
/* ── GLOSSARY TOOLTIPS ─────────────────────────────────────── */
let activeTooltip = null;
function positionTooltip(term, tip) {
const rect = term.getBoundingClientRect();
const tipWidth = Math.min(320, Math.max(200, window.innerWidth * 0.8));
let left = rect.left + rect.width / 2 - tipWidth / 2;
left = Math.max(8, Math.min(left, window.innerWidth - tipWidth - 8));
tip.style.left = left + 'px';
tip.style.width = tipWidth + 'px';
document.body.appendChild(tip);
const tipHeight = tip.offsetHeight;
if (rect.top - tipHeight - 12 < 0) {
tip.style.top = (rect.bottom + 8) + 'px';
tip.classList.add('flip');
} else {
tip.style.top = (rect.top - tipHeight - 8) + 'px';
tip.classList.remove('flip');
}
}
function showTooltip(term, tip) {
if (activeTooltip && activeTooltip !== tip) {
activeTooltip.classList.remove('visible');
activeTooltip.remove();
}
positionTooltip(term, tip);
requestAnimationFrame(() => tip.classList.add('visible'));
activeTooltip = tip;
}
function hideTooltip(tip) {
tip.classList.remove('visible');
setTimeout(() => { if (!tip.classList.contains('visible')) tip.remove(); }, 150);
if (activeTooltip === tip) activeTooltip = null;
}
$$('.term').forEach(term => {
const tip = document.createElement('span');
tip.className = 'term-tooltip';
tip.textContent = term.dataset.definition;
term.addEventListener('mouseenter', () => showTooltip(term, tip));
term.addEventListener('mouseleave', () => hideTooltip(tip));
term.addEventListener('click', e => {
e.stopPropagation();
tip.classList.contains('visible') ? hideTooltip(tip) : showTooltip(term, tip);
});
});
document.addEventListener('click', () => {
if (activeTooltip) { activeTooltip.classList.remove('visible'); activeTooltip.remove(); activeTooltip = null; }
});
/* ── QUIZ ENGINE ───────────────────────────────────────────── */
window.selectOption = function (btn) {
const block = btn.closest('.quiz-question-block');
$$('.quiz-option', block).forEach(o => o.classList.remove('selected'));
btn.classList.add('selected');
};
window.checkQuiz = function (containerId) {
const container = $('#' + containerId);
if (!container) return;
$$('.quiz-question-block', container).forEach(q => {
const selected = $('.quiz-option.selected', q);
const feedback = $('.quiz-feedback', q);
const correct = q.dataset.correct;
const rightExp = q.dataset.explanationRight || '';
const wrongExp = q.dataset.explanationWrong || '';
if (!selected) {
feedback.textContent = 'Pick an answer first!';
feedback.className = 'quiz-feedback show warning';
return;
}
$$('.quiz-option', q).forEach(o => o.disabled = true);
if (selected.dataset.value === correct) {
selected.classList.add('correct');
feedback.innerHTML = '<strong>Exactly!</strong> ' + rightExp;
feedback.className = 'quiz-feedback show success';
} else {
selected.classList.add('incorrect');
const correctBtn = $(`.quiz-option[data-value="${correct}"]`, q);
if (correctBtn) correctBtn.classList.add('correct');
feedback.innerHTML = '<strong>Not quite.</strong> ' + wrongExp;
feedback.className = 'quiz-feedback show error';
}
});
};
window.resetQuiz = function (containerId) {
const container = $('#' + containerId);
if (!container) return;
$$('.quiz-option', container).forEach(o => {
o.classList.remove('selected', 'correct', 'incorrect');
o.disabled = false;
});
$$('.quiz-feedback', container).forEach(f => { f.className = 'quiz-feedback'; f.textContent = ''; });
};
/* ── DRAG-AND-DROP ENGINE ──────────────────────────────────── */
function initDnD(containerEl) {
if (!containerEl) return;
const chips = $$('.dnd-chip', containerEl);
const zones = $$('.dnd-zone', containerEl);
// Mouse (HTML5 Drag API)
chips.forEach(chip => {
chip.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', chip.dataset.answer);
chip.classList.add('dragging');
});
chip.addEventListener('dragend', () => chip.classList.remove('dragging'));
});
zones.forEach(zone => {
const target = $('.dnd-zone-target', zone);
if (!target) return;
target.addEventListener('dragover', e => { e.preventDefault(); target.classList.add('drag-over'); });
target.addEventListener('dragleave', () => target.classList.remove('drag-over'));
target.addEventListener('drop', e => {
e.preventDefault();
target.classList.remove('drag-over');
const answer = e.dataTransfer.getData('text/plain');
const chip = $(`.dnd-chip[data-answer="${answer}"]`, containerEl);
if (!chip) return;
target.textContent = chip.textContent;
target.dataset.placed = answer;
chip.classList.add('placed');
});
});
// Touch
chips.forEach(chip => {
chip.addEventListener('touchstart', e => {
e.preventDefault();
const touch = e.touches[0];
const ghost = chip.cloneNode(true);
ghost.classList.add('touch-ghost');
ghost.style.cssText = `position:fixed;z-index:9999;pointer-events:none;left:${touch.clientX - 40}px;top:${touch.clientY - 20}px;`;
document.body.appendChild(ghost);
chip._ghost = ghost;
chip._answer = chip.dataset.answer;
}, { passive: false });
chip.addEventListener('touchmove', e => {
e.preventDefault();
const touch = e.touches[0];
if (chip._ghost) {
chip._ghost.style.left = (touch.clientX - 40) + 'px';
chip._ghost.style.top = (touch.clientY - 20) + 'px';
}
zones.forEach(z => { const t = $('.dnd-zone-target', z); if (t) t.classList.remove('drag-over'); });
const el = document.elementFromPoint(touch.clientX, touch.clientY);
const zt = el && el.closest('.dnd-zone-target');
if (zt) zt.classList.add('drag-over');
}, { passive: false });
chip.addEventListener('touchend', e => {
if (chip._ghost) { chip._ghost.remove(); chip._ghost = null; }
const touch = e.changedTouches[0];
const el = document.elementFromPoint(touch.clientX, touch.clientY);
const zt = el && el.closest('.dnd-zone-target');
if (zt) {
zt.textContent = chip.textContent;
zt.dataset.placed = chip._answer;
chip.classList.add('placed');
}
zones.forEach(z => { const t = $('.dnd-zone-target', z); if (t) t.classList.remove('drag-over'); });
});
});
}
window.checkDnD = function (containerId) {
const container = $('#' + containerId);
if (!container) return;
$$('.dnd-zone', container).forEach(zone => {
const target = $('.dnd-zone-target', zone);
if (!target || !target.dataset.placed) return;
if (target.dataset.placed === zone.dataset.correct) {
target.classList.add('correct-placed');
} else {
target.classList.add('incorrect-placed');
}
});
};
window.resetDnD = function (containerId) {
const container = $('#' + containerId);
if (!container) return;
$$('.dnd-zone-target', container).forEach(t => {
t.textContent = 'Drop here';
delete t.dataset.placed;
t.classList.remove('correct-placed', 'incorrect-placed');
});
$$('.dnd-chip', container).forEach(c => c.classList.remove('placed', 'dragging'));
};
// Auto-init all dnd containers
$$('.dnd-container').forEach(el => initDnD(el));
/* ── GROUP CHAT ENGINE ─────────────────────────────────────── */
function initChat(containerEl) {
if (!containerEl) return;
const messages = $$('.chat-message', containerEl);
const typingEl = $('.chat-typing', containerEl);
const typingAvEl = $('#' + containerEl.id + '-typing-avatar') || $('.chat-avatar', typingEl);
const progressEl = $('.chat-progress', containerEl);
let index = 0;
// Build actor map from messages
const actors = {};
messages.forEach(msg => {
const sender = msg.dataset.sender;
const avatar = $('.chat-avatar', msg);
if (avatar && !actors[sender]) {
actors[sender] = { initial: avatar.textContent.trim(), style: avatar.style.background };
}
});
function updateProgress() {
if (progressEl) progressEl.textContent = index + ' / ' + messages.length + ' messages';
}
function showNext() {
if (index >= messages.length) return;
const msg = messages[index];
const sender = msg.dataset.sender;
if (typingEl && actors[sender]) {
if (typingAvEl) {
typingAvEl.textContent = actors[sender].initial;
typingAvEl.style.background = actors[sender].style;
}
typingEl.style.display = 'flex';
}
setTimeout(() => {
if (typingEl) typingEl.style.display = 'none';
msg.style.display = 'flex';
msg.style.animation = 'fadeSlideUp 0.3s var(--ease-out)';
index++;
updateProgress();
}, 800);
}
function showAll() {
const iv = setInterval(() => {
if (index >= messages.length) { clearInterval(iv); return; }
showNext();
}, 1200);
}
function reset() {
index = 0;
messages.forEach(m => { m.style.display = 'none'; m.style.animation = ''; });
if (typingEl) typingEl.style.display = 'none';
updateProgress();
}
// Bind controls
const nextBtn = $('.chat-next-btn', containerEl);
const allBtn = $('.chat-all-btn', containerEl);
const resetBtn = $('.chat-reset-btn', containerEl);
if (nextBtn) nextBtn.addEventListener('click', showNext);
if (allBtn) allBtn.addEventListener('click', showAll);
if (resetBtn) resetBtn.addEventListener('click', reset);
updateProgress();
}
$$('.chat-window').forEach(el => initChat(el));
/* ── FLOW ANIMATION ENGINE ─────────────────────────────────── */
function initFlow(containerEl) {
if (!containerEl) return;
const stepsData = JSON.parse(containerEl.dataset.steps || '[]');
const labelEl = $('.flow-step-label', containerEl);
const progressEl = $('.flow-progress', containerEl);
const packet = $('.flow-packet', containerEl);
let step = 0;
function updateProgress() {
if (progressEl) progressEl.textContent = 'Step ' + step + ' / ' + stepsData.length;
}
function animatePacket(fromId, toId) {
if (!packet) return;
const fromEl = $('#' + fromId);
const toEl = $('#' + toId);
if (!fromEl || !toEl) return;
const fromR = fromEl.getBoundingClientRect();
const toR = toEl.getBoundingClientRect();
const contR = containerEl.getBoundingClientRect();
const fx = fromR.left + fromR.width / 2 - contR.left;
const fy = fromR.top + fromR.height / 2 - contR.top;
const tx = toR.left + toR.width / 2 - contR.left;
const ty = toR.top + toR.height / 2 - contR.top;
packet.style.setProperty('--packet-from-x', fx + 'px');
packet.style.setProperty('--packet-from-y', fy + 'px');
packet.style.setProperty('--packet-to-x', tx + 'px');
packet.style.setProperty('--packet-to-y', ty + 'px');
packet.style.display = 'block';
packet.style.animation = 'none';
packet.offsetHeight; // reflow
packet.style.animation = 'packetMove 0.8s var(--ease-in-out) forwards';
setTimeout(() => { packet.style.display = 'none'; }, 850);
}
function next() {
if (step >= stepsData.length) return;
const s = stepsData[step];
$$('.flow-actor', containerEl).forEach(a => a.classList.remove('active'));
if (s.highlight) {
const hEl = $('#' + s.highlight, containerEl) || $('#flow-' + s.highlight);
if (hEl) hEl.classList.add('active');
}
if (s.packet && s.from && s.to) animatePacket('flow-' + s.from, 'flow-' + s.to);
if (labelEl) labelEl.textContent = s.label || '';
step++;
updateProgress();
}
function reset() {
step = 0;
$$('.flow-actor', containerEl).forEach(a => a.classList.remove('active'));
if (labelEl) labelEl.textContent = 'Click "Next Step" to begin';
if (packet) packet.style.display = 'none';
updateProgress();
}
const nextBtn = $('.flow-next-btn', containerEl);
const resetBtn = $('.flow-reset-btn', containerEl);
if (nextBtn) nextBtn.addEventListener('click', next);
if (resetBtn) resetBtn.addEventListener('click', reset);
updateProgress();
}
$$('.flow-animation').forEach(el => initFlow(el));
/* ── ARCHITECTURE DIAGRAM ──────────────────────────────────── */
$$('.arch-component').forEach(comp => {
comp.addEventListener('click', function () {
const diagram = this.closest('.arch-diagram');
$$('.arch-component', diagram).forEach(c => c.classList.remove('active'));
this.classList.add('active');
const descEl = $('.arch-description', diagram);
if (descEl) descEl.textContent = this.dataset.desc || '';
});
});
/* ── BUG CHALLENGE ─────────────────────────────────────────── */
window.checkBugLine = function (el, isCorrect) {
const challenge = el.closest('.bug-challenge');
const feedback = $('.bug-feedback', challenge);
if (isCorrect) {
el.classList.add('correct');
feedback.innerHTML = '<strong>Found it!</strong> ' + (el.dataset.explanation || '');
feedback.className = 'bug-feedback show success';
$$('.bug-line', challenge).forEach(l => l.style.pointerEvents = 'none');
} else {
el.classList.add('incorrect');
feedback.innerHTML = (el.dataset.hint || 'Not this line — keep looking...');
feedback.className = 'bug-feedback show error';
setTimeout(() => {
el.classList.remove('incorrect');
feedback.className = 'bug-feedback';
}, 1800);
}
};
/* ── LAYER TOGGLE ──────────────────────────────────────────── */
window.showLayer = function (layerId, btn) {
const demo = btn ? btn.closest('.layer-demo') : null;
if (!demo) return;
$$('.layer', demo).forEach(l => l.style.display = 'none');
$$('.layer-tab', demo).forEach(t => t.classList.remove('active'));
const layer = $('#' + layerId);
if (layer) layer.style.display = 'block';
btn.classList.add('active');
};
})();
Module Brief Template
When to read this: During Phase 2.5 (planning checkpoint) for complex codebases. Fill in one brief per module, save to course-name/briefs/0N-slug.md. Each brief gives a parallel agent everything it needs to write one module without reading the codebase or SKILL.md.---
Module N: [Title]
Teaching Arc
- Metaphor: [A fresh, specific metaphor — never "restaurant." See
references/content-philosophy.md> Metaphors First] - Opening hook: [1 sentence that connects to something the learner already knows from using the app]
- Key insight: [The one thing the learner should walk away understanding]
- "Why should I care?": [How this helps them steer AI / debug / make decisions]
Code Snippets (pre-extracted)
Include the actual code the module will use in code↔English translation blocks. Copy-paste from the codebase with file path and line numbers. The writing agent will use these verbatim — it will NOT re-read the codebase.
File: src/example/file.ts (lines 12-24) [paste actual code here]
File: src/another/file.ts (lines 45-52) [paste actual code here]
Interactive Elements
Check which elements this module needs. Include enough detail for the writing agent to build them.
- [ ] Code↔English translation — which snippet(s) from above
- [ ] Quiz — [number] questions, style: [scenario / debugging / architecture / tracing]. Brief description of each question's angle.
- [ ] Group chat animation — actors: [list]. Message flow summary: [who says what to whom, in what order]
- [ ] Data flow animation — actors: [list]. Steps: [sequence of highlights and packet movements]
- [ ] Drag-and-drop — items: [list], targets: [list]
- [ ] Other — [architecture diagram, layer toggle, pattern cards, etc.]
Reference Files to Read
List only the sections the writing agent needs — not the whole file.
references/interactive-elements.md→ [section names, e.g., "Multiple-Choice Quizzes", "Group Chat Animation"]references/design-system.md→ [only if needed for specific tokens not in the brief]references/content-philosophy.md→ [always include — agent needs content rules]references/gotchas.md→ [always include — agent needs the checklist]
Connections
- Previous module: [Title — what it covered, so this module can build on it]
- Next module: [Title — what it will cover, so this module can set it up]
- Tone/style notes: [Any course-wide consistency notes: accent color name, actor naming convention, etc.]
Related skills
FAQ
What does codebase-to-course do?
codebase-to-course is a Claude Code skill for ai & agent building.
When should I use codebase-to-course?
When you need to helps with ai & agent building tasks., or when codebase-to-course is a claude code skill for ai & agent building.
What are the main capabilities?
codebase-to-course; AI & Agent Building; AI-coding skill.