feat: add HTML lessons mode and side-by-side comparison UI

- Add HTML mode support with new validation types (element_exists,
  element_count, attribute_value, element_text, parent_child, sibling)
- Create 3 HTML lesson modules: Elements, Forms Basic, Forms Validation
- Implement side-by-side preview comparison (Your Output vs Expected)
- Add merge animation with "Perfect Match!" overlay on validation success
- Render expected output from solutionCode field in lesson JSON
- Update schema to support HTML mode and solutionCode
- Reorder modules: HTML first, then CSS, then Tailwind
- Update tests for new functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-21 22:12:00 +01:00
parent 94cdf368bc
commit 862d29aa19
15 changed files with 1136 additions and 66 deletions

View File

@@ -0,0 +1,97 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "html-elements",
"title": "HTML Elements: Block vs Inline",
"description": "Understanding the fundamental difference between container (block) and inline elements",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "block-vs-inline-intro",
"title": "Block vs Inline Elements",
"description": "HTML elements fall into two main categories:<br><br><strong>Block elements</strong> (containers) start on a new line and take full width. Examples: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Create a paragraph with a <kbd>&lt;strong&gt;</kbd> word inside it. Notice how the paragraph is a block element (takes full width) while strong is inline (flows with text).",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
"sandboxCSS": "",
"initialCode": "<p>This is a paragraph with an important word.</p>",
"solutionCode": "<p>This is a paragraph with a <strong>important</strong> word.</p>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "p",
"message": "Add a <p> paragraph element"
},
{
"type": "parent_child",
"value": { "parent": "p", "child": "strong" },
"message": "Place a <strong> element inside your paragraph"
}
]
},
{
"id": "semantic-containers",
"title": "Semantic Container Elements",
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd>&lt;header&gt;</kbd> - Page or section header<br><kbd>&lt;nav&gt;</kbd> - Navigation links<br><kbd>&lt;main&gt;</kbd> - Main content area<br><kbd>&lt;section&gt;</kbd> - Thematic grouping<br><kbd>&lt;article&gt;</kbd> - Self-contained content<br><kbd>&lt;footer&gt;</kbd> - Page or section footer",
"task": "Create a basic page structure with <kbd>&lt;header&gt;</kbd>, <kbd>&lt;main&gt;</kbd>, and <kbd>&lt;footer&gt;</kbd> elements. Add a heading in the header.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }",
"sandboxCSS": "",
"initialCode": "<!-- Create your page structure here -->",
"solutionCode": "<header>\n <h1>My Website</h1>\n</header>\n<main>\n <p>Welcome to my site!</p>\n</main>\n<footer>\n <p>Copyright 2025</p>\n</footer>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "header",
"message": "Add a <header> element"
},
{
"type": "element_exists",
"value": "main",
"message": "Add a <main> element"
},
{
"type": "element_exists",
"value": "footer",
"message": "Add a <footer> element"
},
{
"type": "parent_child",
"value": { "parent": "header", "child": "h1" },
"message": "Add an <h1> heading inside your header"
}
]
},
{
"id": "div-vs-span",
"title": "Generic Containers: div and span",
"description": "When you need a container without semantic meaning:<br><br><kbd>&lt;div&gt;</kbd> - Generic block container (for layout/grouping)<br><kbd>&lt;span&gt;</kbd> - Generic inline container (for styling text portions)<br><br>Use semantic elements when possible, div/span when no semantic element fits.",
"task": "Wrap the word 'highlighted' in a <kbd>&lt;span&gt;</kbd> to style it differently. Wrap the whole quote in a <kbd>&lt;div&gt;</kbd>.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: Georgia, serif; padding: 20px; } div { background: #f5f5f5; padding: 15px; border-left: 4px solid #1976d2; } span { background: #fff59d; padding: 2px 4px; }",
"sandboxCSS": "",
"initialCode": "The most highlighted moment was unforgettable.",
"solutionCode": "<div>The most <span>highlighted</span> moment was unforgettable.</div>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "div",
"message": "Wrap everything in a <div> element"
},
{
"type": "element_exists",
"value": "span",
"message": "Add a <span> around the word 'highlighted'"
},
{
"type": "element_text",
"value": { "selector": "span", "text": "highlighted" },
"message": "The <span> should contain the word 'highlighted'"
}
]
}
]
}

View File

@@ -0,0 +1,102 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "html-forms-basic",
"title": "HTML Forms: Basic Inputs",
"description": "Learn to create forms with various input types",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "form-structure",
"title": "Form Structure",
"description": "Every form needs a <kbd>&lt;form&gt;</kbd> wrapper. Inside, use <kbd>&lt;label&gt;</kbd> to describe inputs and <kbd>&lt;input&gt;</kbd> for user data entry.<br><br>The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs for accessibility.",
"task": "Create a form with a text input for 'Name'. Include a label connected to the input using for/id attributes.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "",
"initialCode": "<!-- Create your form here -->",
"solutionCode": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "form",
"message": "Wrap everything in a <form> element"
},
{
"type": "element_exists",
"value": "label",
"message": "Add a <label> for your input"
},
{
"type": "element_exists",
"value": "input",
"message": "Add an <input> element"
},
{
"type": "attribute_value",
"value": { "selector": "label", "attr": "for", "value": null },
"message": "Add a 'for' attribute to your label"
},
{
"type": "attribute_value",
"value": { "selector": "input", "attr": "id", "value": null },
"message": "Add an 'id' attribute to your input"
}
]
},
{
"id": "input-types",
"title": "Input Types",
"description": "Different input types provide appropriate keyboards and validation:<br><br><kbd>type=\"text\"</kbd> - General text<br><kbd>type=\"email\"</kbd> - Email with @ validation<br><kbd>type=\"password\"</kbd> - Hidden characters<br><kbd>type=\"number\"</kbd> - Numeric keyboard<br><kbd>type=\"tel\"</kbd> - Phone keyboard",
"task": "Create a login form with an email input and a password input, each with proper labels.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "",
"initialCode": "<form>\n <!-- Add email and password inputs -->\n</form>",
"solutionCode": "<form>\n <div class=\"form-group\">\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n </div>\n <div class=\"form-group\">\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n </div>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "input[type='email']",
"message": "Add an input with type=\"email\""
},
{
"type": "element_exists",
"value": "input[type='password']",
"message": "Add an input with type=\"password\""
},
{
"type": "element_count",
"value": { "selector": "label", "min": 2 },
"message": "Add labels for both inputs"
}
]
},
{
"id": "submit-button",
"title": "Submit Button",
"description": "Forms need a way to submit data. Use:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferred, flexible content<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Simple text-only button<br><br>The button text should be action-oriented (e.g., 'Sign In', 'Register', 'Send').",
"task": "Add a submit button to the form with the text 'Sign In'.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
"sandboxCSS": "",
"initialCode": "<form>\n <div class=\"form-group\">\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n </div>\n <div class=\"form-group\">\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n </div>\n <!-- Add submit button -->\n</form>",
"solutionCode": "<form>\n <div class=\"form-group\">\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n </div>\n <div class=\"form-group\">\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n </div>\n <button type=\"submit\">Sign In</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "button[type='submit'], input[type='submit']",
"message": "Add a submit button to your form"
},
{
"type": "element_text",
"value": { "selector": "button", "text": "Sign In" },
"message": "The button should say 'Sign In'"
}
]
}
]
}

View File

@@ -0,0 +1,112 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation",
"title": "HTML Forms: Validation",
"description": "Learn HTML5 built-in form validation attributes",
"mode": "html",
"difficulty": "intermediate",
"lessons": [
{
"id": "required-fields",
"title": "Required Fields",
"description": "The <kbd>required</kbd> attribute prevents form submission if the field is empty.<br><br>Add it to any input that must be filled:<br><kbd>&lt;input type=\"text\" required&gt;</kbd><br><br>The browser shows a validation message automatically.",
"task": "Make both the name and email fields required.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } label .required { color: #d32f2f; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "<form>\n <div class=\"form-group\">\n <label for=\"name\">Name: <span class=\"required\">*</span></label>\n <input type=\"text\" id=\"name\" name=\"name\">\n </div>\n <div class=\"form-group\">\n <label for=\"email\">Email: <span class=\"required\">*</span></label>\n <input type=\"email\" id=\"email\" name=\"email\">\n </div>\n <button type=\"submit\">Submit</button>\n</form>",
"solutionCode": "<form>\n <div class=\"form-group\">\n <label for=\"name\">Name: <span class=\"required\">*</span></label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n </div>\n <div class=\"form-group\">\n <label for=\"email\">Email: <span class=\"required\">*</span></label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n </div>\n <button type=\"submit\">Submit</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[name='name']", "attr": "required", "value": true },
"message": "Add the 'required' attribute to the name input"
},
{
"type": "attribute_value",
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
"message": "Add the 'required' attribute to the email input"
}
]
},
{
"id": "input-constraints",
"title": "Input Constraints",
"description": "Control what users can enter:<br><br><kbd>minlength</kbd> / <kbd>maxlength</kbd> - Text length limits<br><kbd>min</kbd> / <kbd>max</kbd> - Number range<br><kbd>pattern</kbd> - Regex pattern matching<br><kbd>placeholder</kbd> - Hint text (not a label!)",
"task": "Add validation: password must be 8-20 characters. Add a helpful placeholder.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid:not(:placeholder-shown) { border-color: #d32f2f; } .hint { font-size: 12px; color: #666; margin-top: 4px; } button { padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "<form>\n <div class=\"form-group\">\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required>\n <div class=\"hint\">Must be 8-20 characters</div>\n </div>\n <button type=\"submit\">Create Account</button>\n</form>",
"solutionCode": "<form>\n <div class=\"form-group\">\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\">\n <div class=\"hint\">Must be 8-20 characters</div>\n </div>\n <button type=\"submit\">Create Account</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "minlength", "value": "8" },
"message": "Add minlength=\"8\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
"message": "Add maxlength=\"20\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
"message": "Add a placeholder to hint what to enter"
}
]
},
{
"id": "complete-registration",
"title": "Complete Registration Form",
"description": "Build a complete registration form with all validation concepts:<br><br>- Required fields marked with *<br>- Email validation (use type=\"email\")<br>- Password with length constraints<br>- Terms checkbox (required)<br>- Submit button",
"task": "Complete the registration form. Add required attributes, proper input types, and validation constraints.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 400px; background: #f5f5f5; padding: 25px; border-radius: 8px; } h2 { margin-top: 0; margin-bottom: 20px; } .form-group { margin-bottom: 18px; } label { display: block; margin-bottom: 5px; font-weight: 500; } .required { color: #d32f2f; } input[type='text'], input[type='email'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 14px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; } .checkbox-group { display: flex; align-items: flex-start; gap: 8px; } .checkbox-group input { width: auto; margin-top: 3px; } .checkbox-group label { margin: 0; font-weight: normal; } button { width: 100%; padding: 12px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 500; } button:hover { background: #1565c0; }",
"sandboxCSS": "",
"initialCode": "<form>\n <h2>Create Account</h2>\n \n <div class=\"form-group\">\n <label for=\"fullname\">Full Name <span class=\"required\">*</span></label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n </div>\n \n <div class=\"form-group\">\n <label for=\"email\">Email <span class=\"required\">*</span></label>\n <input id=\"email\" name=\"email\">\n </div>\n \n <div class=\"form-group\">\n <label for=\"password\">Password <span class=\"required\">*</span></label>\n <input id=\"password\" name=\"password\">\n </div>\n \n <div class=\"form-group checkbox-group\">\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n <label for=\"terms\">I agree to the Terms of Service <span class=\"required\">*</span></label>\n </div>\n \n <button type=\"submit\">Register</button>\n</form>",
"solutionCode": "<form>\n <h2>Create Account</h2>\n \n <div class=\"form-group\">\n <label for=\"fullname\">Full Name <span class=\"required\">*</span></label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n </div>\n \n <div class=\"form-group\">\n <label for=\"email\">Email <span class=\"required\">*</span></label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n </div>\n \n <div class=\"form-group\">\n <label for=\"password\">Password <span class=\"required\">*</span></label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n </div>\n \n <div class=\"form-group checkbox-group\">\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n <label for=\"terms\">I agree to the Terms of Service <span class=\"required\">*</span></label>\n </div>\n \n <button type=\"submit\">Register</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "#fullname", "attr": "required", "value": true },
"message": "Make the full name field required"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "type", "value": "email" },
"message": "Set the email input type to 'email'"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "required", "value": true },
"message": "Make the email field required"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "type", "value": "password" },
"message": "Set the password input type to 'password'"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "required", "value": true },
"message": "Make the password field required"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
"message": "Add minlength=\"8\" to password"
},
{
"type": "attribute_value",
"value": { "selector": "#terms", "attr": "required", "value": true },
"message": "Make the terms checkbox required"
}
]
}
]
}