feat(i18n): add support for Polish, Spanish, Arabic, and Ukrainian
- Create lesson directories and copy English lessons as templates - Add full UI translations for pl, es, ar, uk languages - Update lessons.js with module stores for all new languages - Implement language cycling (en → de → pl → es → ar → uk → en) - Fix playground mode detection (lesson.mode takes precedence)
This commit is contained in:
550
lessons/ar/00-basic-selectors.json
Normal file
550
lessons/ar/00-basic-selectors.json
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "css-basic-selectors",
|
||||||
|
"title": "CSS Selectors",
|
||||||
|
"description": "CSS selectors are the foundation of styling web pages, allowing you to target specific HTML elements for styling. This module introduces fundamental selector types including element type selectors, class selectors, ID selectors, and the universal selector.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "introduction-to-selectors",
|
||||||
|
"title": "What's a Selector?",
|
||||||
|
"description": "A CSS selector is the first part of a CSS rule that tells the browser which HTML elements should receive the styles defined in the declaration block. Selectors are essentially patterns that match against elements in your HTML document. Understanding selectors is fundamental because they determine which elements your CSS rules will affect. The element or elements targeted by a selector are referred to as the 'subject of the selector.' When writing a CSS rule, you first specify the selector, followed by curly braces that contain the style declarations.<br/>For example, to change the text color of elements, you can use the <kbd>color</kbd> property within your declaration block.<br><br><pre>/* Element selector */\np {\n color: orangered;\n /* │ └─── Indicates the value of the expression\n │ \n └─────────── Indicates the property of the expression */\n}</pre>",
|
||||||
|
"task": "Write a CSS rule using a type selector that targets all paragraph elements <kbd>p</kbd> in the document. Make the text blue by setting the <kbd>color</kbd> property to <kbd>blue</kbd>.",
|
||||||
|
"previewHTML": "<h1>Introduction to CSS Selectors</h1>\n<p>This paragraph should turn blue.</p>\n<div>This div element should remain unchanged.</div>\n<p>This second paragraph should also turn blue.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Write a type selector to target all paragraph elements */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p { color: blue }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>p { … }</kbd> to select all paragraph elements",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color:</kbd> property in your CSS rule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "blue",
|
||||||
|
"message": "Set the color value to <kbd>blue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "blue"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>color: blue</kbd> to set the text color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type-selectors",
|
||||||
|
"title": "Type Selectors",
|
||||||
|
"description": "Type selectors (also called tag name selectors or element selectors) target HTML elements based on their tag name. For example, <kbd>p</kbd> selects all paragraph elements, <kbd>h1</kbd> selects all level-one headings, and <kbd>div</kbd> selects all division elements. Type selectors are the most fundamental way to select elements, applying styles consistently to all instances of a particular HTML element throughout your document. You can define a variety of CSS properties with type selectors, such as <kbd>color</kbd> for text color, <kbd>background-color</kbd> for the background, and <kbd>font-weight</kbd> for text emphasis. They provide a broad approach for styling your page and are often the starting point for more specific styling using other selector types.",
|
||||||
|
"task": "Write three separate CSS rules using type selectors to target specific HTML elements: make <kbd>h2</kbd> headings <kbd>purple</kbd>, give <kbd>span</kbd> elements a <kbd>yellow</kbd> background, and make <kbd>strong</kbd> elements <kbd>red</kbd>.",
|
||||||
|
"previewHTML": "<h2>Type Selectors Example</h2>\n<p>Regular paragraph text <span>with a highlighted span</span> that should have a yellow background.</p>\n<p>Another paragraph with <strong>strong important text</strong> that should be red.</p>\n<h2>Another Heading</h2>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
|
||||||
|
"codePrefix": "/* Write three separate type selectors below */\n\n",
|
||||||
|
"initialCode": "/* 1. Make h2 headings purple */\n\n\n/* 2. Give span elements a yellow background */\n\n\n/* 3. Make strong elements red */\n",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "/* 1. Make h2 headings purple */\nh2 {\n color: purple;\n}\n\n/* 2. Give span elements a yellow background */\nspan {\n background-color: yellow;\n}\n\n/* 3. Make strong elements red */\nstrong {\n color: red;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^h2\\s*{",
|
||||||
|
"message": "Include an <kbd>h2 { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Set the <kbd>color</kbd> property to <kbd>purple</kbd> for h2 elements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "h2\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your h2 rule with a closing brace <kbd>}</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\s*{",
|
||||||
|
"message": "Include a <kbd>span { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Set a <kbd>background-color: yellow</kbd> for span elements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your span rule with a closing brace <kbd>}</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^strong\\s*{",
|
||||||
|
"message": "Include a <kbd>strong { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
|
||||||
|
"message": "Set the <kbd>color: red</kbd> for strong elements"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-selectors",
|
||||||
|
"title": "Class Selectors",
|
||||||
|
"description": "Class selectors target elements with a specific class attribute value. They begin with a dot (.) followed by the class name. Classes are powerful because they allow you to apply the same styles to multiple elements regardless of their type. An HTML element can have multiple classes (separated by spaces in the class attribute), and a class can be applied to any number of elements. When using class selectors, you can apply properties like <kbd>background-color</kbd> to set the background color of elements, and <kbd>font-weight</kbd> to control text thickness, making text bold or normal. This flexibility makes class selectors one of the most commonly used methods for applying styles in CSS, allowing for modular and reusable styling across your website.",
|
||||||
|
"task": "Create a CSS rule using a class selector that targets elements with the class <kbd>highlight</kbd>. Give these elements a <kbd>yellow</kbd> background and <kbd>bold</kbd> text.",
|
||||||
|
"previewHTML": "<h2>Using Class Selectors</h2>\n<p>This is a regular paragraph, but <span class=\"highlight\">this span has the highlight class</span> applied to it.</p>\n<p class=\"highlight\">This entire paragraph has the highlight class.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"highlight\">This list item is highlighted</li>\n</ul>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
|
||||||
|
"codePrefix": "/* Create a class selector for elements with the 'highlight' class */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.highlight\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>.highlight { … }</kbd> to create a class selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color:</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>yellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-weight:",
|
||||||
|
"message": "Include the <kbd>font-weight:</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-weight",
|
||||||
|
"expected": "bold"
|
||||||
|
},
|
||||||
|
"message": "Set the font-weight to <kbd>bold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "multiple-classes",
|
||||||
|
"title": "Multiple Classes",
|
||||||
|
"description": "HTML elements can have multiple classes applied simultaneously, allowing for composable and modular CSS designs. When an element has multiple classes, it will receive styles from all matching class selectors. This approach enables you to build a library of reusable CSS classes that can be combined in different ways. You can also target elements that have a specific combination of classes by chaining class selectors together without spaces (e.g., <kbd>.class1.class2</kbd>). When styling these elements, you might use properties like <kbd>border-color</kbd> to change the color of element borders, and <kbd>background-color</kbd> to set the background color of elements. This technique lets you create conditional styles that only apply when certain classes appear together.",
|
||||||
|
"task": "Complete the CSS rule that targets elements with both <kbd>card</kbd> and <kbd>featured</kbd> classes by chaining the selectors. Set the border-color to gold and the background-color to lemonchiffon to make featured cards stand out.",
|
||||||
|
"previewHTML": "<h2>Multiple Class Combinations</h2>\n<div class=\"card\">Regular Card</div>\n<div class=\"card featured\">Featured Card</div>\n<div class=\"featured\">Just Featured (not a card)</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* The .card class already has basic styling */\n/* Now target elements with BOTH classes: 'card' AND 'featured' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.card\\.featured\\s*{",
|
||||||
|
"message": "Chain the selectors as <kbd>.card.featured</kbd> (no space between them)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-color:",
|
||||||
|
"message": "Include the <kbd>border-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-color",
|
||||||
|
"expected": "gold"
|
||||||
|
},
|
||||||
|
"message": "Set the border color to <kbd>gold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*;",
|
||||||
|
"message": "Make sure to end your CSS rule with a semicolon <kbd>;</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lemonchiffon"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>lemonchiffon</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-with-type",
|
||||||
|
"title": "Combining Types",
|
||||||
|
"description": "You can combine type selectors with class selectors to target specific HTML elements that have a certain class. This creates a more specific selector that only matches when both conditions are true: the element is of the specified type AND it has the specified class. For example, <kbd>p.note</kbd> would select paragraph elements with the class <kbd>note</kbd>, but would not select divs or spans with that same class. You can style these combined selections using properties like <kbd>background-color</kbd> to set a colored background for your elements. This approach allows you to apply different styles to the same class when it appears on different element types.",
|
||||||
|
"task": "Create a CSS rule that specifically targets <kbd><span></kbd> elements with the class <kbd>highlight</kbd>. Make those elements have an orange background, while other elements with the highlight class remain untouched.",
|
||||||
|
"previewHTML": "<h2>Type and Class Combinations</h2>\n<p>This paragraph has a <span class=\"highlight\">highlighted span</span> that should have an orange background.</p>\n<p class=\"highlight\">This paragraph has the highlight class but should NOT have an orange background.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
|
||||||
|
"sandboxCSS": "h2, p, span { padding: 5px; }",
|
||||||
|
"codePrefix": "/* The .highlight class already sets font-weight to bold */\n/* Now target ONLY span elements with the highlight class */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\.highlight\\s*{",
|
||||||
|
"message": "Use <kbd>span.highlight</kbd> selector (no space between element and class)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "orange"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>orange</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-selectors",
|
||||||
|
"title": "ID Selectors",
|
||||||
|
"description": "ID selectors target elements with a specific id attribute. They begin with a hash/pound sign (#) followed by the ID name. Unlike classes, IDs must be unique within a document—each ID value should be used only once per page. ID selectors have higher specificity than class or element selectors, meaning they override those selectors when conflicts arise. When styling with ID selectors, you can use properties like <kbd>color</kbd> to define text color, and <kbd>text-decoration</kbd> to control the appearance of text, such as adding underlines to elements. Because of their uniqueness requirement, IDs are best used for one-of-a-kind elements like page headers, main navigation, or specific unique components that appear only once on a page.",
|
||||||
|
"task": "Create a CSS rule with an ID selector that targets the element with the ID <kbd>main-title</kbd>. Set its color to purple and add an underline with <kbd>text-decoration: underline</kbd>.",
|
||||||
|
"previewHTML": "<h1 id=\"main-title\">Main Page Title</h1>\n<p>Regular paragraph content.</p>\n<h2>Secondary Heading</h2>\n<p id=\"intro\">Introduction paragraph (different ID).</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Create an ID selector to target the element with id=\"main-title\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^#main-title\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>#main-title</kbd> to create an ID selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Set the color to <kbd>purple</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-decoration:",
|
||||||
|
"message": "Include the <kbd>text-decoration</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "text-decoration",
|
||||||
|
"expected": "underline"
|
||||||
|
},
|
||||||
|
"message": "Set the text-decoration to <kbd>underline</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "#main-title\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-with-type",
|
||||||
|
"title": "Type + ID",
|
||||||
|
"description": "Similar to how you can combine type and class selectors, you can also combine type selectors with ID selectors. For example, <kbd>h1#title</kbd> targets an h1 element with the ID 'title'. When using this combined approach, you can apply CSS properties like <kbd>font-style</kbd> to control the slant of the text, making it italic or normal. While this selector combination is more specific than using just the ID selector, it's often unnecessary since IDs should already be unique in a document. However, this technique can be useful for improving code readability or when you want to emphasize that a particular ID should only appear on a specific element type.",
|
||||||
|
"task": "Create a CSS rule that combines a type selector with an ID selector to target specifically a paragraph element with the ID <kbd>special</kbd>. Set its font style to italic.",
|
||||||
|
"previewHTML": "<h2 id=\"special\">Heading with ID \"special\" (should NOT be affected)</h2>\n<p id=\"special\">Paragraph with ID \"special\" (should become italic)</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Create a combined type+ID selector for a paragraph with id=\"special\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p#special\\s*{",
|
||||||
|
"message": "Use <kbd>p#special</kbd> to target paragraphs with ID <kbd>special</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-style:",
|
||||||
|
"message": "Include the <kbd>font-style</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-style",
|
||||||
|
"expected": "italic"
|
||||||
|
},
|
||||||
|
"message": "Set the font-style to <kbd>italic</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p#special\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "selector-lists",
|
||||||
|
"title": "Selector Lists",
|
||||||
|
"description": "When multiple elements need the same styling, you can group them together using a selector list (also known as grouping selectors). Selector lists are created by separating individual selectors with commas. This approach reduces repetition in your CSS, making it more maintainable and efficient. For example, <kbd>h1, h2, h3 { color: blue; }</kbd> applies the same blue color to all three heading levels. When styling multiple selectors at once, you can apply properties like <kbd>background-color</kbd> to set the background, <kbd>border-left</kbd> to create a left border with a specific thickness, style, and color, and <kbd>padding-left</kbd> to create space between the content and the left border. Whitespace around commas is optional, and each selector in the list can be any valid selector type-elements, classes, IDs, or even more complex selectors.",
|
||||||
|
"task": "Create a selector list that applies the same styles to three different elements: paragraphs with class <kbd>note</kbd>, list items with class <kbd>important</kbd>, and the element with ID <kbd>summary</kbd>. Give them a <kbd>lightyellow</kbd> background, a <kbd>gold</kbd> left border, and some left <kbd>padding</kbd>.",
|
||||||
|
"previewHTML": "<p class=\"note\">This is a note paragraph.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"important\">Important list item</li>\n</ul>\n<div id=\"summary\">Summary section</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
|
||||||
|
"codePrefix": "/* Create a selector list to apply the same styles to multiple different elements */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "p.note",
|
||||||
|
"message": "Include <kbd>p.note</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "li.important",
|
||||||
|
"message": "Include <kbd>li.important</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "#summary",
|
||||||
|
"message": "Include <kbd>#summary</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
|
||||||
|
"message": "Create a comma-separated list with all three selectors",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lightyellow"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>lightyellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-left:",
|
||||||
|
"message": "Include the <kbd>border-left</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-left",
|
||||||
|
"expected": "3px solid gold"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>border-left: 3px solid gold</kbd> to create a left border"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "padding-left:",
|
||||||
|
"message": "Include the <kbd>padding-left</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "padding-left",
|
||||||
|
"expected": "10px"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>padding-left: 10px</kbd> to add left padding"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "universal-selector",
|
||||||
|
"title": "Universal (*)",
|
||||||
|
"description": "The universal selector is denoted by an asterisk (*) and matches any element of any type. It selects everything in the document or, when combined with other selectors, everything within a specific context. For example, <kbd>* { margin: 0; }</kbd> removes margins from all elements, while <kbd>article *</kbd> selects all elements inside article elements. When using the universal selector in combination with other selectors, you can apply properties like <kbd>margin</kbd> to control the spacing around elements. The universal selector is powerful but should be used carefully due to its broad impact. It's commonly used in CSS resets, to override default browser styling, or to target all children of a particular element.",
|
||||||
|
"task": "Use the universal selector to remove margins from all elements inside the container div. Create a rule using <kbd>div.container *</kbd> as the selector and set <kbd>margin: 0</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"container\">\n <h2>Inside Container</h2>\n <p>This paragraph is inside the container.</p>\n <ul>\n <li>List item inside container</li>\n </ul>\n</div>\n<p>This paragraph is outside the container and should not be affected.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use the universal selector to target all elements inside the container */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^div\\.container\\s+\\*\\s*{",
|
||||||
|
"message": "Use <kbd>div.container *</kbd> selector (with a space between container and *)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "margin:",
|
||||||
|
"message": "Include the <kbd>margin</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "margin",
|
||||||
|
"expected": "0"
|
||||||
|
},
|
||||||
|
"message": "Set margin to <kbd>0</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "specificity-basics",
|
||||||
|
"title": "Specificity",
|
||||||
|
"description": "CSS specificity determines which styles take precedence when multiple conflicting rules target the same element. Specificity follows a hierarchical system: inline styles have the highest specificity, followed by ID selectors, then class/attribute/pseudo-class selectors, and finally element/pseudo-element selectors. This can be conceptualized as a four-part score (inline, ID, class, element). When creating multiple rules that may target the same elements, you can use the <kbd>color</kbd> property to set text colors, and specificity will determine which color is actually applied. Understanding specificity is crucial for predictable styling and debugging CSS conflicts. When two selectors have equal specificity, the one that comes last in the stylesheet wins.",
|
||||||
|
"task": "Examine the existing CSS rules and add a new rule with higher specificity to override the text color of the paragraph. Create a rule using '.content p' as the selector and set color: green.",
|
||||||
|
"previewHTML": "<div class=\"content\">\n <p>What color will this paragraph be? Look at the CSS rules and their specificity.</p>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
|
||||||
|
"codePrefix": "/* These CSS rules target the same paragraph but have different specificity */\n\n/* Rule 1: Element selector (lowest specificity) */\np {\n color: red;\n}\n\n/* Rule 2: Descendant selector (higher specificity than just 'p') */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.content\\s+p\\s*{",
|
||||||
|
"message": "Use <kbd>.content p</kbd> as your selector (note the space between)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "green",
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
62
lessons/ar/00-welcome.json
Normal file
62
lessons/ar/00-welcome.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "welcome",
|
||||||
|
"title": "Code Crispies",
|
||||||
|
"description": "Welcome to Code Crispies - your interactive web development learning platform",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "get-started",
|
||||||
|
"title": "Get Started",
|
||||||
|
"description": "<strong>Code Crispies</strong> is a free, open-source platform for learning web development through hands-on exercises. No account required!<br><br><strong>What you'll learn:</strong><br>• <strong>HTML</strong> - Semantic elements, forms, tables, SVG (<em>HTML Block & Inline</em>, <em>HTML Forms</em>, <em>HTML Tables</em>)<br>• <strong>CSS</strong> - Selectors, box model, flexbox, animations (<em>CSS Selectors</em>, <em>CSS Box Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br><strong>How it works:</strong><br>1. Read the task in the left panel<br>2. Write code in the editor<br>3. See live results in the preview<br>4. Get instant feedback with hints<br><br><strong>Keyboard shortcuts:</strong> <kbd>Ctrl+Z</kbd> to undo, <kbd>Ctrl+Shift+Z</kbd> to redo<br><br><strong>More resources:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript solutions<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript technology roadmap",
|
||||||
|
"task": "Hello World",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; } h1 { color: #6366f1; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<h1>Hello World</h1>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "Hello",
|
||||||
|
"message": "Write 'Hello World'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "overview",
|
||||||
|
"title": "Overview",
|
||||||
|
"description": "<strong>You're ready!</strong> Open the menu (☰) to explore all modules.<br><br><strong>Recommended learning path:</strong><br>1. <em>HTML Block & Inline</em> - Understand container vs inline elements<br>2. <em>HTML Forms</em> - Build interactive forms with validation<br>3. <em>CSS Selectors</em> - Target elements precisely<br>4. <em>CSS Box Model</em> - Master padding, margin, borders<br>5. <em>CSS Flexbox</em> - Create flexible layouts<br>6. <em>CSS Animations</em> - Add motion and transitions<br><br><strong>Tips:</strong><br>• Use <em>Show Expected</em> to see the target result<br>• Your progress is saved automatically<br>• Try Emmet in HTML mode: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Made by <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>",
|
||||||
|
"task": "Click Next to continue",
|
||||||
|
"previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"solution": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "Hello",
|
||||||
|
"message": "Click Next to continue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "playground",
|
||||||
|
"title": "Playground",
|
||||||
|
"mode": "playground",
|
||||||
|
"description": "",
|
||||||
|
"task": "",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Start coding!</p>",
|
||||||
|
"solution": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
180
lessons/ar/01-box-model.json
Normal file
180
lessons/ar/01-box-model.json
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "box-model",
|
||||||
|
"title": "CSS Box Model",
|
||||||
|
"description": "Master the fundamental principles of space management in web design through the CSS box model. This module explores how content, padding, borders, and margins combine to create layout structures that are both visually appealing and structurally sound.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "box-model-1",
|
||||||
|
"title": "Box Model Components",
|
||||||
|
"description": "The CSS box model consists of four concentric layers: content area (innermost), padding, border, and margin (outermost). Understanding how these components interact is essential for precise layout control.",
|
||||||
|
"task": "Add <kbd>padding: 1rem</kbd> to <kbd>.box</kbd> to create space between its content and border.",
|
||||||
|
"previewHTML": "<div class=\"box\">Box Model Components</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".box {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "padding: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "padding", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>padding: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-2",
|
||||||
|
"title": "Adding Borders",
|
||||||
|
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.",
|
||||||
|
"task": "Add <kbd>border: 2px solid darkslategray</kbd> to <kbd>.box</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">This box needs a border</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".box {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "border: 2px solid darkslategray;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border:\\s*2px\\s+solid\\s+darkslategray",
|
||||||
|
"message": "Set <kbd>border: 2px solid darkslategray</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-3",
|
||||||
|
"title": "Adding Margins",
|
||||||
|
"description": "Margins create space between elements, controlling how they relate to one another within a layout. Unlike padding (which affects internal spacing), margins exist outside the element's border.",
|
||||||
|
"task": "Add <kbd>margin: 1rem</kbd> to <kbd>.outer</kbd> to create space between it and the adjacent element.",
|
||||||
|
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".outer {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "margin", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>margin: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-4",
|
||||||
|
"title": "Box Sizing: Border-Box",
|
||||||
|
"description": "The <kbd>box-sizing</kbd> property determines how element dimensions are calculated. The default <kbd>content-box</kbd> excludes padding and border from width/height, while <kbd>border-box</kbd> includes them, making layout calculations more intuitive.",
|
||||||
|
"task": "Add <kbd>box-sizing: border-box</kbd> to <kbd>.sized</kbd> so padding and border are included in its width.",
|
||||||
|
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".sized {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "box-sizing: border-box;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||||
|
"message": "Set <kbd>box-sizing: border-box</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-5",
|
||||||
|
"title": "Margin Collapse",
|
||||||
|
"description": "When two vertical margins meet, they collapse to the larger value instead of adding up. Understanding this behavior is crucial for consistent vertical spacing.",
|
||||||
|
"task": "Add <kbd>margin-bottom: 2rem</kbd> to <kbd>.first</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.",
|
||||||
|
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">This paragraph has a bottom margin.</p><p class=\"second\">This paragraph has a top margin of 1rem.</p></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".first {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin-bottom: 2rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "margin-bottom", "expected": "2rem" },
|
||||||
|
"message": "Set <kbd>margin-bottom: 2rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-6",
|
||||||
|
"title": "Margin Shorthand Notation",
|
||||||
|
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.",
|
||||||
|
"task": "Add <kbd>margin: 1rem 2rem</kbd> to <kbd>.spaced</kbd> for 1rem top/bottom and 2rem left/right.",
|
||||||
|
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".spaced {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin: 1rem 2rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "margin:\\s*1rem\\s+2rem",
|
||||||
|
"message": "Set <kbd>margin: 1rem 2rem</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-7",
|
||||||
|
"title": "Padding Shorthand Notation",
|
||||||
|
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.",
|
||||||
|
"task": "Add <kbd>padding: 1.5rem</kbd> to <kbd>.padded</kbd> to add equal padding on all sides.",
|
||||||
|
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".padded {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "padding: 1.5rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "padding", "expected": "1.5rem" },
|
||||||
|
"message": "Set <kbd>padding: 1.5rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-8",
|
||||||
|
"title": "Border on Specific Sides",
|
||||||
|
"description": "For granular control, you can target specific sides with <kbd>border-top</kbd>, <kbd>border-right</kbd>, <kbd>border-bottom</kbd>, or <kbd>border-left</kbd>.",
|
||||||
|
"task": "Add <kbd>border-bottom: 4px solid dodgerblue</kbd> to <kbd>.line</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".line {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "border-bottom: 4px solid dodgerblue;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue",
|
||||||
|
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
116
lessons/ar/05-units-variables.json
Normal file
116
lessons/ar/05-units-variables.json
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "units-variables",
|
||||||
|
"title": "CSS Units & Variables",
|
||||||
|
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "units-1",
|
||||||
|
"title": "Absolute vs. Relative Units",
|
||||||
|
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.",
|
||||||
|
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">Resize me!</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set flexible sizing */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: 80%;\n max-width: 37.5rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "width", "message": "Use <kbd>width</kbd> property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to <kbd>80%</kbd>" },
|
||||||
|
{ "type": "contains", "value": "max-width", "message": "Use <kbd>max-width</kbd> property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "max-width", "expected": "37.5rem" },
|
||||||
|
"message": "Set max-width to <kbd>37.5rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-2",
|
||||||
|
"title": "CSS Custom Properties",
|
||||||
|
"description": "Define and reuse variables (--custom properties) to centralize your theme values.",
|
||||||
|
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>#6200ee</kbd> and apply it as the border color on <kbd>.themed</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"themed\">Variable Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define and use a CSS variable */\n:root {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.themed { }",
|
||||||
|
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "--main-color",
|
||||||
|
"message": "Define <kbd>--main-color</kbd> in :root",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "var(--main-color)",
|
||||||
|
"message": "Use <kbd>var(--main-color)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "border", "expected": "var(--main-color)" },
|
||||||
|
"message": "Apply variable to border color",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-3",
|
||||||
|
"title": "Unit Calculations (calc)",
|
||||||
|
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.",
|
||||||
|
"task": "Set the <kbd>width</kbd> of <kbd>.sized</kbd> to <kbd>calc(100% - 2rem)</kbd> and <kbd>min-height</kbd> to <kbd>calc(10vh + 1rem)</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"sized\">Calc Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "width:\\s*calc\\(100% - 2rem\\)",
|
||||||
|
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
|
||||||
|
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-4",
|
||||||
|
"title": "Viewport & Responsive Units",
|
||||||
|
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.",
|
||||||
|
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"view\">Viewport Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use viewport units */\n.view {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: 50vw;\n height: 20vh;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "vw", "message": "Use <kbd>vw</kbd> unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "vh", "message": "Use <kbd>vh</kbd> unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" },
|
||||||
|
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
145
lessons/ar/06-transitions-animations.json
Normal file
145
lessons/ar/06-transitions-animations.json
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "transitions-animations",
|
||||||
|
"title": "CSS Animations",
|
||||||
|
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "transitions-1",
|
||||||
|
"title": "Transitions",
|
||||||
|
"description": "Learn how to apply <kbd>transition</kbd> to properties for smooth changes on state changes.",
|
||||||
|
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Hover Me</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add transition */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " transition: background-color 0.3s;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition",
|
||||||
|
"message": "Use the <kbd>transition</kbd> property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||||
|
"message": "Set <kbd>transition: background-color 0.3s</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-2",
|
||||||
|
"title": "Timing Funcs",
|
||||||
|
"description": "Explore easing functions like <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> to control animation pacing.",
|
||||||
|
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Timing</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set timing function */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " transition-timing-function: ease-in-out;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition-timing-function",
|
||||||
|
"message": "Use <kbd>transition-timing-function</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||||
|
"message": "Set timing to <kbd>ease-in-out</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-3",
|
||||||
|
"title": "Keyframes",
|
||||||
|
"description": "Create named animations using <kbd>@keyframes</kbd> and apply them via the <kbd>animation</kbd> shorthand.",
|
||||||
|
"task": "Define a keyframe at <kbd>50%</kbd> with <kbd>transform: translateY(-20px)</kbd> and apply <kbd>animation: bounce 1s infinite</kbd> to <kbd>.ball</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"ball\"></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define keyframes and apply animation */\n@keyframes bounce {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.ball { }",
|
||||||
|
"solution": " 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "@keyframes bounce",
|
||||||
|
"message": "Define <kbd>@keyframes bounce</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "50%.*transform: translateY\\(-20px\\)",
|
||||||
|
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "animation",
|
||||||
|
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "animation:.*bounce.*1s.*infinite",
|
||||||
|
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-4",
|
||||||
|
"title": "Anim Properties",
|
||||||
|
"description": "Fine-tune animations with <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd>, and <kbd>animation-fill-mode</kbd>.",
|
||||||
|
"task": "Apply the <kbd>pulse</kbd> animation to <kbd>.box</kbd> with <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd>, and <kbd>animation-fill-mode: forwards</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">Pulse</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply animation properties */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " animation-name: pulse;\n animation-duration: 2s;\n animation-delay: 1s;\n animation-iteration-count: 2;\n animation-fill-mode: forwards;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-name", "expected": "pulse" },
|
||||||
|
"message": "Set <kbd>animation-name: pulse</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-duration", "expected": "2s" },
|
||||||
|
"message": "Set <kbd>animation-duration: 2s</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-delay", "expected": "1s" },
|
||||||
|
"message": "Set <kbd>animation-delay: 1s</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||||
|
"message": "Set <kbd>animation-iteration-count: 2</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||||
|
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
126
lessons/ar/08-responsive.json
Normal file
126
lessons/ar/08-responsive.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "responsive-design",
|
||||||
|
"title": "CSS Responsive Design",
|
||||||
|
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "responsive-1",
|
||||||
|
"title": "Media Queries",
|
||||||
|
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.",
|
||||||
|
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"panel\">Resize the window</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add your media query below */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"solution": "@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||||
|
"message": "Use <kbd>@media (max-width: 600px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".panel",
|
||||||
|
"message": "Target <kbd>.panel</kbd> inside the media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "background", "expected": "lightcoral" },
|
||||||
|
"message": "Set <kbd>background: lightcoral</kbd>",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-2",
|
||||||
|
"title": "Fluid Type",
|
||||||
|
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.",
|
||||||
|
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.",
|
||||||
|
"previewHTML": "<p class=\"text\">Fluid Typography</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply fluid font sizing */\n.text {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " font-size: 5vw;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-3",
|
||||||
|
"title": "Flex Grids",
|
||||||
|
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.",
|
||||||
|
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.cards</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Create a responsive grid */\n.cards {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "display", "expected": "grid" },
|
||||||
|
"message": "Set <kbd>display: grid</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||||
|
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "gap", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-4",
|
||||||
|
"title": "Mobile-First",
|
||||||
|
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.",
|
||||||
|
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.",
|
||||||
|
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add mobile-first enhancement */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"solution": "@media (min-width: 768px) {\n .sidebar {\n width: 250px;\n }\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||||
|
"message": "Use <kbd>@media (min-width: 768px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".sidebar",
|
||||||
|
"message": "Target <kbd>.sidebar</kbd> inside media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "width", "expected": "250px" },
|
||||||
|
"message": "Set <kbd>width: 250px</kbd>",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/ar/20-html-elements.json
Normal file
97
lessons/ar/20-html-elements.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-elements",
|
||||||
|
"title": "HTML Block & 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><div></kbd>, <kbd><p></kbd>, <kbd><h1></kbd>, <kbd><section></kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd><span></kbd>, <kbd><a></kbd>, <kbd><strong></kbd>, <kbd><em></kbd>",
|
||||||
|
"task": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (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>",
|
||||||
|
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Add a <kbd><p></kbd> paragraph element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "p", "child": "strong" },
|
||||||
|
"message": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "semantic-containers",
|
||||||
|
"title": "Semantic Tags",
|
||||||
|
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd><header></kbd> - Page or section header<br><kbd><nav></kbd> - Navigation links<br><kbd><main></kbd> - Main content area<br><kbd><section></kbd> - Thematic grouping<br><kbd><article></kbd> - Self-contained content<br><kbd><footer></kbd> - Page or section footer",
|
||||||
|
"task": "Create a basic page structure:<br>1. Add a <kbd><header></kbd> with an <kbd><h1></kbd> containing the text 'My Website'<br>2. Add a <kbd><main></kbd> element with a paragraph saying 'Welcome to my site!'<br>3. Add a <kbd><footer></kbd> with a paragraph saying 'Copyright 2025'",
|
||||||
|
"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": "",
|
||||||
|
"solution": "<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 <kbd><header></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "main",
|
||||||
|
"message": "Add a <kbd><main></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "footer",
|
||||||
|
"message": "Add a <kbd><footer></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "header", "child": "h1" },
|
||||||
|
"message": "Add an <kbd><h1></kbd> heading inside your header"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "div-vs-span",
|
||||||
|
"title": "div & span",
|
||||||
|
"description": "When you need a container without semantic meaning:<br><br><kbd><div></kbd> - Generic block container (for layout/grouping)<br><kbd><span></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><span></kbd> to style it differently. Wrap the whole quote in a <kbd><div></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.",
|
||||||
|
"solution": "<div>The most <span>highlighted</span> moment was unforgettable.</div>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "div",
|
||||||
|
"message": "Wrap everything in a <kbd><div></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "span",
|
||||||
|
"message": "Add a <kbd><span></kbd> around the word <kbd>highlighted</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_text",
|
||||||
|
"value": { "selector": "span", "text": "highlighted" },
|
||||||
|
"message": "The <kbd><span></kbd> should contain the word <kbd>highlighted</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/ar/21-html-forms-basic.json
Normal file
102
lessons/ar/21-html-forms-basic.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-basic",
|
||||||
|
"title": "HTML Forms",
|
||||||
|
"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><form></kbd> wrapper. Inside, use <kbd><label></kbd> to describe inputs and <kbd><input></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:<br>1. A <kbd><label></kbd> with the text 'Name:' and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd><input></kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> 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": "",
|
||||||
|
"solution": "<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 <kbd><form></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for your input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "input",
|
||||||
|
"message": "Add an <kbd><input></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "label", "attr": "for", "value": null },
|
||||||
|
"message": "Add a <kbd>for</kbd> attribute to your label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input", "attr": "id", "value": null },
|
||||||
|
"message": "Add an <kbd>id</kbd> 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 two fields:<br>1. An email field: <kbd><label for=\"email\">Email:</label></kbd> and <kbd><input type=\"email\" id=\"email\"></kbd><br>2. A password field: <kbd><label for=\"password\">Password:</label></kbd> and <kbd><input type=\"password\" id=\"password\"></kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n \n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\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><button type=\"submit\"></kbd> - Preferred, flexible content<br><kbd><input type=\"submit\"></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; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \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 <kbd>Sign In</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
112
lessons/ar/22-html-forms-validation.json
Normal file
112
lessons/ar/22-html-forms-validation.json
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-validation",
|
||||||
|
"title": "HTML 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><input type=\"text\" required></kbd><br><br>The browser shows a validation message automatically.",
|
||||||
|
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Submit</button>\n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \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 <kbd>required</kbd> attribute to the name input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
|
||||||
|
"message": "Add the <kbd>required</kbd> attribute to the email input"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "input-constraints",
|
||||||
|
"title": "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 to the password input:<br>1. Add <kbd>minlength=\"8\"</kbd> for minimum length<br>2. Add <kbd>maxlength=\"20\"</kbd> for maximum length<br>3. Add <kbd>placeholder=\"Enter password\"</kbd> as a hint",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } 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; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \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 <kbd>minlength</kbd>=\"8\" to the password input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
|
||||||
|
"message": "Add <kbd>maxlength</kbd>=\"20\" to the password input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
|
||||||
|
"message": "Add a <kbd>placeholder</kbd> to hint what to enter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "complete-registration",
|
||||||
|
"title": "Full 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; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } 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; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; 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 <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">Email *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Password *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
|
||||||
|
"solution": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Password *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n I agree to the Terms of Service *\n </label>\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 <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "type", "value": "email" },
|
||||||
|
"message": "Set the email input <kbd>type=\"email\"</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "required", "value": true },
|
||||||
|
"message": "Make the email field <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "type", "value": "password" },
|
||||||
|
"message": "Set the password input <kbd>type=\"password\"</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "required", "value": true },
|
||||||
|
"message": "Make the password field <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
|
||||||
|
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#terms", "attr": "required", "value": true },
|
||||||
|
"message": "Make the terms checkbox <kbd>required</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/ar/23-html-details-summary.json
Normal file
97
lessons/ar/23-html-details-summary.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-details-summary",
|
||||||
|
"title": "HTML Details & Summary",
|
||||||
|
"description": "Create expandable content sections without JavaScript",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "details-summary-basic",
|
||||||
|
"title": "First Widget",
|
||||||
|
"description": "The <kbd><details></kbd> element creates a collapsible section. The <kbd><summary></kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!",
|
||||||
|
"task": "Create a <kbd><details></kbd> element with:<br>1. A <kbd><summary></kbd> saying 'Click to reveal'<br>2. A <kbd><p></kbd> with the text 'This content was hidden!'",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<details>\n <summary>Click to reveal</summary>\n <p>This content was hidden!</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "details",
|
||||||
|
"message": "Add a <kbd><details></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "summary",
|
||||||
|
"message": "Add a <kbd><summary></kbd> inside the details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "details", "child": "summary" },
|
||||||
|
"message": "The <kbd><summary></kbd> must be inside <kbd><details></kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "details", "child": "p" },
|
||||||
|
"message": "Add a <kbd><p></kbd> inside <kbd><details></kbd> for the hidden content"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "details-open-attribute",
|
||||||
|
"title": "Pre-expanded Details",
|
||||||
|
"description": "By default, <kbd><details></kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.",
|
||||||
|
"task": "Add the <kbd>open</kbd> attribute to the <kbd><details></kbd> element to show the content by default.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<details>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
|
||||||
|
"solution": "<details open>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "details", "attr": "open", "value": true },
|
||||||
|
"message": "Add the <kbd>open</kbd> attribute to <kbd><details></kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "faq-accordion",
|
||||||
|
"title": "FAQ Accordion",
|
||||||
|
"description": "Multiple <kbd><details></kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3>summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>></kbd> nests inside, <kbd>+</kbd> adds siblings.",
|
||||||
|
"task": "Create an FAQ section with:<br>1. An <kbd><h1></kbd> saying 'Frequently Asked Questions'<br>2. Three <kbd><details></kbd> elements, each with a question in <kbd><summary></kbd> and an answer in <kbd><p></kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<h1>Frequently Asked Questions</h1>\n\n<details>\n <summary>What is HTML5?</summary>\n <p>HTML5 is the latest version of HTML with new semantic elements and APIs.</p>\n</details>\n\n<details>\n <summary>Do I need JavaScript?</summary>\n <p>Many interactive features work with pure HTML5, no JavaScript required!</p>\n</details>\n\n<details>\n <summary>Is this accessible?</summary>\n <p>Yes! Native HTML elements have built-in keyboard and screen reader support.</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "h1",
|
||||||
|
"message": "Add an <kbd><h1></kbd> heading for the FAQ title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "details", "min": 3 },
|
||||||
|
"message": "Create at least 3 <kbd><details></kbd> elements for the FAQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "summary", "min": 3 },
|
||||||
|
"message": "Each <kbd><details></kbd> needs a <kbd><summary></kbd> for the question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "details p", "min": 3 },
|
||||||
|
"message": "Each <kbd><details></kbd> needs a <kbd><p></kbd> for the answer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/ar/24-html-progress-meter.json
Normal file
102
lessons/ar/24-html-progress-meter.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-progress-meter",
|
||||||
|
"title": "HTML Progress & Meter",
|
||||||
|
"description": "Display completion status and scalar measurements natively",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "progress-basic",
|
||||||
|
"title": "Progress Bars",
|
||||||
|
"description": "The <kbd><progress></kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd><progress>...</progress></kbd> with fallback text inside for older browsers.",
|
||||||
|
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd><label></kbd> saying 'Download:'<br>2. Add a <kbd><progress></kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<label for=\"download\">Download:</label>\n<progress id=\"download\" value=\"70\" max=\"100\">70%</progress>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "progress",
|
||||||
|
"message": "Add a <kbd><progress></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "progress", "attr": "value", "value": "70" },
|
||||||
|
"message": "Set <kbd>value=</kbd>\"70\" on the progress element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "progress", "attr": "max", "value": "100" },
|
||||||
|
"message": "Set <kbd>max=</kbd>\"100\" on the progress element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for the progress bar"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "progress-indeterminate",
|
||||||
|
"title": "Indeterminate Progress",
|
||||||
|
"description": "When progress is unknown (like loading), omit the <kbd>value</kbd> attribute. This creates an animated indeterminate state.<br><br>Useful for network requests or processes with unknown duration.",
|
||||||
|
"task": "Create a loading indicator:<br>1. Add a <kbd><p></kbd> saying 'Loading...'<br>2. Add a <kbd><progress></kbd> without a value attribute",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<p>Loading...</p>\n<progress></progress>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "progress",
|
||||||
|
"message": "Add a <kbd><progress></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Add a <kbd><p></kbd> with loading text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "meter-gauge",
|
||||||
|
"title": "Meter Gauges",
|
||||||
|
"description": "The <kbd><meter></kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!",
|
||||||
|
"task": "Create a battery level meter:<br>1. Add a <kbd><label></kbd> saying 'Battery:'<br>2. Add a <kbd><meter></kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<label for=\"battery\">Battery:</label>\n<meter id=\"battery\" value=\"0.8\" min=\"0\" max=\"1\" low=\"0.2\" high=\"0.8\" optimum=\"1\">80%</meter>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "meter",
|
||||||
|
"message": "Add a <kbd><meter></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "meter", "attr": "value", "value": "0.8" },
|
||||||
|
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "meter", "attr": "low", "value": "0.2" },
|
||||||
|
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for the meter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
127
lessons/ar/30-html-tables.json
Normal file
127
lessons/ar/30-html-tables.json
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-tables",
|
||||||
|
"title": "HTML Tables",
|
||||||
|
"description": "Create structured data tables with headers and captions",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "table-basic",
|
||||||
|
"title": "Basic Table Structure",
|
||||||
|
"description": "Tables use <kbd><table></kbd> with <kbd><tr></kbd> for rows. Inside rows, use <kbd><th></kbd> for headers and <kbd><td></kbd> for data cells.<br><br>The <kbd><caption></kbd> element provides an accessible title for the table.",
|
||||||
|
"task": "Create a simple table with:<br>1. A <kbd><caption></kbd> saying 'Fruit Prices'<br>2. A header row with 'Fruit' and 'Price' columns<br>3. At least 2 data rows",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Fruit Prices</caption>\n <tr>\n <th>Fruit</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Apple</td>\n <td>$1.50</td>\n </tr>\n <tr>\n <td>Banana</td>\n <td>$0.75</td>\n </tr>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> for the table title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "th", "min": 2 },
|
||||||
|
"message": "Add at least 2 header cells (th)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tr", "min": 3 },
|
||||||
|
"message": "Add at least 3 rows (1 header + 2 data rows)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table-thead-tbody",
|
||||||
|
"title": "Table Head & Body",
|
||||||
|
"description": "Use <kbd><thead></kbd> to group header rows and <kbd><tbody></kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd><tfoot></kbd> for footer rows like totals.",
|
||||||
|
"task": "Create a structured table:<br>1. A <kbd><caption></kbd> with 'Monthly Sales'<br>2. A <kbd><thead></kbd> with Month and Revenue headers<br>3. A <kbd><tbody></kbd> with at least 2 data rows",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Monthly Sales</caption>\n <thead>\n <tr>\n <th>Month</th>\n <th>Revenue</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>January</td>\n <td>$12,500</td>\n </tr>\n <tr>\n <td>February</td>\n <td>$14,200</td>\n </tr>\n </tbody>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "thead",
|
||||||
|
"message": "Add a <kbd><thead></kbd> for the header section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tbody",
|
||||||
|
"message": "Add a <kbd><tbody></kbd> for the data rows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tbody tr", "min": 2 },
|
||||||
|
"message": "Add at least 2 data rows in tbody"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table-complete",
|
||||||
|
"title": "Complete Table with Footer",
|
||||||
|
"description": "Add <kbd><tfoot></kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
|
||||||
|
"task": "Create a complete table:<br>1. A <kbd><caption></kbd> with 'Order Summary'<br>2. A <kbd><thead></kbd> with Item and Price headers<br>3. A <kbd><tbody></kbd> with 2 items<br>4. A <kbd><tfoot></kbd> with a Total row",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Order Summary</caption>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>$25.00</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>$35.00</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Total</td>\n <td>$60.00</td>\n </tr>\n </tfoot>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "thead",
|
||||||
|
"message": "Add a <kbd><thead></kbd> section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tbody",
|
||||||
|
"message": "Add a <kbd><tbody></kbd> section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tfoot",
|
||||||
|
"message": "Add a <kbd><tfoot></kbd> section for the total"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tbody tr", "min": 2 },
|
||||||
|
"message": "Add at least 2 item rows in tbody"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
82
lessons/ar/31-html-marquee.json
Normal file
82
lessons/ar/31-html-marquee.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-marquee",
|
||||||
|
"title": "HTML Marquee",
|
||||||
|
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "marquee-basic",
|
||||||
|
"title": "Scrolling Text",
|
||||||
|
"description": "The <kbd><marquee></kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!",
|
||||||
|
"task": "Create a simple marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Put some text inside like 'Welcome to my website!'",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee>Welcome to my website!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marquee-direction",
|
||||||
|
"title": "Direction & Behavior",
|
||||||
|
"description": "Control the marquee with attributes:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (default), slide (stops at edge), alternate (bounces)<br>• <kbd>scrollamount</kbd>: speed (default is 6)",
|
||||||
|
"task": "Create a bouncing marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee behavior=\"alternate\">Bounce! Bounce! Bounce!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
|
||||||
|
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marquee-retro",
|
||||||
|
"title": "Retro News Ticker",
|
||||||
|
"description": "Combine multiple marquee attributes for a classic news ticker effect. You can even put multiple elements inside!<br><br>Remember: This is deprecated HTML. Modern sites use CSS animations, but marquee is great for understanding web history.",
|
||||||
|
"task": "Create a news ticker:<br>1. A <kbd><marquee></kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee direction=\"left\" scrollamount=\"5\">BREAKING NEWS: Marquee element still works in browsers!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "direction", "value": "left" },
|
||||||
|
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
|
||||||
|
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/ar/32-html-svg.json
Normal file
102
lessons/ar/32-html-svg.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-svg",
|
||||||
|
"title": "HTML SVG",
|
||||||
|
"description": "Draw scalable vector graphics directly in HTML",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "svg-circle",
|
||||||
|
"title": "Drawing Circles",
|
||||||
|
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd><svg></kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd><circle></kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.",
|
||||||
|
"task": "Create an SVG with a circle:<br>1. An <kbd><svg></kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd><circle></kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "circle",
|
||||||
|
"message": "Add a <kbd><circle></kbd> element inside the SVG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "circle", "attr": "cx", "value": "100" },
|
||||||
|
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "circle", "attr": "cy", "value": "100" },
|
||||||
|
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "svg-rect-line",
|
||||||
|
"title": "Rectangles & Lines",
|
||||||
|
"description": "Draw rectangles with <kbd><rect></kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd><line></kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!",
|
||||||
|
"task": "Create an SVG with:<br>1. An <kbd><svg></kbd> (200x150)<br>2. A <kbd><rect></kbd> at position (20,20) with size 80x60<br>3. A <kbd><line></kbd> from (120,30) to (180,90) with a stroke color",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "rect",
|
||||||
|
"message": "Add a <kbd><rect></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "line",
|
||||||
|
"message": "Add a <kbd><line></kbd> element"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "svg-shapes",
|
||||||
|
"title": "Multiple Shapes",
|
||||||
|
"description": "Combine shapes to create simple graphics! Add <kbd>stroke</kbd> for outlines and <kbd>stroke-width</kbd> for thickness.<br><br>Use <kbd>fill=\"none\"</kbd> for hollow shapes. Shapes stack in order - later elements appear on top.",
|
||||||
|
"task": "Create a simple face:<br>1. An <kbd><svg></kbd> (200x200)<br>2. A large <kbd><circle></kbd> for the face<br>3. Two small <kbd><circle></kbd> elements for eyes<br>4. A <kbd><line></kbd> for the smile",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "circle", "min": 3 },
|
||||||
|
"message": "Add at least 3 circles (1 face + 2 eyes)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "line",
|
||||||
|
"message": "Add a <kbd><line></kbd> for the smile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
173
lessons/ar/flexbox.json
Normal file
173
lessons/ar/flexbox.json
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "flexbox",
|
||||||
|
"title": "CSS Flexbox",
|
||||||
|
"description": "Master the flexible box layout model for modern responsive designs",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "flexbox-1",
|
||||||
|
"title": "Container",
|
||||||
|
"description": "Learn how to create a flex container and understand the main and cross axes.",
|
||||||
|
"task": "Add <kbd>display: flex</kbd> to <kbd>.wrap</kbd> to create a flexbox layout.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "display: flex;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "display",
|
||||||
|
"expected": "flex"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>display: flex</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-2",
|
||||||
|
"title": "Direction & Wrap",
|
||||||
|
"description": "Control the direction and wrapping of flex items within a container.",
|
||||||
|
"task": "Add <kbd>flex-direction: column</kbd> and <kbd>flex-wrap: wrap</kbd> to <kbd>.wrap</kbd>.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div><div class='box'>4</div><div class='box'>5</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; height: 16rem; display: flex; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "flex-direction: column;\n flex-wrap: wrap;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex-direction",
|
||||||
|
"expected": "column"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex-direction: column</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex-wrap",
|
||||||
|
"expected": "wrap"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex-wrap: wrap</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-3",
|
||||||
|
"title": "Justify Content",
|
||||||
|
"description": "Learn how to align flex items along the main axis of the flex container.",
|
||||||
|
"task": "Add <kbd>justify-content: space-between</kbd> to <kbd>.wrap</kbd> to distribute the boxes evenly.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "justify-content: space-between;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "justify-content",
|
||||||
|
"expected": "space-between"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>justify-content: space-between</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-4",
|
||||||
|
"title": "Align Items",
|
||||||
|
"description": "Control how flex items are aligned along the cross axis of the flex container.",
|
||||||
|
"task": "Add <kbd>align-items: center</kbd> to <kbd>.wrap</kbd> to vertically center the boxes.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box tall'>1</div><div class='box'>2</div><div class='box short'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; display: flex; justify-content: center; } .tall { height: 6rem; } .short { height: 3rem; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 10rem; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "align-items: center;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "align-items",
|
||||||
|
"expected": "center"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>align-items: center</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-5",
|
||||||
|
"title": "Flex Grow",
|
||||||
|
"description": "The <kbd>flex</kbd> property controls how much an item grows relative to others.",
|
||||||
|
"task": "Add <kbd>flex: 2</kbd> to <kbd>.box2</kbd> to make it grow twice as wide.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box box1'>1</div><div class='box box2'>2</div><div class='box box3'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; display: flex; align-items: center; justify-content: center; } .box1 { background: coral; flex: 1; } .box2 { background: mediumseagreen; } .box3 { background: gold; flex: 1; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
|
||||||
|
"codePrefix": ".box2 {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "flex: 2;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex",
|
||||||
|
"expected": "2"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex: 2</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-6",
|
||||||
|
"title": "Align Self",
|
||||||
|
"description": "Use <kbd>align-self</kbd> to override alignment for a single flex item.",
|
||||||
|
"task": "Add <kbd>align-self: flex-start</kbd> to <kbd>.middle</kbd> to move it to the top.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box middle'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; } .middle { background: mediumseagreen; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 12rem; align-items: center; }",
|
||||||
|
"codePrefix": ".middle {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "align-self: flex-start;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "align-self",
|
||||||
|
"expected": "flex-start"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>align-self: flex-start</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
550
lessons/es/00-basic-selectors.json
Normal file
550
lessons/es/00-basic-selectors.json
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "css-basic-selectors",
|
||||||
|
"title": "CSS Selectors",
|
||||||
|
"description": "CSS selectors are the foundation of styling web pages, allowing you to target specific HTML elements for styling. This module introduces fundamental selector types including element type selectors, class selectors, ID selectors, and the universal selector.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "introduction-to-selectors",
|
||||||
|
"title": "What's a Selector?",
|
||||||
|
"description": "A CSS selector is the first part of a CSS rule that tells the browser which HTML elements should receive the styles defined in the declaration block. Selectors are essentially patterns that match against elements in your HTML document. Understanding selectors is fundamental because they determine which elements your CSS rules will affect. The element or elements targeted by a selector are referred to as the 'subject of the selector.' When writing a CSS rule, you first specify the selector, followed by curly braces that contain the style declarations.<br/>For example, to change the text color of elements, you can use the <kbd>color</kbd> property within your declaration block.<br><br><pre>/* Element selector */\np {\n color: orangered;\n /* │ └─── Indicates the value of the expression\n │ \n └─────────── Indicates the property of the expression */\n}</pre>",
|
||||||
|
"task": "Write a CSS rule using a type selector that targets all paragraph elements <kbd>p</kbd> in the document. Make the text blue by setting the <kbd>color</kbd> property to <kbd>blue</kbd>.",
|
||||||
|
"previewHTML": "<h1>Introduction to CSS Selectors</h1>\n<p>This paragraph should turn blue.</p>\n<div>This div element should remain unchanged.</div>\n<p>This second paragraph should also turn blue.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Write a type selector to target all paragraph elements */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p { color: blue }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>p { … }</kbd> to select all paragraph elements",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color:</kbd> property in your CSS rule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "blue",
|
||||||
|
"message": "Set the color value to <kbd>blue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "blue"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>color: blue</kbd> to set the text color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type-selectors",
|
||||||
|
"title": "Type Selectors",
|
||||||
|
"description": "Type selectors (also called tag name selectors or element selectors) target HTML elements based on their tag name. For example, <kbd>p</kbd> selects all paragraph elements, <kbd>h1</kbd> selects all level-one headings, and <kbd>div</kbd> selects all division elements. Type selectors are the most fundamental way to select elements, applying styles consistently to all instances of a particular HTML element throughout your document. You can define a variety of CSS properties with type selectors, such as <kbd>color</kbd> for text color, <kbd>background-color</kbd> for the background, and <kbd>font-weight</kbd> for text emphasis. They provide a broad approach for styling your page and are often the starting point for more specific styling using other selector types.",
|
||||||
|
"task": "Write three separate CSS rules using type selectors to target specific HTML elements: make <kbd>h2</kbd> headings <kbd>purple</kbd>, give <kbd>span</kbd> elements a <kbd>yellow</kbd> background, and make <kbd>strong</kbd> elements <kbd>red</kbd>.",
|
||||||
|
"previewHTML": "<h2>Type Selectors Example</h2>\n<p>Regular paragraph text <span>with a highlighted span</span> that should have a yellow background.</p>\n<p>Another paragraph with <strong>strong important text</strong> that should be red.</p>\n<h2>Another Heading</h2>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
|
||||||
|
"codePrefix": "/* Write three separate type selectors below */\n\n",
|
||||||
|
"initialCode": "/* 1. Make h2 headings purple */\n\n\n/* 2. Give span elements a yellow background */\n\n\n/* 3. Make strong elements red */\n",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "/* 1. Make h2 headings purple */\nh2 {\n color: purple;\n}\n\n/* 2. Give span elements a yellow background */\nspan {\n background-color: yellow;\n}\n\n/* 3. Make strong elements red */\nstrong {\n color: red;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^h2\\s*{",
|
||||||
|
"message": "Include an <kbd>h2 { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Set the <kbd>color</kbd> property to <kbd>purple</kbd> for h2 elements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "h2\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your h2 rule with a closing brace <kbd>}</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\s*{",
|
||||||
|
"message": "Include a <kbd>span { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Set a <kbd>background-color: yellow</kbd> for span elements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your span rule with a closing brace <kbd>}</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^strong\\s*{",
|
||||||
|
"message": "Include a <kbd>strong { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
|
||||||
|
"message": "Set the <kbd>color: red</kbd> for strong elements"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-selectors",
|
||||||
|
"title": "Class Selectors",
|
||||||
|
"description": "Class selectors target elements with a specific class attribute value. They begin with a dot (.) followed by the class name. Classes are powerful because they allow you to apply the same styles to multiple elements regardless of their type. An HTML element can have multiple classes (separated by spaces in the class attribute), and a class can be applied to any number of elements. When using class selectors, you can apply properties like <kbd>background-color</kbd> to set the background color of elements, and <kbd>font-weight</kbd> to control text thickness, making text bold or normal. This flexibility makes class selectors one of the most commonly used methods for applying styles in CSS, allowing for modular and reusable styling across your website.",
|
||||||
|
"task": "Create a CSS rule using a class selector that targets elements with the class <kbd>highlight</kbd>. Give these elements a <kbd>yellow</kbd> background and <kbd>bold</kbd> text.",
|
||||||
|
"previewHTML": "<h2>Using Class Selectors</h2>\n<p>This is a regular paragraph, but <span class=\"highlight\">this span has the highlight class</span> applied to it.</p>\n<p class=\"highlight\">This entire paragraph has the highlight class.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"highlight\">This list item is highlighted</li>\n</ul>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
|
||||||
|
"codePrefix": "/* Create a class selector for elements with the 'highlight' class */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.highlight\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>.highlight { … }</kbd> to create a class selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color:</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>yellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-weight:",
|
||||||
|
"message": "Include the <kbd>font-weight:</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-weight",
|
||||||
|
"expected": "bold"
|
||||||
|
},
|
||||||
|
"message": "Set the font-weight to <kbd>bold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "multiple-classes",
|
||||||
|
"title": "Multiple Classes",
|
||||||
|
"description": "HTML elements can have multiple classes applied simultaneously, allowing for composable and modular CSS designs. When an element has multiple classes, it will receive styles from all matching class selectors. This approach enables you to build a library of reusable CSS classes that can be combined in different ways. You can also target elements that have a specific combination of classes by chaining class selectors together without spaces (e.g., <kbd>.class1.class2</kbd>). When styling these elements, you might use properties like <kbd>border-color</kbd> to change the color of element borders, and <kbd>background-color</kbd> to set the background color of elements. This technique lets you create conditional styles that only apply when certain classes appear together.",
|
||||||
|
"task": "Complete the CSS rule that targets elements with both <kbd>card</kbd> and <kbd>featured</kbd> classes by chaining the selectors. Set the border-color to gold and the background-color to lemonchiffon to make featured cards stand out.",
|
||||||
|
"previewHTML": "<h2>Multiple Class Combinations</h2>\n<div class=\"card\">Regular Card</div>\n<div class=\"card featured\">Featured Card</div>\n<div class=\"featured\">Just Featured (not a card)</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* The .card class already has basic styling */\n/* Now target elements with BOTH classes: 'card' AND 'featured' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.card\\.featured\\s*{",
|
||||||
|
"message": "Chain the selectors as <kbd>.card.featured</kbd> (no space between them)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-color:",
|
||||||
|
"message": "Include the <kbd>border-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-color",
|
||||||
|
"expected": "gold"
|
||||||
|
},
|
||||||
|
"message": "Set the border color to <kbd>gold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*;",
|
||||||
|
"message": "Make sure to end your CSS rule with a semicolon <kbd>;</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lemonchiffon"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>lemonchiffon</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-with-type",
|
||||||
|
"title": "Combining Types",
|
||||||
|
"description": "You can combine type selectors with class selectors to target specific HTML elements that have a certain class. This creates a more specific selector that only matches when both conditions are true: the element is of the specified type AND it has the specified class. For example, <kbd>p.note</kbd> would select paragraph elements with the class <kbd>note</kbd>, but would not select divs or spans with that same class. You can style these combined selections using properties like <kbd>background-color</kbd> to set a colored background for your elements. This approach allows you to apply different styles to the same class when it appears on different element types.",
|
||||||
|
"task": "Create a CSS rule that specifically targets <kbd><span></kbd> elements with the class <kbd>highlight</kbd>. Make those elements have an orange background, while other elements with the highlight class remain untouched.",
|
||||||
|
"previewHTML": "<h2>Type and Class Combinations</h2>\n<p>This paragraph has a <span class=\"highlight\">highlighted span</span> that should have an orange background.</p>\n<p class=\"highlight\">This paragraph has the highlight class but should NOT have an orange background.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
|
||||||
|
"sandboxCSS": "h2, p, span { padding: 5px; }",
|
||||||
|
"codePrefix": "/* The .highlight class already sets font-weight to bold */\n/* Now target ONLY span elements with the highlight class */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\.highlight\\s*{",
|
||||||
|
"message": "Use <kbd>span.highlight</kbd> selector (no space between element and class)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "orange"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>orange</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-selectors",
|
||||||
|
"title": "ID Selectors",
|
||||||
|
"description": "ID selectors target elements with a specific id attribute. They begin with a hash/pound sign (#) followed by the ID name. Unlike classes, IDs must be unique within a document—each ID value should be used only once per page. ID selectors have higher specificity than class or element selectors, meaning they override those selectors when conflicts arise. When styling with ID selectors, you can use properties like <kbd>color</kbd> to define text color, and <kbd>text-decoration</kbd> to control the appearance of text, such as adding underlines to elements. Because of their uniqueness requirement, IDs are best used for one-of-a-kind elements like page headers, main navigation, or specific unique components that appear only once on a page.",
|
||||||
|
"task": "Create a CSS rule with an ID selector that targets the element with the ID <kbd>main-title</kbd>. Set its color to purple and add an underline with <kbd>text-decoration: underline</kbd>.",
|
||||||
|
"previewHTML": "<h1 id=\"main-title\">Main Page Title</h1>\n<p>Regular paragraph content.</p>\n<h2>Secondary Heading</h2>\n<p id=\"intro\">Introduction paragraph (different ID).</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Create an ID selector to target the element with id=\"main-title\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^#main-title\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>#main-title</kbd> to create an ID selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Set the color to <kbd>purple</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-decoration:",
|
||||||
|
"message": "Include the <kbd>text-decoration</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "text-decoration",
|
||||||
|
"expected": "underline"
|
||||||
|
},
|
||||||
|
"message": "Set the text-decoration to <kbd>underline</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "#main-title\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-with-type",
|
||||||
|
"title": "Type + ID",
|
||||||
|
"description": "Similar to how you can combine type and class selectors, you can also combine type selectors with ID selectors. For example, <kbd>h1#title</kbd> targets an h1 element with the ID 'title'. When using this combined approach, you can apply CSS properties like <kbd>font-style</kbd> to control the slant of the text, making it italic or normal. While this selector combination is more specific than using just the ID selector, it's often unnecessary since IDs should already be unique in a document. However, this technique can be useful for improving code readability or when you want to emphasize that a particular ID should only appear on a specific element type.",
|
||||||
|
"task": "Create a CSS rule that combines a type selector with an ID selector to target specifically a paragraph element with the ID <kbd>special</kbd>. Set its font style to italic.",
|
||||||
|
"previewHTML": "<h2 id=\"special\">Heading with ID \"special\" (should NOT be affected)</h2>\n<p id=\"special\">Paragraph with ID \"special\" (should become italic)</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Create a combined type+ID selector for a paragraph with id=\"special\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p#special\\s*{",
|
||||||
|
"message": "Use <kbd>p#special</kbd> to target paragraphs with ID <kbd>special</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-style:",
|
||||||
|
"message": "Include the <kbd>font-style</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-style",
|
||||||
|
"expected": "italic"
|
||||||
|
},
|
||||||
|
"message": "Set the font-style to <kbd>italic</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p#special\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "selector-lists",
|
||||||
|
"title": "Selector Lists",
|
||||||
|
"description": "When multiple elements need the same styling, you can group them together using a selector list (also known as grouping selectors). Selector lists are created by separating individual selectors with commas. This approach reduces repetition in your CSS, making it more maintainable and efficient. For example, <kbd>h1, h2, h3 { color: blue; }</kbd> applies the same blue color to all three heading levels. When styling multiple selectors at once, you can apply properties like <kbd>background-color</kbd> to set the background, <kbd>border-left</kbd> to create a left border with a specific thickness, style, and color, and <kbd>padding-left</kbd> to create space between the content and the left border. Whitespace around commas is optional, and each selector in the list can be any valid selector type-elements, classes, IDs, or even more complex selectors.",
|
||||||
|
"task": "Create a selector list that applies the same styles to three different elements: paragraphs with class <kbd>note</kbd>, list items with class <kbd>important</kbd>, and the element with ID <kbd>summary</kbd>. Give them a <kbd>lightyellow</kbd> background, a <kbd>gold</kbd> left border, and some left <kbd>padding</kbd>.",
|
||||||
|
"previewHTML": "<p class=\"note\">This is a note paragraph.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"important\">Important list item</li>\n</ul>\n<div id=\"summary\">Summary section</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
|
||||||
|
"codePrefix": "/* Create a selector list to apply the same styles to multiple different elements */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "p.note",
|
||||||
|
"message": "Include <kbd>p.note</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "li.important",
|
||||||
|
"message": "Include <kbd>li.important</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "#summary",
|
||||||
|
"message": "Include <kbd>#summary</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
|
||||||
|
"message": "Create a comma-separated list with all three selectors",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lightyellow"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>lightyellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-left:",
|
||||||
|
"message": "Include the <kbd>border-left</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-left",
|
||||||
|
"expected": "3px solid gold"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>border-left: 3px solid gold</kbd> to create a left border"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "padding-left:",
|
||||||
|
"message": "Include the <kbd>padding-left</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "padding-left",
|
||||||
|
"expected": "10px"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>padding-left: 10px</kbd> to add left padding"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "universal-selector",
|
||||||
|
"title": "Universal (*)",
|
||||||
|
"description": "The universal selector is denoted by an asterisk (*) and matches any element of any type. It selects everything in the document or, when combined with other selectors, everything within a specific context. For example, <kbd>* { margin: 0; }</kbd> removes margins from all elements, while <kbd>article *</kbd> selects all elements inside article elements. When using the universal selector in combination with other selectors, you can apply properties like <kbd>margin</kbd> to control the spacing around elements. The universal selector is powerful but should be used carefully due to its broad impact. It's commonly used in CSS resets, to override default browser styling, or to target all children of a particular element.",
|
||||||
|
"task": "Use the universal selector to remove margins from all elements inside the container div. Create a rule using <kbd>div.container *</kbd> as the selector and set <kbd>margin: 0</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"container\">\n <h2>Inside Container</h2>\n <p>This paragraph is inside the container.</p>\n <ul>\n <li>List item inside container</li>\n </ul>\n</div>\n<p>This paragraph is outside the container and should not be affected.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use the universal selector to target all elements inside the container */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^div\\.container\\s+\\*\\s*{",
|
||||||
|
"message": "Use <kbd>div.container *</kbd> selector (with a space between container and *)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "margin:",
|
||||||
|
"message": "Include the <kbd>margin</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "margin",
|
||||||
|
"expected": "0"
|
||||||
|
},
|
||||||
|
"message": "Set margin to <kbd>0</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "specificity-basics",
|
||||||
|
"title": "Specificity",
|
||||||
|
"description": "CSS specificity determines which styles take precedence when multiple conflicting rules target the same element. Specificity follows a hierarchical system: inline styles have the highest specificity, followed by ID selectors, then class/attribute/pseudo-class selectors, and finally element/pseudo-element selectors. This can be conceptualized as a four-part score (inline, ID, class, element). When creating multiple rules that may target the same elements, you can use the <kbd>color</kbd> property to set text colors, and specificity will determine which color is actually applied. Understanding specificity is crucial for predictable styling and debugging CSS conflicts. When two selectors have equal specificity, the one that comes last in the stylesheet wins.",
|
||||||
|
"task": "Examine the existing CSS rules and add a new rule with higher specificity to override the text color of the paragraph. Create a rule using '.content p' as the selector and set color: green.",
|
||||||
|
"previewHTML": "<div class=\"content\">\n <p>What color will this paragraph be? Look at the CSS rules and their specificity.</p>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
|
||||||
|
"codePrefix": "/* These CSS rules target the same paragraph but have different specificity */\n\n/* Rule 1: Element selector (lowest specificity) */\np {\n color: red;\n}\n\n/* Rule 2: Descendant selector (higher specificity than just 'p') */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.content\\s+p\\s*{",
|
||||||
|
"message": "Use <kbd>.content p</kbd> as your selector (note the space between)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "green",
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
62
lessons/es/00-welcome.json
Normal file
62
lessons/es/00-welcome.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "welcome",
|
||||||
|
"title": "Code Crispies",
|
||||||
|
"description": "Welcome to Code Crispies - your interactive web development learning platform",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "get-started",
|
||||||
|
"title": "Get Started",
|
||||||
|
"description": "<strong>Code Crispies</strong> is a free, open-source platform for learning web development through hands-on exercises. No account required!<br><br><strong>What you'll learn:</strong><br>• <strong>HTML</strong> - Semantic elements, forms, tables, SVG (<em>HTML Block & Inline</em>, <em>HTML Forms</em>, <em>HTML Tables</em>)<br>• <strong>CSS</strong> - Selectors, box model, flexbox, animations (<em>CSS Selectors</em>, <em>CSS Box Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br><strong>How it works:</strong><br>1. Read the task in the left panel<br>2. Write code in the editor<br>3. See live results in the preview<br>4. Get instant feedback with hints<br><br><strong>Keyboard shortcuts:</strong> <kbd>Ctrl+Z</kbd> to undo, <kbd>Ctrl+Shift+Z</kbd> to redo<br><br><strong>More resources:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript solutions<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript technology roadmap",
|
||||||
|
"task": "Hello World",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; } h1 { color: #6366f1; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<h1>Hello World</h1>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "Hello",
|
||||||
|
"message": "Write 'Hello World'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "overview",
|
||||||
|
"title": "Overview",
|
||||||
|
"description": "<strong>You're ready!</strong> Open the menu (☰) to explore all modules.<br><br><strong>Recommended learning path:</strong><br>1. <em>HTML Block & Inline</em> - Understand container vs inline elements<br>2. <em>HTML Forms</em> - Build interactive forms with validation<br>3. <em>CSS Selectors</em> - Target elements precisely<br>4. <em>CSS Box Model</em> - Master padding, margin, borders<br>5. <em>CSS Flexbox</em> - Create flexible layouts<br>6. <em>CSS Animations</em> - Add motion and transitions<br><br><strong>Tips:</strong><br>• Use <em>Show Expected</em> to see the target result<br>• Your progress is saved automatically<br>• Try Emmet in HTML mode: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Made by <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>",
|
||||||
|
"task": "Click Next to continue",
|
||||||
|
"previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"solution": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "Hello",
|
||||||
|
"message": "Click Next to continue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "playground",
|
||||||
|
"title": "Playground",
|
||||||
|
"mode": "playground",
|
||||||
|
"description": "",
|
||||||
|
"task": "",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Start coding!</p>",
|
||||||
|
"solution": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
180
lessons/es/01-box-model.json
Normal file
180
lessons/es/01-box-model.json
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "box-model",
|
||||||
|
"title": "CSS Box Model",
|
||||||
|
"description": "Master the fundamental principles of space management in web design through the CSS box model. This module explores how content, padding, borders, and margins combine to create layout structures that are both visually appealing and structurally sound.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "box-model-1",
|
||||||
|
"title": "Box Model Components",
|
||||||
|
"description": "The CSS box model consists of four concentric layers: content area (innermost), padding, border, and margin (outermost). Understanding how these components interact is essential for precise layout control.",
|
||||||
|
"task": "Add <kbd>padding: 1rem</kbd> to <kbd>.box</kbd> to create space between its content and border.",
|
||||||
|
"previewHTML": "<div class=\"box\">Box Model Components</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".box {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "padding: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "padding", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>padding: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-2",
|
||||||
|
"title": "Adding Borders",
|
||||||
|
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.",
|
||||||
|
"task": "Add <kbd>border: 2px solid darkslategray</kbd> to <kbd>.box</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">This box needs a border</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".box {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "border: 2px solid darkslategray;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border:\\s*2px\\s+solid\\s+darkslategray",
|
||||||
|
"message": "Set <kbd>border: 2px solid darkslategray</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-3",
|
||||||
|
"title": "Adding Margins",
|
||||||
|
"description": "Margins create space between elements, controlling how they relate to one another within a layout. Unlike padding (which affects internal spacing), margins exist outside the element's border.",
|
||||||
|
"task": "Add <kbd>margin: 1rem</kbd> to <kbd>.outer</kbd> to create space between it and the adjacent element.",
|
||||||
|
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".outer {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "margin", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>margin: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-4",
|
||||||
|
"title": "Box Sizing: Border-Box",
|
||||||
|
"description": "The <kbd>box-sizing</kbd> property determines how element dimensions are calculated. The default <kbd>content-box</kbd> excludes padding and border from width/height, while <kbd>border-box</kbd> includes them, making layout calculations more intuitive.",
|
||||||
|
"task": "Add <kbd>box-sizing: border-box</kbd> to <kbd>.sized</kbd> so padding and border are included in its width.",
|
||||||
|
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".sized {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "box-sizing: border-box;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||||
|
"message": "Set <kbd>box-sizing: border-box</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-5",
|
||||||
|
"title": "Margin Collapse",
|
||||||
|
"description": "When two vertical margins meet, they collapse to the larger value instead of adding up. Understanding this behavior is crucial for consistent vertical spacing.",
|
||||||
|
"task": "Add <kbd>margin-bottom: 2rem</kbd> to <kbd>.first</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.",
|
||||||
|
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">This paragraph has a bottom margin.</p><p class=\"second\">This paragraph has a top margin of 1rem.</p></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".first {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin-bottom: 2rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "margin-bottom", "expected": "2rem" },
|
||||||
|
"message": "Set <kbd>margin-bottom: 2rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-6",
|
||||||
|
"title": "Margin Shorthand Notation",
|
||||||
|
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.",
|
||||||
|
"task": "Add <kbd>margin: 1rem 2rem</kbd> to <kbd>.spaced</kbd> for 1rem top/bottom and 2rem left/right.",
|
||||||
|
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".spaced {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin: 1rem 2rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "margin:\\s*1rem\\s+2rem",
|
||||||
|
"message": "Set <kbd>margin: 1rem 2rem</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-7",
|
||||||
|
"title": "Padding Shorthand Notation",
|
||||||
|
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.",
|
||||||
|
"task": "Add <kbd>padding: 1.5rem</kbd> to <kbd>.padded</kbd> to add equal padding on all sides.",
|
||||||
|
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".padded {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "padding: 1.5rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "padding", "expected": "1.5rem" },
|
||||||
|
"message": "Set <kbd>padding: 1.5rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-8",
|
||||||
|
"title": "Border on Specific Sides",
|
||||||
|
"description": "For granular control, you can target specific sides with <kbd>border-top</kbd>, <kbd>border-right</kbd>, <kbd>border-bottom</kbd>, or <kbd>border-left</kbd>.",
|
||||||
|
"task": "Add <kbd>border-bottom: 4px solid dodgerblue</kbd> to <kbd>.line</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".line {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "border-bottom: 4px solid dodgerblue;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue",
|
||||||
|
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
116
lessons/es/05-units-variables.json
Normal file
116
lessons/es/05-units-variables.json
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "units-variables",
|
||||||
|
"title": "CSS Units & Variables",
|
||||||
|
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "units-1",
|
||||||
|
"title": "Absolute vs. Relative Units",
|
||||||
|
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.",
|
||||||
|
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">Resize me!</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set flexible sizing */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: 80%;\n max-width: 37.5rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "width", "message": "Use <kbd>width</kbd> property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to <kbd>80%</kbd>" },
|
||||||
|
{ "type": "contains", "value": "max-width", "message": "Use <kbd>max-width</kbd> property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "max-width", "expected": "37.5rem" },
|
||||||
|
"message": "Set max-width to <kbd>37.5rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-2",
|
||||||
|
"title": "CSS Custom Properties",
|
||||||
|
"description": "Define and reuse variables (--custom properties) to centralize your theme values.",
|
||||||
|
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>#6200ee</kbd> and apply it as the border color on <kbd>.themed</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"themed\">Variable Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define and use a CSS variable */\n:root {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.themed { }",
|
||||||
|
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "--main-color",
|
||||||
|
"message": "Define <kbd>--main-color</kbd> in :root",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "var(--main-color)",
|
||||||
|
"message": "Use <kbd>var(--main-color)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "border", "expected": "var(--main-color)" },
|
||||||
|
"message": "Apply variable to border color",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-3",
|
||||||
|
"title": "Unit Calculations (calc)",
|
||||||
|
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.",
|
||||||
|
"task": "Set the <kbd>width</kbd> of <kbd>.sized</kbd> to <kbd>calc(100% - 2rem)</kbd> and <kbd>min-height</kbd> to <kbd>calc(10vh + 1rem)</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"sized\">Calc Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "width:\\s*calc\\(100% - 2rem\\)",
|
||||||
|
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
|
||||||
|
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-4",
|
||||||
|
"title": "Viewport & Responsive Units",
|
||||||
|
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.",
|
||||||
|
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"view\">Viewport Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use viewport units */\n.view {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: 50vw;\n height: 20vh;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "vw", "message": "Use <kbd>vw</kbd> unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "vh", "message": "Use <kbd>vh</kbd> unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" },
|
||||||
|
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
145
lessons/es/06-transitions-animations.json
Normal file
145
lessons/es/06-transitions-animations.json
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "transitions-animations",
|
||||||
|
"title": "CSS Animations",
|
||||||
|
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "transitions-1",
|
||||||
|
"title": "Transitions",
|
||||||
|
"description": "Learn how to apply <kbd>transition</kbd> to properties for smooth changes on state changes.",
|
||||||
|
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Hover Me</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add transition */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " transition: background-color 0.3s;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition",
|
||||||
|
"message": "Use the <kbd>transition</kbd> property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||||
|
"message": "Set <kbd>transition: background-color 0.3s</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-2",
|
||||||
|
"title": "Timing Funcs",
|
||||||
|
"description": "Explore easing functions like <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> to control animation pacing.",
|
||||||
|
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Timing</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set timing function */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " transition-timing-function: ease-in-out;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition-timing-function",
|
||||||
|
"message": "Use <kbd>transition-timing-function</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||||
|
"message": "Set timing to <kbd>ease-in-out</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-3",
|
||||||
|
"title": "Keyframes",
|
||||||
|
"description": "Create named animations using <kbd>@keyframes</kbd> and apply them via the <kbd>animation</kbd> shorthand.",
|
||||||
|
"task": "Define a keyframe at <kbd>50%</kbd> with <kbd>transform: translateY(-20px)</kbd> and apply <kbd>animation: bounce 1s infinite</kbd> to <kbd>.ball</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"ball\"></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define keyframes and apply animation */\n@keyframes bounce {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.ball { }",
|
||||||
|
"solution": " 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "@keyframes bounce",
|
||||||
|
"message": "Define <kbd>@keyframes bounce</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "50%.*transform: translateY\\(-20px\\)",
|
||||||
|
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "animation",
|
||||||
|
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "animation:.*bounce.*1s.*infinite",
|
||||||
|
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-4",
|
||||||
|
"title": "Anim Properties",
|
||||||
|
"description": "Fine-tune animations with <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd>, and <kbd>animation-fill-mode</kbd>.",
|
||||||
|
"task": "Apply the <kbd>pulse</kbd> animation to <kbd>.box</kbd> with <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd>, and <kbd>animation-fill-mode: forwards</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">Pulse</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply animation properties */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " animation-name: pulse;\n animation-duration: 2s;\n animation-delay: 1s;\n animation-iteration-count: 2;\n animation-fill-mode: forwards;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-name", "expected": "pulse" },
|
||||||
|
"message": "Set <kbd>animation-name: pulse</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-duration", "expected": "2s" },
|
||||||
|
"message": "Set <kbd>animation-duration: 2s</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-delay", "expected": "1s" },
|
||||||
|
"message": "Set <kbd>animation-delay: 1s</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||||
|
"message": "Set <kbd>animation-iteration-count: 2</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||||
|
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
126
lessons/es/08-responsive.json
Normal file
126
lessons/es/08-responsive.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "responsive-design",
|
||||||
|
"title": "CSS Responsive Design",
|
||||||
|
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "responsive-1",
|
||||||
|
"title": "Media Queries",
|
||||||
|
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.",
|
||||||
|
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"panel\">Resize the window</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add your media query below */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"solution": "@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||||
|
"message": "Use <kbd>@media (max-width: 600px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".panel",
|
||||||
|
"message": "Target <kbd>.panel</kbd> inside the media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "background", "expected": "lightcoral" },
|
||||||
|
"message": "Set <kbd>background: lightcoral</kbd>",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-2",
|
||||||
|
"title": "Fluid Type",
|
||||||
|
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.",
|
||||||
|
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.",
|
||||||
|
"previewHTML": "<p class=\"text\">Fluid Typography</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply fluid font sizing */\n.text {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " font-size: 5vw;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-3",
|
||||||
|
"title": "Flex Grids",
|
||||||
|
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.",
|
||||||
|
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.cards</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Create a responsive grid */\n.cards {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "display", "expected": "grid" },
|
||||||
|
"message": "Set <kbd>display: grid</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||||
|
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "gap", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-4",
|
||||||
|
"title": "Mobile-First",
|
||||||
|
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.",
|
||||||
|
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.",
|
||||||
|
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add mobile-first enhancement */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"solution": "@media (min-width: 768px) {\n .sidebar {\n width: 250px;\n }\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||||
|
"message": "Use <kbd>@media (min-width: 768px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".sidebar",
|
||||||
|
"message": "Target <kbd>.sidebar</kbd> inside media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "width", "expected": "250px" },
|
||||||
|
"message": "Set <kbd>width: 250px</kbd>",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/es/20-html-elements.json
Normal file
97
lessons/es/20-html-elements.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-elements",
|
||||||
|
"title": "HTML Block & 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><div></kbd>, <kbd><p></kbd>, <kbd><h1></kbd>, <kbd><section></kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd><span></kbd>, <kbd><a></kbd>, <kbd><strong></kbd>, <kbd><em></kbd>",
|
||||||
|
"task": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (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>",
|
||||||
|
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Add a <kbd><p></kbd> paragraph element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "p", "child": "strong" },
|
||||||
|
"message": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "semantic-containers",
|
||||||
|
"title": "Semantic Tags",
|
||||||
|
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd><header></kbd> - Page or section header<br><kbd><nav></kbd> - Navigation links<br><kbd><main></kbd> - Main content area<br><kbd><section></kbd> - Thematic grouping<br><kbd><article></kbd> - Self-contained content<br><kbd><footer></kbd> - Page or section footer",
|
||||||
|
"task": "Create a basic page structure:<br>1. Add a <kbd><header></kbd> with an <kbd><h1></kbd> containing the text 'My Website'<br>2. Add a <kbd><main></kbd> element with a paragraph saying 'Welcome to my site!'<br>3. Add a <kbd><footer></kbd> with a paragraph saying 'Copyright 2025'",
|
||||||
|
"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": "",
|
||||||
|
"solution": "<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 <kbd><header></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "main",
|
||||||
|
"message": "Add a <kbd><main></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "footer",
|
||||||
|
"message": "Add a <kbd><footer></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "header", "child": "h1" },
|
||||||
|
"message": "Add an <kbd><h1></kbd> heading inside your header"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "div-vs-span",
|
||||||
|
"title": "div & span",
|
||||||
|
"description": "When you need a container without semantic meaning:<br><br><kbd><div></kbd> - Generic block container (for layout/grouping)<br><kbd><span></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><span></kbd> to style it differently. Wrap the whole quote in a <kbd><div></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.",
|
||||||
|
"solution": "<div>The most <span>highlighted</span> moment was unforgettable.</div>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "div",
|
||||||
|
"message": "Wrap everything in a <kbd><div></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "span",
|
||||||
|
"message": "Add a <kbd><span></kbd> around the word <kbd>highlighted</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_text",
|
||||||
|
"value": { "selector": "span", "text": "highlighted" },
|
||||||
|
"message": "The <kbd><span></kbd> should contain the word <kbd>highlighted</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/es/21-html-forms-basic.json
Normal file
102
lessons/es/21-html-forms-basic.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-basic",
|
||||||
|
"title": "HTML Forms",
|
||||||
|
"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><form></kbd> wrapper. Inside, use <kbd><label></kbd> to describe inputs and <kbd><input></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:<br>1. A <kbd><label></kbd> with the text 'Name:' and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd><input></kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> 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": "",
|
||||||
|
"solution": "<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 <kbd><form></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for your input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "input",
|
||||||
|
"message": "Add an <kbd><input></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "label", "attr": "for", "value": null },
|
||||||
|
"message": "Add a <kbd>for</kbd> attribute to your label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input", "attr": "id", "value": null },
|
||||||
|
"message": "Add an <kbd>id</kbd> 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 two fields:<br>1. An email field: <kbd><label for=\"email\">Email:</label></kbd> and <kbd><input type=\"email\" id=\"email\"></kbd><br>2. A password field: <kbd><label for=\"password\">Password:</label></kbd> and <kbd><input type=\"password\" id=\"password\"></kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n \n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\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><button type=\"submit\"></kbd> - Preferred, flexible content<br><kbd><input type=\"submit\"></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; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \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 <kbd>Sign In</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
112
lessons/es/22-html-forms-validation.json
Normal file
112
lessons/es/22-html-forms-validation.json
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-validation",
|
||||||
|
"title": "HTML 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><input type=\"text\" required></kbd><br><br>The browser shows a validation message automatically.",
|
||||||
|
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Submit</button>\n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \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 <kbd>required</kbd> attribute to the name input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
|
||||||
|
"message": "Add the <kbd>required</kbd> attribute to the email input"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "input-constraints",
|
||||||
|
"title": "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 to the password input:<br>1. Add <kbd>minlength=\"8\"</kbd> for minimum length<br>2. Add <kbd>maxlength=\"20\"</kbd> for maximum length<br>3. Add <kbd>placeholder=\"Enter password\"</kbd> as a hint",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } 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; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \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 <kbd>minlength</kbd>=\"8\" to the password input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
|
||||||
|
"message": "Add <kbd>maxlength</kbd>=\"20\" to the password input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
|
||||||
|
"message": "Add a <kbd>placeholder</kbd> to hint what to enter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "complete-registration",
|
||||||
|
"title": "Full 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; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } 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; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; 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 <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">Email *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Password *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
|
||||||
|
"solution": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Password *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n I agree to the Terms of Service *\n </label>\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 <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "type", "value": "email" },
|
||||||
|
"message": "Set the email input <kbd>type=\"email\"</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "required", "value": true },
|
||||||
|
"message": "Make the email field <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "type", "value": "password" },
|
||||||
|
"message": "Set the password input <kbd>type=\"password\"</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "required", "value": true },
|
||||||
|
"message": "Make the password field <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
|
||||||
|
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#terms", "attr": "required", "value": true },
|
||||||
|
"message": "Make the terms checkbox <kbd>required</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/es/23-html-details-summary.json
Normal file
97
lessons/es/23-html-details-summary.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-details-summary",
|
||||||
|
"title": "HTML Details & Summary",
|
||||||
|
"description": "Create expandable content sections without JavaScript",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "details-summary-basic",
|
||||||
|
"title": "First Widget",
|
||||||
|
"description": "The <kbd><details></kbd> element creates a collapsible section. The <kbd><summary></kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!",
|
||||||
|
"task": "Create a <kbd><details></kbd> element with:<br>1. A <kbd><summary></kbd> saying 'Click to reveal'<br>2. A <kbd><p></kbd> with the text 'This content was hidden!'",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<details>\n <summary>Click to reveal</summary>\n <p>This content was hidden!</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "details",
|
||||||
|
"message": "Add a <kbd><details></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "summary",
|
||||||
|
"message": "Add a <kbd><summary></kbd> inside the details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "details", "child": "summary" },
|
||||||
|
"message": "The <kbd><summary></kbd> must be inside <kbd><details></kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "details", "child": "p" },
|
||||||
|
"message": "Add a <kbd><p></kbd> inside <kbd><details></kbd> for the hidden content"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "details-open-attribute",
|
||||||
|
"title": "Pre-expanded Details",
|
||||||
|
"description": "By default, <kbd><details></kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.",
|
||||||
|
"task": "Add the <kbd>open</kbd> attribute to the <kbd><details></kbd> element to show the content by default.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<details>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
|
||||||
|
"solution": "<details open>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "details", "attr": "open", "value": true },
|
||||||
|
"message": "Add the <kbd>open</kbd> attribute to <kbd><details></kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "faq-accordion",
|
||||||
|
"title": "FAQ Accordion",
|
||||||
|
"description": "Multiple <kbd><details></kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3>summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>></kbd> nests inside, <kbd>+</kbd> adds siblings.",
|
||||||
|
"task": "Create an FAQ section with:<br>1. An <kbd><h1></kbd> saying 'Frequently Asked Questions'<br>2. Three <kbd><details></kbd> elements, each with a question in <kbd><summary></kbd> and an answer in <kbd><p></kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<h1>Frequently Asked Questions</h1>\n\n<details>\n <summary>What is HTML5?</summary>\n <p>HTML5 is the latest version of HTML with new semantic elements and APIs.</p>\n</details>\n\n<details>\n <summary>Do I need JavaScript?</summary>\n <p>Many interactive features work with pure HTML5, no JavaScript required!</p>\n</details>\n\n<details>\n <summary>Is this accessible?</summary>\n <p>Yes! Native HTML elements have built-in keyboard and screen reader support.</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "h1",
|
||||||
|
"message": "Add an <kbd><h1></kbd> heading for the FAQ title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "details", "min": 3 },
|
||||||
|
"message": "Create at least 3 <kbd><details></kbd> elements for the FAQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "summary", "min": 3 },
|
||||||
|
"message": "Each <kbd><details></kbd> needs a <kbd><summary></kbd> for the question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "details p", "min": 3 },
|
||||||
|
"message": "Each <kbd><details></kbd> needs a <kbd><p></kbd> for the answer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/es/24-html-progress-meter.json
Normal file
102
lessons/es/24-html-progress-meter.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-progress-meter",
|
||||||
|
"title": "HTML Progress & Meter",
|
||||||
|
"description": "Display completion status and scalar measurements natively",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "progress-basic",
|
||||||
|
"title": "Progress Bars",
|
||||||
|
"description": "The <kbd><progress></kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd><progress>...</progress></kbd> with fallback text inside for older browsers.",
|
||||||
|
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd><label></kbd> saying 'Download:'<br>2. Add a <kbd><progress></kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<label for=\"download\">Download:</label>\n<progress id=\"download\" value=\"70\" max=\"100\">70%</progress>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "progress",
|
||||||
|
"message": "Add a <kbd><progress></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "progress", "attr": "value", "value": "70" },
|
||||||
|
"message": "Set <kbd>value=</kbd>\"70\" on the progress element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "progress", "attr": "max", "value": "100" },
|
||||||
|
"message": "Set <kbd>max=</kbd>\"100\" on the progress element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for the progress bar"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "progress-indeterminate",
|
||||||
|
"title": "Indeterminate Progress",
|
||||||
|
"description": "When progress is unknown (like loading), omit the <kbd>value</kbd> attribute. This creates an animated indeterminate state.<br><br>Useful for network requests or processes with unknown duration.",
|
||||||
|
"task": "Create a loading indicator:<br>1. Add a <kbd><p></kbd> saying 'Loading...'<br>2. Add a <kbd><progress></kbd> without a value attribute",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<p>Loading...</p>\n<progress></progress>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "progress",
|
||||||
|
"message": "Add a <kbd><progress></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Add a <kbd><p></kbd> with loading text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "meter-gauge",
|
||||||
|
"title": "Meter Gauges",
|
||||||
|
"description": "The <kbd><meter></kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!",
|
||||||
|
"task": "Create a battery level meter:<br>1. Add a <kbd><label></kbd> saying 'Battery:'<br>2. Add a <kbd><meter></kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<label for=\"battery\">Battery:</label>\n<meter id=\"battery\" value=\"0.8\" min=\"0\" max=\"1\" low=\"0.2\" high=\"0.8\" optimum=\"1\">80%</meter>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "meter",
|
||||||
|
"message": "Add a <kbd><meter></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "meter", "attr": "value", "value": "0.8" },
|
||||||
|
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "meter", "attr": "low", "value": "0.2" },
|
||||||
|
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for the meter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
127
lessons/es/30-html-tables.json
Normal file
127
lessons/es/30-html-tables.json
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-tables",
|
||||||
|
"title": "HTML Tables",
|
||||||
|
"description": "Create structured data tables with headers and captions",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "table-basic",
|
||||||
|
"title": "Basic Table Structure",
|
||||||
|
"description": "Tables use <kbd><table></kbd> with <kbd><tr></kbd> for rows. Inside rows, use <kbd><th></kbd> for headers and <kbd><td></kbd> for data cells.<br><br>The <kbd><caption></kbd> element provides an accessible title for the table.",
|
||||||
|
"task": "Create a simple table with:<br>1. A <kbd><caption></kbd> saying 'Fruit Prices'<br>2. A header row with 'Fruit' and 'Price' columns<br>3. At least 2 data rows",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Fruit Prices</caption>\n <tr>\n <th>Fruit</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Apple</td>\n <td>$1.50</td>\n </tr>\n <tr>\n <td>Banana</td>\n <td>$0.75</td>\n </tr>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> for the table title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "th", "min": 2 },
|
||||||
|
"message": "Add at least 2 header cells (th)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tr", "min": 3 },
|
||||||
|
"message": "Add at least 3 rows (1 header + 2 data rows)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table-thead-tbody",
|
||||||
|
"title": "Table Head & Body",
|
||||||
|
"description": "Use <kbd><thead></kbd> to group header rows and <kbd><tbody></kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd><tfoot></kbd> for footer rows like totals.",
|
||||||
|
"task": "Create a structured table:<br>1. A <kbd><caption></kbd> with 'Monthly Sales'<br>2. A <kbd><thead></kbd> with Month and Revenue headers<br>3. A <kbd><tbody></kbd> with at least 2 data rows",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Monthly Sales</caption>\n <thead>\n <tr>\n <th>Month</th>\n <th>Revenue</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>January</td>\n <td>$12,500</td>\n </tr>\n <tr>\n <td>February</td>\n <td>$14,200</td>\n </tr>\n </tbody>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "thead",
|
||||||
|
"message": "Add a <kbd><thead></kbd> for the header section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tbody",
|
||||||
|
"message": "Add a <kbd><tbody></kbd> for the data rows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tbody tr", "min": 2 },
|
||||||
|
"message": "Add at least 2 data rows in tbody"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table-complete",
|
||||||
|
"title": "Complete Table with Footer",
|
||||||
|
"description": "Add <kbd><tfoot></kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
|
||||||
|
"task": "Create a complete table:<br>1. A <kbd><caption></kbd> with 'Order Summary'<br>2. A <kbd><thead></kbd> with Item and Price headers<br>3. A <kbd><tbody></kbd> with 2 items<br>4. A <kbd><tfoot></kbd> with a Total row",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Order Summary</caption>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>$25.00</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>$35.00</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Total</td>\n <td>$60.00</td>\n </tr>\n </tfoot>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "thead",
|
||||||
|
"message": "Add a <kbd><thead></kbd> section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tbody",
|
||||||
|
"message": "Add a <kbd><tbody></kbd> section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tfoot",
|
||||||
|
"message": "Add a <kbd><tfoot></kbd> section for the total"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tbody tr", "min": 2 },
|
||||||
|
"message": "Add at least 2 item rows in tbody"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
82
lessons/es/31-html-marquee.json
Normal file
82
lessons/es/31-html-marquee.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-marquee",
|
||||||
|
"title": "HTML Marquee",
|
||||||
|
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "marquee-basic",
|
||||||
|
"title": "Scrolling Text",
|
||||||
|
"description": "The <kbd><marquee></kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!",
|
||||||
|
"task": "Create a simple marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Put some text inside like 'Welcome to my website!'",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee>Welcome to my website!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marquee-direction",
|
||||||
|
"title": "Direction & Behavior",
|
||||||
|
"description": "Control the marquee with attributes:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (default), slide (stops at edge), alternate (bounces)<br>• <kbd>scrollamount</kbd>: speed (default is 6)",
|
||||||
|
"task": "Create a bouncing marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee behavior=\"alternate\">Bounce! Bounce! Bounce!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
|
||||||
|
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marquee-retro",
|
||||||
|
"title": "Retro News Ticker",
|
||||||
|
"description": "Combine multiple marquee attributes for a classic news ticker effect. You can even put multiple elements inside!<br><br>Remember: This is deprecated HTML. Modern sites use CSS animations, but marquee is great for understanding web history.",
|
||||||
|
"task": "Create a news ticker:<br>1. A <kbd><marquee></kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee direction=\"left\" scrollamount=\"5\">BREAKING NEWS: Marquee element still works in browsers!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "direction", "value": "left" },
|
||||||
|
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
|
||||||
|
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/es/32-html-svg.json
Normal file
102
lessons/es/32-html-svg.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-svg",
|
||||||
|
"title": "HTML SVG",
|
||||||
|
"description": "Draw scalable vector graphics directly in HTML",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "svg-circle",
|
||||||
|
"title": "Drawing Circles",
|
||||||
|
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd><svg></kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd><circle></kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.",
|
||||||
|
"task": "Create an SVG with a circle:<br>1. An <kbd><svg></kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd><circle></kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "circle",
|
||||||
|
"message": "Add a <kbd><circle></kbd> element inside the SVG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "circle", "attr": "cx", "value": "100" },
|
||||||
|
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "circle", "attr": "cy", "value": "100" },
|
||||||
|
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "svg-rect-line",
|
||||||
|
"title": "Rectangles & Lines",
|
||||||
|
"description": "Draw rectangles with <kbd><rect></kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd><line></kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!",
|
||||||
|
"task": "Create an SVG with:<br>1. An <kbd><svg></kbd> (200x150)<br>2. A <kbd><rect></kbd> at position (20,20) with size 80x60<br>3. A <kbd><line></kbd> from (120,30) to (180,90) with a stroke color",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "rect",
|
||||||
|
"message": "Add a <kbd><rect></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "line",
|
||||||
|
"message": "Add a <kbd><line></kbd> element"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "svg-shapes",
|
||||||
|
"title": "Multiple Shapes",
|
||||||
|
"description": "Combine shapes to create simple graphics! Add <kbd>stroke</kbd> for outlines and <kbd>stroke-width</kbd> for thickness.<br><br>Use <kbd>fill=\"none\"</kbd> for hollow shapes. Shapes stack in order - later elements appear on top.",
|
||||||
|
"task": "Create a simple face:<br>1. An <kbd><svg></kbd> (200x200)<br>2. A large <kbd><circle></kbd> for the face<br>3. Two small <kbd><circle></kbd> elements for eyes<br>4. A <kbd><line></kbd> for the smile",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "circle", "min": 3 },
|
||||||
|
"message": "Add at least 3 circles (1 face + 2 eyes)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "line",
|
||||||
|
"message": "Add a <kbd><line></kbd> for the smile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
173
lessons/es/flexbox.json
Normal file
173
lessons/es/flexbox.json
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "flexbox",
|
||||||
|
"title": "CSS Flexbox",
|
||||||
|
"description": "Master the flexible box layout model for modern responsive designs",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "flexbox-1",
|
||||||
|
"title": "Container",
|
||||||
|
"description": "Learn how to create a flex container and understand the main and cross axes.",
|
||||||
|
"task": "Add <kbd>display: flex</kbd> to <kbd>.wrap</kbd> to create a flexbox layout.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "display: flex;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "display",
|
||||||
|
"expected": "flex"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>display: flex</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-2",
|
||||||
|
"title": "Direction & Wrap",
|
||||||
|
"description": "Control the direction and wrapping of flex items within a container.",
|
||||||
|
"task": "Add <kbd>flex-direction: column</kbd> and <kbd>flex-wrap: wrap</kbd> to <kbd>.wrap</kbd>.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div><div class='box'>4</div><div class='box'>5</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; height: 16rem; display: flex; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "flex-direction: column;\n flex-wrap: wrap;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex-direction",
|
||||||
|
"expected": "column"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex-direction: column</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex-wrap",
|
||||||
|
"expected": "wrap"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex-wrap: wrap</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-3",
|
||||||
|
"title": "Justify Content",
|
||||||
|
"description": "Learn how to align flex items along the main axis of the flex container.",
|
||||||
|
"task": "Add <kbd>justify-content: space-between</kbd> to <kbd>.wrap</kbd> to distribute the boxes evenly.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "justify-content: space-between;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "justify-content",
|
||||||
|
"expected": "space-between"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>justify-content: space-between</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-4",
|
||||||
|
"title": "Align Items",
|
||||||
|
"description": "Control how flex items are aligned along the cross axis of the flex container.",
|
||||||
|
"task": "Add <kbd>align-items: center</kbd> to <kbd>.wrap</kbd> to vertically center the boxes.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box tall'>1</div><div class='box'>2</div><div class='box short'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; display: flex; justify-content: center; } .tall { height: 6rem; } .short { height: 3rem; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 10rem; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "align-items: center;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "align-items",
|
||||||
|
"expected": "center"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>align-items: center</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-5",
|
||||||
|
"title": "Flex Grow",
|
||||||
|
"description": "The <kbd>flex</kbd> property controls how much an item grows relative to others.",
|
||||||
|
"task": "Add <kbd>flex: 2</kbd> to <kbd>.box2</kbd> to make it grow twice as wide.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box box1'>1</div><div class='box box2'>2</div><div class='box box3'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; display: flex; align-items: center; justify-content: center; } .box1 { background: coral; flex: 1; } .box2 { background: mediumseagreen; } .box3 { background: gold; flex: 1; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
|
||||||
|
"codePrefix": ".box2 {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "flex: 2;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex",
|
||||||
|
"expected": "2"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex: 2</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-6",
|
||||||
|
"title": "Align Self",
|
||||||
|
"description": "Use <kbd>align-self</kbd> to override alignment for a single flex item.",
|
||||||
|
"task": "Add <kbd>align-self: flex-start</kbd> to <kbd>.middle</kbd> to move it to the top.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box middle'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; } .middle { background: mediumseagreen; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 12rem; align-items: center; }",
|
||||||
|
"codePrefix": ".middle {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "align-self: flex-start;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "align-self",
|
||||||
|
"expected": "flex-start"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>align-self: flex-start</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
550
lessons/pl/00-basic-selectors.json
Normal file
550
lessons/pl/00-basic-selectors.json
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "css-basic-selectors",
|
||||||
|
"title": "CSS Selectors",
|
||||||
|
"description": "CSS selectors are the foundation of styling web pages, allowing you to target specific HTML elements for styling. This module introduces fundamental selector types including element type selectors, class selectors, ID selectors, and the universal selector.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "introduction-to-selectors",
|
||||||
|
"title": "What's a Selector?",
|
||||||
|
"description": "A CSS selector is the first part of a CSS rule that tells the browser which HTML elements should receive the styles defined in the declaration block. Selectors are essentially patterns that match against elements in your HTML document. Understanding selectors is fundamental because they determine which elements your CSS rules will affect. The element or elements targeted by a selector are referred to as the 'subject of the selector.' When writing a CSS rule, you first specify the selector, followed by curly braces that contain the style declarations.<br/>For example, to change the text color of elements, you can use the <kbd>color</kbd> property within your declaration block.<br><br><pre>/* Element selector */\np {\n color: orangered;\n /* │ └─── Indicates the value of the expression\n │ \n └─────────── Indicates the property of the expression */\n}</pre>",
|
||||||
|
"task": "Write a CSS rule using a type selector that targets all paragraph elements <kbd>p</kbd> in the document. Make the text blue by setting the <kbd>color</kbd> property to <kbd>blue</kbd>.",
|
||||||
|
"previewHTML": "<h1>Introduction to CSS Selectors</h1>\n<p>This paragraph should turn blue.</p>\n<div>This div element should remain unchanged.</div>\n<p>This second paragraph should also turn blue.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Write a type selector to target all paragraph elements */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p { color: blue }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>p { … }</kbd> to select all paragraph elements",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color:</kbd> property in your CSS rule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "blue",
|
||||||
|
"message": "Set the color value to <kbd>blue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "blue"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>color: blue</kbd> to set the text color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type-selectors",
|
||||||
|
"title": "Type Selectors",
|
||||||
|
"description": "Type selectors (also called tag name selectors or element selectors) target HTML elements based on their tag name. For example, <kbd>p</kbd> selects all paragraph elements, <kbd>h1</kbd> selects all level-one headings, and <kbd>div</kbd> selects all division elements. Type selectors are the most fundamental way to select elements, applying styles consistently to all instances of a particular HTML element throughout your document. You can define a variety of CSS properties with type selectors, such as <kbd>color</kbd> for text color, <kbd>background-color</kbd> for the background, and <kbd>font-weight</kbd> for text emphasis. They provide a broad approach for styling your page and are often the starting point for more specific styling using other selector types.",
|
||||||
|
"task": "Write three separate CSS rules using type selectors to target specific HTML elements: make <kbd>h2</kbd> headings <kbd>purple</kbd>, give <kbd>span</kbd> elements a <kbd>yellow</kbd> background, and make <kbd>strong</kbd> elements <kbd>red</kbd>.",
|
||||||
|
"previewHTML": "<h2>Type Selectors Example</h2>\n<p>Regular paragraph text <span>with a highlighted span</span> that should have a yellow background.</p>\n<p>Another paragraph with <strong>strong important text</strong> that should be red.</p>\n<h2>Another Heading</h2>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
|
||||||
|
"codePrefix": "/* Write three separate type selectors below */\n\n",
|
||||||
|
"initialCode": "/* 1. Make h2 headings purple */\n\n\n/* 2. Give span elements a yellow background */\n\n\n/* 3. Make strong elements red */\n",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "/* 1. Make h2 headings purple */\nh2 {\n color: purple;\n}\n\n/* 2. Give span elements a yellow background */\nspan {\n background-color: yellow;\n}\n\n/* 3. Make strong elements red */\nstrong {\n color: red;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^h2\\s*{",
|
||||||
|
"message": "Include an <kbd>h2 { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Set the <kbd>color</kbd> property to <kbd>purple</kbd> for h2 elements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "h2\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your h2 rule with a closing brace <kbd>}</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\s*{",
|
||||||
|
"message": "Include a <kbd>span { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Set a <kbd>background-color: yellow</kbd> for span elements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your span rule with a closing brace <kbd>}</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^strong\\s*{",
|
||||||
|
"message": "Include a <kbd>strong { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
|
||||||
|
"message": "Set the <kbd>color: red</kbd> for strong elements"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-selectors",
|
||||||
|
"title": "Class Selectors",
|
||||||
|
"description": "Class selectors target elements with a specific class attribute value. They begin with a dot (.) followed by the class name. Classes are powerful because they allow you to apply the same styles to multiple elements regardless of their type. An HTML element can have multiple classes (separated by spaces in the class attribute), and a class can be applied to any number of elements. When using class selectors, you can apply properties like <kbd>background-color</kbd> to set the background color of elements, and <kbd>font-weight</kbd> to control text thickness, making text bold or normal. This flexibility makes class selectors one of the most commonly used methods for applying styles in CSS, allowing for modular and reusable styling across your website.",
|
||||||
|
"task": "Create a CSS rule using a class selector that targets elements with the class <kbd>highlight</kbd>. Give these elements a <kbd>yellow</kbd> background and <kbd>bold</kbd> text.",
|
||||||
|
"previewHTML": "<h2>Using Class Selectors</h2>\n<p>This is a regular paragraph, but <span class=\"highlight\">this span has the highlight class</span> applied to it.</p>\n<p class=\"highlight\">This entire paragraph has the highlight class.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"highlight\">This list item is highlighted</li>\n</ul>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
|
||||||
|
"codePrefix": "/* Create a class selector for elements with the 'highlight' class */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.highlight\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>.highlight { … }</kbd> to create a class selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color:</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>yellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-weight:",
|
||||||
|
"message": "Include the <kbd>font-weight:</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-weight",
|
||||||
|
"expected": "bold"
|
||||||
|
},
|
||||||
|
"message": "Set the font-weight to <kbd>bold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "multiple-classes",
|
||||||
|
"title": "Multiple Classes",
|
||||||
|
"description": "HTML elements can have multiple classes applied simultaneously, allowing for composable and modular CSS designs. When an element has multiple classes, it will receive styles from all matching class selectors. This approach enables you to build a library of reusable CSS classes that can be combined in different ways. You can also target elements that have a specific combination of classes by chaining class selectors together without spaces (e.g., <kbd>.class1.class2</kbd>). When styling these elements, you might use properties like <kbd>border-color</kbd> to change the color of element borders, and <kbd>background-color</kbd> to set the background color of elements. This technique lets you create conditional styles that only apply when certain classes appear together.",
|
||||||
|
"task": "Complete the CSS rule that targets elements with both <kbd>card</kbd> and <kbd>featured</kbd> classes by chaining the selectors. Set the border-color to gold and the background-color to lemonchiffon to make featured cards stand out.",
|
||||||
|
"previewHTML": "<h2>Multiple Class Combinations</h2>\n<div class=\"card\">Regular Card</div>\n<div class=\"card featured\">Featured Card</div>\n<div class=\"featured\">Just Featured (not a card)</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* The .card class already has basic styling */\n/* Now target elements with BOTH classes: 'card' AND 'featured' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.card\\.featured\\s*{",
|
||||||
|
"message": "Chain the selectors as <kbd>.card.featured</kbd> (no space between them)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-color:",
|
||||||
|
"message": "Include the <kbd>border-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-color",
|
||||||
|
"expected": "gold"
|
||||||
|
},
|
||||||
|
"message": "Set the border color to <kbd>gold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*;",
|
||||||
|
"message": "Make sure to end your CSS rule with a semicolon <kbd>;</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lemonchiffon"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>lemonchiffon</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-with-type",
|
||||||
|
"title": "Combining Types",
|
||||||
|
"description": "You can combine type selectors with class selectors to target specific HTML elements that have a certain class. This creates a more specific selector that only matches when both conditions are true: the element is of the specified type AND it has the specified class. For example, <kbd>p.note</kbd> would select paragraph elements with the class <kbd>note</kbd>, but would not select divs or spans with that same class. You can style these combined selections using properties like <kbd>background-color</kbd> to set a colored background for your elements. This approach allows you to apply different styles to the same class when it appears on different element types.",
|
||||||
|
"task": "Create a CSS rule that specifically targets <kbd><span></kbd> elements with the class <kbd>highlight</kbd>. Make those elements have an orange background, while other elements with the highlight class remain untouched.",
|
||||||
|
"previewHTML": "<h2>Type and Class Combinations</h2>\n<p>This paragraph has a <span class=\"highlight\">highlighted span</span> that should have an orange background.</p>\n<p class=\"highlight\">This paragraph has the highlight class but should NOT have an orange background.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
|
||||||
|
"sandboxCSS": "h2, p, span { padding: 5px; }",
|
||||||
|
"codePrefix": "/* The .highlight class already sets font-weight to bold */\n/* Now target ONLY span elements with the highlight class */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\.highlight\\s*{",
|
||||||
|
"message": "Use <kbd>span.highlight</kbd> selector (no space between element and class)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "orange"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>orange</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-selectors",
|
||||||
|
"title": "ID Selectors",
|
||||||
|
"description": "ID selectors target elements with a specific id attribute. They begin with a hash/pound sign (#) followed by the ID name. Unlike classes, IDs must be unique within a document—each ID value should be used only once per page. ID selectors have higher specificity than class or element selectors, meaning they override those selectors when conflicts arise. When styling with ID selectors, you can use properties like <kbd>color</kbd> to define text color, and <kbd>text-decoration</kbd> to control the appearance of text, such as adding underlines to elements. Because of their uniqueness requirement, IDs are best used for one-of-a-kind elements like page headers, main navigation, or specific unique components that appear only once on a page.",
|
||||||
|
"task": "Create a CSS rule with an ID selector that targets the element with the ID <kbd>main-title</kbd>. Set its color to purple and add an underline with <kbd>text-decoration: underline</kbd>.",
|
||||||
|
"previewHTML": "<h1 id=\"main-title\">Main Page Title</h1>\n<p>Regular paragraph content.</p>\n<h2>Secondary Heading</h2>\n<p id=\"intro\">Introduction paragraph (different ID).</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Create an ID selector to target the element with id=\"main-title\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^#main-title\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>#main-title</kbd> to create an ID selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Set the color to <kbd>purple</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-decoration:",
|
||||||
|
"message": "Include the <kbd>text-decoration</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "text-decoration",
|
||||||
|
"expected": "underline"
|
||||||
|
},
|
||||||
|
"message": "Set the text-decoration to <kbd>underline</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "#main-title\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-with-type",
|
||||||
|
"title": "Type + ID",
|
||||||
|
"description": "Similar to how you can combine type and class selectors, you can also combine type selectors with ID selectors. For example, <kbd>h1#title</kbd> targets an h1 element with the ID 'title'. When using this combined approach, you can apply CSS properties like <kbd>font-style</kbd> to control the slant of the text, making it italic or normal. While this selector combination is more specific than using just the ID selector, it's often unnecessary since IDs should already be unique in a document. However, this technique can be useful for improving code readability or when you want to emphasize that a particular ID should only appear on a specific element type.",
|
||||||
|
"task": "Create a CSS rule that combines a type selector with an ID selector to target specifically a paragraph element with the ID <kbd>special</kbd>. Set its font style to italic.",
|
||||||
|
"previewHTML": "<h2 id=\"special\">Heading with ID \"special\" (should NOT be affected)</h2>\n<p id=\"special\">Paragraph with ID \"special\" (should become italic)</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Create a combined type+ID selector for a paragraph with id=\"special\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p#special\\s*{",
|
||||||
|
"message": "Use <kbd>p#special</kbd> to target paragraphs with ID <kbd>special</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-style:",
|
||||||
|
"message": "Include the <kbd>font-style</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-style",
|
||||||
|
"expected": "italic"
|
||||||
|
},
|
||||||
|
"message": "Set the font-style to <kbd>italic</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p#special\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "selector-lists",
|
||||||
|
"title": "Selector Lists",
|
||||||
|
"description": "When multiple elements need the same styling, you can group them together using a selector list (also known as grouping selectors). Selector lists are created by separating individual selectors with commas. This approach reduces repetition in your CSS, making it more maintainable and efficient. For example, <kbd>h1, h2, h3 { color: blue; }</kbd> applies the same blue color to all three heading levels. When styling multiple selectors at once, you can apply properties like <kbd>background-color</kbd> to set the background, <kbd>border-left</kbd> to create a left border with a specific thickness, style, and color, and <kbd>padding-left</kbd> to create space between the content and the left border. Whitespace around commas is optional, and each selector in the list can be any valid selector type-elements, classes, IDs, or even more complex selectors.",
|
||||||
|
"task": "Create a selector list that applies the same styles to three different elements: paragraphs with class <kbd>note</kbd>, list items with class <kbd>important</kbd>, and the element with ID <kbd>summary</kbd>. Give them a <kbd>lightyellow</kbd> background, a <kbd>gold</kbd> left border, and some left <kbd>padding</kbd>.",
|
||||||
|
"previewHTML": "<p class=\"note\">This is a note paragraph.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"important\">Important list item</li>\n</ul>\n<div id=\"summary\">Summary section</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
|
||||||
|
"codePrefix": "/* Create a selector list to apply the same styles to multiple different elements */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "p.note",
|
||||||
|
"message": "Include <kbd>p.note</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "li.important",
|
||||||
|
"message": "Include <kbd>li.important</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "#summary",
|
||||||
|
"message": "Include <kbd>#summary</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
|
||||||
|
"message": "Create a comma-separated list with all three selectors",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lightyellow"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>lightyellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-left:",
|
||||||
|
"message": "Include the <kbd>border-left</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-left",
|
||||||
|
"expected": "3px solid gold"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>border-left: 3px solid gold</kbd> to create a left border"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "padding-left:",
|
||||||
|
"message": "Include the <kbd>padding-left</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "padding-left",
|
||||||
|
"expected": "10px"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>padding-left: 10px</kbd> to add left padding"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "universal-selector",
|
||||||
|
"title": "Universal (*)",
|
||||||
|
"description": "The universal selector is denoted by an asterisk (*) and matches any element of any type. It selects everything in the document or, when combined with other selectors, everything within a specific context. For example, <kbd>* { margin: 0; }</kbd> removes margins from all elements, while <kbd>article *</kbd> selects all elements inside article elements. When using the universal selector in combination with other selectors, you can apply properties like <kbd>margin</kbd> to control the spacing around elements. The universal selector is powerful but should be used carefully due to its broad impact. It's commonly used in CSS resets, to override default browser styling, or to target all children of a particular element.",
|
||||||
|
"task": "Use the universal selector to remove margins from all elements inside the container div. Create a rule using <kbd>div.container *</kbd> as the selector and set <kbd>margin: 0</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"container\">\n <h2>Inside Container</h2>\n <p>This paragraph is inside the container.</p>\n <ul>\n <li>List item inside container</li>\n </ul>\n</div>\n<p>This paragraph is outside the container and should not be affected.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use the universal selector to target all elements inside the container */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^div\\.container\\s+\\*\\s*{",
|
||||||
|
"message": "Use <kbd>div.container *</kbd> selector (with a space between container and *)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "margin:",
|
||||||
|
"message": "Include the <kbd>margin</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "margin",
|
||||||
|
"expected": "0"
|
||||||
|
},
|
||||||
|
"message": "Set margin to <kbd>0</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "specificity-basics",
|
||||||
|
"title": "Specificity",
|
||||||
|
"description": "CSS specificity determines which styles take precedence when multiple conflicting rules target the same element. Specificity follows a hierarchical system: inline styles have the highest specificity, followed by ID selectors, then class/attribute/pseudo-class selectors, and finally element/pseudo-element selectors. This can be conceptualized as a four-part score (inline, ID, class, element). When creating multiple rules that may target the same elements, you can use the <kbd>color</kbd> property to set text colors, and specificity will determine which color is actually applied. Understanding specificity is crucial for predictable styling and debugging CSS conflicts. When two selectors have equal specificity, the one that comes last in the stylesheet wins.",
|
||||||
|
"task": "Examine the existing CSS rules and add a new rule with higher specificity to override the text color of the paragraph. Create a rule using '.content p' as the selector and set color: green.",
|
||||||
|
"previewHTML": "<div class=\"content\">\n <p>What color will this paragraph be? Look at the CSS rules and their specificity.</p>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
|
||||||
|
"codePrefix": "/* These CSS rules target the same paragraph but have different specificity */\n\n/* Rule 1: Element selector (lowest specificity) */\np {\n color: red;\n}\n\n/* Rule 2: Descendant selector (higher specificity than just 'p') */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.content\\s+p\\s*{",
|
||||||
|
"message": "Use <kbd>.content p</kbd> as your selector (note the space between)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "green",
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
62
lessons/pl/00-welcome.json
Normal file
62
lessons/pl/00-welcome.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "welcome",
|
||||||
|
"title": "Code Crispies",
|
||||||
|
"description": "Welcome to Code Crispies - your interactive web development learning platform",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "get-started",
|
||||||
|
"title": "Get Started",
|
||||||
|
"description": "<strong>Code Crispies</strong> is a free, open-source platform for learning web development through hands-on exercises. No account required!<br><br><strong>What you'll learn:</strong><br>• <strong>HTML</strong> - Semantic elements, forms, tables, SVG (<em>HTML Block & Inline</em>, <em>HTML Forms</em>, <em>HTML Tables</em>)<br>• <strong>CSS</strong> - Selectors, box model, flexbox, animations (<em>CSS Selectors</em>, <em>CSS Box Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br><strong>How it works:</strong><br>1. Read the task in the left panel<br>2. Write code in the editor<br>3. See live results in the preview<br>4. Get instant feedback with hints<br><br><strong>Keyboard shortcuts:</strong> <kbd>Ctrl+Z</kbd> to undo, <kbd>Ctrl+Shift+Z</kbd> to redo<br><br><strong>More resources:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript solutions<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript technology roadmap",
|
||||||
|
"task": "Hello World",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; } h1 { color: #6366f1; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<h1>Hello World</h1>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "Hello",
|
||||||
|
"message": "Write 'Hello World'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "overview",
|
||||||
|
"title": "Overview",
|
||||||
|
"description": "<strong>You're ready!</strong> Open the menu (☰) to explore all modules.<br><br><strong>Recommended learning path:</strong><br>1. <em>HTML Block & Inline</em> - Understand container vs inline elements<br>2. <em>HTML Forms</em> - Build interactive forms with validation<br>3. <em>CSS Selectors</em> - Target elements precisely<br>4. <em>CSS Box Model</em> - Master padding, margin, borders<br>5. <em>CSS Flexbox</em> - Create flexible layouts<br>6. <em>CSS Animations</em> - Add motion and transitions<br><br><strong>Tips:</strong><br>• Use <em>Show Expected</em> to see the target result<br>• Your progress is saved automatically<br>• Try Emmet in HTML mode: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Made by <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>",
|
||||||
|
"task": "Click Next to continue",
|
||||||
|
"previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"solution": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "Hello",
|
||||||
|
"message": "Click Next to continue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "playground",
|
||||||
|
"title": "Playground",
|
||||||
|
"mode": "playground",
|
||||||
|
"description": "",
|
||||||
|
"task": "",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Start coding!</p>",
|
||||||
|
"solution": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
180
lessons/pl/01-box-model.json
Normal file
180
lessons/pl/01-box-model.json
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "box-model",
|
||||||
|
"title": "CSS Box Model",
|
||||||
|
"description": "Master the fundamental principles of space management in web design through the CSS box model. This module explores how content, padding, borders, and margins combine to create layout structures that are both visually appealing and structurally sound.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "box-model-1",
|
||||||
|
"title": "Box Model Components",
|
||||||
|
"description": "The CSS box model consists of four concentric layers: content area (innermost), padding, border, and margin (outermost). Understanding how these components interact is essential for precise layout control.",
|
||||||
|
"task": "Add <kbd>padding: 1rem</kbd> to <kbd>.box</kbd> to create space between its content and border.",
|
||||||
|
"previewHTML": "<div class=\"box\">Box Model Components</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".box {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "padding: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "padding", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>padding: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-2",
|
||||||
|
"title": "Adding Borders",
|
||||||
|
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.",
|
||||||
|
"task": "Add <kbd>border: 2px solid darkslategray</kbd> to <kbd>.box</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">This box needs a border</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".box {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "border: 2px solid darkslategray;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border:\\s*2px\\s+solid\\s+darkslategray",
|
||||||
|
"message": "Set <kbd>border: 2px solid darkslategray</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-3",
|
||||||
|
"title": "Adding Margins",
|
||||||
|
"description": "Margins create space between elements, controlling how they relate to one another within a layout. Unlike padding (which affects internal spacing), margins exist outside the element's border.",
|
||||||
|
"task": "Add <kbd>margin: 1rem</kbd> to <kbd>.outer</kbd> to create space between it and the adjacent element.",
|
||||||
|
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".outer {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "margin", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>margin: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-4",
|
||||||
|
"title": "Box Sizing: Border-Box",
|
||||||
|
"description": "The <kbd>box-sizing</kbd> property determines how element dimensions are calculated. The default <kbd>content-box</kbd> excludes padding and border from width/height, while <kbd>border-box</kbd> includes them, making layout calculations more intuitive.",
|
||||||
|
"task": "Add <kbd>box-sizing: border-box</kbd> to <kbd>.sized</kbd> so padding and border are included in its width.",
|
||||||
|
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".sized {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "box-sizing: border-box;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||||
|
"message": "Set <kbd>box-sizing: border-box</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-5",
|
||||||
|
"title": "Margin Collapse",
|
||||||
|
"description": "When two vertical margins meet, they collapse to the larger value instead of adding up. Understanding this behavior is crucial for consistent vertical spacing.",
|
||||||
|
"task": "Add <kbd>margin-bottom: 2rem</kbd> to <kbd>.first</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.",
|
||||||
|
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">This paragraph has a bottom margin.</p><p class=\"second\">This paragraph has a top margin of 1rem.</p></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".first {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin-bottom: 2rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "margin-bottom", "expected": "2rem" },
|
||||||
|
"message": "Set <kbd>margin-bottom: 2rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-6",
|
||||||
|
"title": "Margin Shorthand Notation",
|
||||||
|
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.",
|
||||||
|
"task": "Add <kbd>margin: 1rem 2rem</kbd> to <kbd>.spaced</kbd> for 1rem top/bottom and 2rem left/right.",
|
||||||
|
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".spaced {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin: 1rem 2rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "margin:\\s*1rem\\s+2rem",
|
||||||
|
"message": "Set <kbd>margin: 1rem 2rem</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-7",
|
||||||
|
"title": "Padding Shorthand Notation",
|
||||||
|
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.",
|
||||||
|
"task": "Add <kbd>padding: 1.5rem</kbd> to <kbd>.padded</kbd> to add equal padding on all sides.",
|
||||||
|
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".padded {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "padding: 1.5rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "padding", "expected": "1.5rem" },
|
||||||
|
"message": "Set <kbd>padding: 1.5rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-8",
|
||||||
|
"title": "Border on Specific Sides",
|
||||||
|
"description": "For granular control, you can target specific sides with <kbd>border-top</kbd>, <kbd>border-right</kbd>, <kbd>border-bottom</kbd>, or <kbd>border-left</kbd>.",
|
||||||
|
"task": "Add <kbd>border-bottom: 4px solid dodgerblue</kbd> to <kbd>.line</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".line {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "border-bottom: 4px solid dodgerblue;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue",
|
||||||
|
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
116
lessons/pl/05-units-variables.json
Normal file
116
lessons/pl/05-units-variables.json
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "units-variables",
|
||||||
|
"title": "CSS Units & Variables",
|
||||||
|
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "units-1",
|
||||||
|
"title": "Absolute vs. Relative Units",
|
||||||
|
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.",
|
||||||
|
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">Resize me!</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set flexible sizing */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: 80%;\n max-width: 37.5rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "width", "message": "Use <kbd>width</kbd> property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to <kbd>80%</kbd>" },
|
||||||
|
{ "type": "contains", "value": "max-width", "message": "Use <kbd>max-width</kbd> property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "max-width", "expected": "37.5rem" },
|
||||||
|
"message": "Set max-width to <kbd>37.5rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-2",
|
||||||
|
"title": "CSS Custom Properties",
|
||||||
|
"description": "Define and reuse variables (--custom properties) to centralize your theme values.",
|
||||||
|
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>#6200ee</kbd> and apply it as the border color on <kbd>.themed</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"themed\">Variable Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define and use a CSS variable */\n:root {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.themed { }",
|
||||||
|
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "--main-color",
|
||||||
|
"message": "Define <kbd>--main-color</kbd> in :root",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "var(--main-color)",
|
||||||
|
"message": "Use <kbd>var(--main-color)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "border", "expected": "var(--main-color)" },
|
||||||
|
"message": "Apply variable to border color",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-3",
|
||||||
|
"title": "Unit Calculations (calc)",
|
||||||
|
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.",
|
||||||
|
"task": "Set the <kbd>width</kbd> of <kbd>.sized</kbd> to <kbd>calc(100% - 2rem)</kbd> and <kbd>min-height</kbd> to <kbd>calc(10vh + 1rem)</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"sized\">Calc Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "width:\\s*calc\\(100% - 2rem\\)",
|
||||||
|
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
|
||||||
|
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-4",
|
||||||
|
"title": "Viewport & Responsive Units",
|
||||||
|
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.",
|
||||||
|
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"view\">Viewport Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use viewport units */\n.view {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: 50vw;\n height: 20vh;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "vw", "message": "Use <kbd>vw</kbd> unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "vh", "message": "Use <kbd>vh</kbd> unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" },
|
||||||
|
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
145
lessons/pl/06-transitions-animations.json
Normal file
145
lessons/pl/06-transitions-animations.json
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "transitions-animations",
|
||||||
|
"title": "CSS Animations",
|
||||||
|
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "transitions-1",
|
||||||
|
"title": "Transitions",
|
||||||
|
"description": "Learn how to apply <kbd>transition</kbd> to properties for smooth changes on state changes.",
|
||||||
|
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Hover Me</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add transition */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " transition: background-color 0.3s;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition",
|
||||||
|
"message": "Use the <kbd>transition</kbd> property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||||
|
"message": "Set <kbd>transition: background-color 0.3s</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-2",
|
||||||
|
"title": "Timing Funcs",
|
||||||
|
"description": "Explore easing functions like <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> to control animation pacing.",
|
||||||
|
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Timing</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set timing function */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " transition-timing-function: ease-in-out;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition-timing-function",
|
||||||
|
"message": "Use <kbd>transition-timing-function</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||||
|
"message": "Set timing to <kbd>ease-in-out</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-3",
|
||||||
|
"title": "Keyframes",
|
||||||
|
"description": "Create named animations using <kbd>@keyframes</kbd> and apply them via the <kbd>animation</kbd> shorthand.",
|
||||||
|
"task": "Define a keyframe at <kbd>50%</kbd> with <kbd>transform: translateY(-20px)</kbd> and apply <kbd>animation: bounce 1s infinite</kbd> to <kbd>.ball</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"ball\"></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define keyframes and apply animation */\n@keyframes bounce {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.ball { }",
|
||||||
|
"solution": " 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "@keyframes bounce",
|
||||||
|
"message": "Define <kbd>@keyframes bounce</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "50%.*transform: translateY\\(-20px\\)",
|
||||||
|
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "animation",
|
||||||
|
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "animation:.*bounce.*1s.*infinite",
|
||||||
|
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-4",
|
||||||
|
"title": "Anim Properties",
|
||||||
|
"description": "Fine-tune animations with <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd>, and <kbd>animation-fill-mode</kbd>.",
|
||||||
|
"task": "Apply the <kbd>pulse</kbd> animation to <kbd>.box</kbd> with <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd>, and <kbd>animation-fill-mode: forwards</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">Pulse</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply animation properties */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " animation-name: pulse;\n animation-duration: 2s;\n animation-delay: 1s;\n animation-iteration-count: 2;\n animation-fill-mode: forwards;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-name", "expected": "pulse" },
|
||||||
|
"message": "Set <kbd>animation-name: pulse</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-duration", "expected": "2s" },
|
||||||
|
"message": "Set <kbd>animation-duration: 2s</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-delay", "expected": "1s" },
|
||||||
|
"message": "Set <kbd>animation-delay: 1s</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||||
|
"message": "Set <kbd>animation-iteration-count: 2</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||||
|
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
126
lessons/pl/08-responsive.json
Normal file
126
lessons/pl/08-responsive.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "responsive-design",
|
||||||
|
"title": "CSS Responsive Design",
|
||||||
|
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "responsive-1",
|
||||||
|
"title": "Media Queries",
|
||||||
|
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.",
|
||||||
|
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"panel\">Resize the window</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add your media query below */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"solution": "@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||||
|
"message": "Use <kbd>@media (max-width: 600px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".panel",
|
||||||
|
"message": "Target <kbd>.panel</kbd> inside the media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "background", "expected": "lightcoral" },
|
||||||
|
"message": "Set <kbd>background: lightcoral</kbd>",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-2",
|
||||||
|
"title": "Fluid Type",
|
||||||
|
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.",
|
||||||
|
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.",
|
||||||
|
"previewHTML": "<p class=\"text\">Fluid Typography</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply fluid font sizing */\n.text {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " font-size: 5vw;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-3",
|
||||||
|
"title": "Flex Grids",
|
||||||
|
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.",
|
||||||
|
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.cards</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Create a responsive grid */\n.cards {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "display", "expected": "grid" },
|
||||||
|
"message": "Set <kbd>display: grid</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||||
|
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "gap", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-4",
|
||||||
|
"title": "Mobile-First",
|
||||||
|
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.",
|
||||||
|
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.",
|
||||||
|
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add mobile-first enhancement */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"solution": "@media (min-width: 768px) {\n .sidebar {\n width: 250px;\n }\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||||
|
"message": "Use <kbd>@media (min-width: 768px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".sidebar",
|
||||||
|
"message": "Target <kbd>.sidebar</kbd> inside media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "width", "expected": "250px" },
|
||||||
|
"message": "Set <kbd>width: 250px</kbd>",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/pl/20-html-elements.json
Normal file
97
lessons/pl/20-html-elements.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-elements",
|
||||||
|
"title": "HTML Block & 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><div></kbd>, <kbd><p></kbd>, <kbd><h1></kbd>, <kbd><section></kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd><span></kbd>, <kbd><a></kbd>, <kbd><strong></kbd>, <kbd><em></kbd>",
|
||||||
|
"task": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (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>",
|
||||||
|
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Add a <kbd><p></kbd> paragraph element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "p", "child": "strong" },
|
||||||
|
"message": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "semantic-containers",
|
||||||
|
"title": "Semantic Tags",
|
||||||
|
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd><header></kbd> - Page or section header<br><kbd><nav></kbd> - Navigation links<br><kbd><main></kbd> - Main content area<br><kbd><section></kbd> - Thematic grouping<br><kbd><article></kbd> - Self-contained content<br><kbd><footer></kbd> - Page or section footer",
|
||||||
|
"task": "Create a basic page structure:<br>1. Add a <kbd><header></kbd> with an <kbd><h1></kbd> containing the text 'My Website'<br>2. Add a <kbd><main></kbd> element with a paragraph saying 'Welcome to my site!'<br>3. Add a <kbd><footer></kbd> with a paragraph saying 'Copyright 2025'",
|
||||||
|
"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": "",
|
||||||
|
"solution": "<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 <kbd><header></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "main",
|
||||||
|
"message": "Add a <kbd><main></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "footer",
|
||||||
|
"message": "Add a <kbd><footer></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "header", "child": "h1" },
|
||||||
|
"message": "Add an <kbd><h1></kbd> heading inside your header"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "div-vs-span",
|
||||||
|
"title": "div & span",
|
||||||
|
"description": "When you need a container without semantic meaning:<br><br><kbd><div></kbd> - Generic block container (for layout/grouping)<br><kbd><span></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><span></kbd> to style it differently. Wrap the whole quote in a <kbd><div></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.",
|
||||||
|
"solution": "<div>The most <span>highlighted</span> moment was unforgettable.</div>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "div",
|
||||||
|
"message": "Wrap everything in a <kbd><div></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "span",
|
||||||
|
"message": "Add a <kbd><span></kbd> around the word <kbd>highlighted</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_text",
|
||||||
|
"value": { "selector": "span", "text": "highlighted" },
|
||||||
|
"message": "The <kbd><span></kbd> should contain the word <kbd>highlighted</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/pl/21-html-forms-basic.json
Normal file
102
lessons/pl/21-html-forms-basic.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-basic",
|
||||||
|
"title": "HTML Forms",
|
||||||
|
"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><form></kbd> wrapper. Inside, use <kbd><label></kbd> to describe inputs and <kbd><input></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:<br>1. A <kbd><label></kbd> with the text 'Name:' and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd><input></kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> 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": "",
|
||||||
|
"solution": "<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 <kbd><form></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for your input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "input",
|
||||||
|
"message": "Add an <kbd><input></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "label", "attr": "for", "value": null },
|
||||||
|
"message": "Add a <kbd>for</kbd> attribute to your label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input", "attr": "id", "value": null },
|
||||||
|
"message": "Add an <kbd>id</kbd> 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 two fields:<br>1. An email field: <kbd><label for=\"email\">Email:</label></kbd> and <kbd><input type=\"email\" id=\"email\"></kbd><br>2. A password field: <kbd><label for=\"password\">Password:</label></kbd> and <kbd><input type=\"password\" id=\"password\"></kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n \n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\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><button type=\"submit\"></kbd> - Preferred, flexible content<br><kbd><input type=\"submit\"></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; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \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 <kbd>Sign In</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
112
lessons/pl/22-html-forms-validation.json
Normal file
112
lessons/pl/22-html-forms-validation.json
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-validation",
|
||||||
|
"title": "HTML 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><input type=\"text\" required></kbd><br><br>The browser shows a validation message automatically.",
|
||||||
|
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Submit</button>\n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \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 <kbd>required</kbd> attribute to the name input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
|
||||||
|
"message": "Add the <kbd>required</kbd> attribute to the email input"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "input-constraints",
|
||||||
|
"title": "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 to the password input:<br>1. Add <kbd>minlength=\"8\"</kbd> for minimum length<br>2. Add <kbd>maxlength=\"20\"</kbd> for maximum length<br>3. Add <kbd>placeholder=\"Enter password\"</kbd> as a hint",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } 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; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \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 <kbd>minlength</kbd>=\"8\" to the password input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
|
||||||
|
"message": "Add <kbd>maxlength</kbd>=\"20\" to the password input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
|
||||||
|
"message": "Add a <kbd>placeholder</kbd> to hint what to enter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "complete-registration",
|
||||||
|
"title": "Full 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; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } 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; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; 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 <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">Email *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Password *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
|
||||||
|
"solution": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Password *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n I agree to the Terms of Service *\n </label>\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 <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "type", "value": "email" },
|
||||||
|
"message": "Set the email input <kbd>type=\"email\"</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "required", "value": true },
|
||||||
|
"message": "Make the email field <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "type", "value": "password" },
|
||||||
|
"message": "Set the password input <kbd>type=\"password\"</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "required", "value": true },
|
||||||
|
"message": "Make the password field <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
|
||||||
|
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#terms", "attr": "required", "value": true },
|
||||||
|
"message": "Make the terms checkbox <kbd>required</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/pl/23-html-details-summary.json
Normal file
97
lessons/pl/23-html-details-summary.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-details-summary",
|
||||||
|
"title": "HTML Details & Summary",
|
||||||
|
"description": "Create expandable content sections without JavaScript",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "details-summary-basic",
|
||||||
|
"title": "First Widget",
|
||||||
|
"description": "The <kbd><details></kbd> element creates a collapsible section. The <kbd><summary></kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!",
|
||||||
|
"task": "Create a <kbd><details></kbd> element with:<br>1. A <kbd><summary></kbd> saying 'Click to reveal'<br>2. A <kbd><p></kbd> with the text 'This content was hidden!'",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<details>\n <summary>Click to reveal</summary>\n <p>This content was hidden!</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "details",
|
||||||
|
"message": "Add a <kbd><details></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "summary",
|
||||||
|
"message": "Add a <kbd><summary></kbd> inside the details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "details", "child": "summary" },
|
||||||
|
"message": "The <kbd><summary></kbd> must be inside <kbd><details></kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "details", "child": "p" },
|
||||||
|
"message": "Add a <kbd><p></kbd> inside <kbd><details></kbd> for the hidden content"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "details-open-attribute",
|
||||||
|
"title": "Pre-expanded Details",
|
||||||
|
"description": "By default, <kbd><details></kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.",
|
||||||
|
"task": "Add the <kbd>open</kbd> attribute to the <kbd><details></kbd> element to show the content by default.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<details>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
|
||||||
|
"solution": "<details open>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "details", "attr": "open", "value": true },
|
||||||
|
"message": "Add the <kbd>open</kbd> attribute to <kbd><details></kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "faq-accordion",
|
||||||
|
"title": "FAQ Accordion",
|
||||||
|
"description": "Multiple <kbd><details></kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3>summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>></kbd> nests inside, <kbd>+</kbd> adds siblings.",
|
||||||
|
"task": "Create an FAQ section with:<br>1. An <kbd><h1></kbd> saying 'Frequently Asked Questions'<br>2. Three <kbd><details></kbd> elements, each with a question in <kbd><summary></kbd> and an answer in <kbd><p></kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<h1>Frequently Asked Questions</h1>\n\n<details>\n <summary>What is HTML5?</summary>\n <p>HTML5 is the latest version of HTML with new semantic elements and APIs.</p>\n</details>\n\n<details>\n <summary>Do I need JavaScript?</summary>\n <p>Many interactive features work with pure HTML5, no JavaScript required!</p>\n</details>\n\n<details>\n <summary>Is this accessible?</summary>\n <p>Yes! Native HTML elements have built-in keyboard and screen reader support.</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "h1",
|
||||||
|
"message": "Add an <kbd><h1></kbd> heading for the FAQ title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "details", "min": 3 },
|
||||||
|
"message": "Create at least 3 <kbd><details></kbd> elements for the FAQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "summary", "min": 3 },
|
||||||
|
"message": "Each <kbd><details></kbd> needs a <kbd><summary></kbd> for the question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "details p", "min": 3 },
|
||||||
|
"message": "Each <kbd><details></kbd> needs a <kbd><p></kbd> for the answer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/pl/24-html-progress-meter.json
Normal file
102
lessons/pl/24-html-progress-meter.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-progress-meter",
|
||||||
|
"title": "HTML Progress & Meter",
|
||||||
|
"description": "Display completion status and scalar measurements natively",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "progress-basic",
|
||||||
|
"title": "Progress Bars",
|
||||||
|
"description": "The <kbd><progress></kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd><progress>...</progress></kbd> with fallback text inside for older browsers.",
|
||||||
|
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd><label></kbd> saying 'Download:'<br>2. Add a <kbd><progress></kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<label for=\"download\">Download:</label>\n<progress id=\"download\" value=\"70\" max=\"100\">70%</progress>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "progress",
|
||||||
|
"message": "Add a <kbd><progress></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "progress", "attr": "value", "value": "70" },
|
||||||
|
"message": "Set <kbd>value=</kbd>\"70\" on the progress element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "progress", "attr": "max", "value": "100" },
|
||||||
|
"message": "Set <kbd>max=</kbd>\"100\" on the progress element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for the progress bar"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "progress-indeterminate",
|
||||||
|
"title": "Indeterminate Progress",
|
||||||
|
"description": "When progress is unknown (like loading), omit the <kbd>value</kbd> attribute. This creates an animated indeterminate state.<br><br>Useful for network requests or processes with unknown duration.",
|
||||||
|
"task": "Create a loading indicator:<br>1. Add a <kbd><p></kbd> saying 'Loading...'<br>2. Add a <kbd><progress></kbd> without a value attribute",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<p>Loading...</p>\n<progress></progress>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "progress",
|
||||||
|
"message": "Add a <kbd><progress></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Add a <kbd><p></kbd> with loading text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "meter-gauge",
|
||||||
|
"title": "Meter Gauges",
|
||||||
|
"description": "The <kbd><meter></kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!",
|
||||||
|
"task": "Create a battery level meter:<br>1. Add a <kbd><label></kbd> saying 'Battery:'<br>2. Add a <kbd><meter></kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<label for=\"battery\">Battery:</label>\n<meter id=\"battery\" value=\"0.8\" min=\"0\" max=\"1\" low=\"0.2\" high=\"0.8\" optimum=\"1\">80%</meter>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "meter",
|
||||||
|
"message": "Add a <kbd><meter></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "meter", "attr": "value", "value": "0.8" },
|
||||||
|
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "meter", "attr": "low", "value": "0.2" },
|
||||||
|
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for the meter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
127
lessons/pl/30-html-tables.json
Normal file
127
lessons/pl/30-html-tables.json
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-tables",
|
||||||
|
"title": "HTML Tables",
|
||||||
|
"description": "Create structured data tables with headers and captions",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "table-basic",
|
||||||
|
"title": "Basic Table Structure",
|
||||||
|
"description": "Tables use <kbd><table></kbd> with <kbd><tr></kbd> for rows. Inside rows, use <kbd><th></kbd> for headers and <kbd><td></kbd> for data cells.<br><br>The <kbd><caption></kbd> element provides an accessible title for the table.",
|
||||||
|
"task": "Create a simple table with:<br>1. A <kbd><caption></kbd> saying 'Fruit Prices'<br>2. A header row with 'Fruit' and 'Price' columns<br>3. At least 2 data rows",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Fruit Prices</caption>\n <tr>\n <th>Fruit</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Apple</td>\n <td>$1.50</td>\n </tr>\n <tr>\n <td>Banana</td>\n <td>$0.75</td>\n </tr>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> for the table title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "th", "min": 2 },
|
||||||
|
"message": "Add at least 2 header cells (th)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tr", "min": 3 },
|
||||||
|
"message": "Add at least 3 rows (1 header + 2 data rows)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table-thead-tbody",
|
||||||
|
"title": "Table Head & Body",
|
||||||
|
"description": "Use <kbd><thead></kbd> to group header rows and <kbd><tbody></kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd><tfoot></kbd> for footer rows like totals.",
|
||||||
|
"task": "Create a structured table:<br>1. A <kbd><caption></kbd> with 'Monthly Sales'<br>2. A <kbd><thead></kbd> with Month and Revenue headers<br>3. A <kbd><tbody></kbd> with at least 2 data rows",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Monthly Sales</caption>\n <thead>\n <tr>\n <th>Month</th>\n <th>Revenue</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>January</td>\n <td>$12,500</td>\n </tr>\n <tr>\n <td>February</td>\n <td>$14,200</td>\n </tr>\n </tbody>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "thead",
|
||||||
|
"message": "Add a <kbd><thead></kbd> for the header section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tbody",
|
||||||
|
"message": "Add a <kbd><tbody></kbd> for the data rows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tbody tr", "min": 2 },
|
||||||
|
"message": "Add at least 2 data rows in tbody"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table-complete",
|
||||||
|
"title": "Complete Table with Footer",
|
||||||
|
"description": "Add <kbd><tfoot></kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
|
||||||
|
"task": "Create a complete table:<br>1. A <kbd><caption></kbd> with 'Order Summary'<br>2. A <kbd><thead></kbd> with Item and Price headers<br>3. A <kbd><tbody></kbd> with 2 items<br>4. A <kbd><tfoot></kbd> with a Total row",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Order Summary</caption>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>$25.00</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>$35.00</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Total</td>\n <td>$60.00</td>\n </tr>\n </tfoot>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "thead",
|
||||||
|
"message": "Add a <kbd><thead></kbd> section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tbody",
|
||||||
|
"message": "Add a <kbd><tbody></kbd> section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tfoot",
|
||||||
|
"message": "Add a <kbd><tfoot></kbd> section for the total"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tbody tr", "min": 2 },
|
||||||
|
"message": "Add at least 2 item rows in tbody"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
82
lessons/pl/31-html-marquee.json
Normal file
82
lessons/pl/31-html-marquee.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-marquee",
|
||||||
|
"title": "HTML Marquee",
|
||||||
|
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "marquee-basic",
|
||||||
|
"title": "Scrolling Text",
|
||||||
|
"description": "The <kbd><marquee></kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!",
|
||||||
|
"task": "Create a simple marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Put some text inside like 'Welcome to my website!'",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee>Welcome to my website!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marquee-direction",
|
||||||
|
"title": "Direction & Behavior",
|
||||||
|
"description": "Control the marquee with attributes:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (default), slide (stops at edge), alternate (bounces)<br>• <kbd>scrollamount</kbd>: speed (default is 6)",
|
||||||
|
"task": "Create a bouncing marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee behavior=\"alternate\">Bounce! Bounce! Bounce!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
|
||||||
|
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marquee-retro",
|
||||||
|
"title": "Retro News Ticker",
|
||||||
|
"description": "Combine multiple marquee attributes for a classic news ticker effect. You can even put multiple elements inside!<br><br>Remember: This is deprecated HTML. Modern sites use CSS animations, but marquee is great for understanding web history.",
|
||||||
|
"task": "Create a news ticker:<br>1. A <kbd><marquee></kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee direction=\"left\" scrollamount=\"5\">BREAKING NEWS: Marquee element still works in browsers!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "direction", "value": "left" },
|
||||||
|
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
|
||||||
|
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/pl/32-html-svg.json
Normal file
102
lessons/pl/32-html-svg.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-svg",
|
||||||
|
"title": "HTML SVG",
|
||||||
|
"description": "Draw scalable vector graphics directly in HTML",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "svg-circle",
|
||||||
|
"title": "Drawing Circles",
|
||||||
|
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd><svg></kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd><circle></kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.",
|
||||||
|
"task": "Create an SVG with a circle:<br>1. An <kbd><svg></kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd><circle></kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "circle",
|
||||||
|
"message": "Add a <kbd><circle></kbd> element inside the SVG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "circle", "attr": "cx", "value": "100" },
|
||||||
|
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "circle", "attr": "cy", "value": "100" },
|
||||||
|
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "svg-rect-line",
|
||||||
|
"title": "Rectangles & Lines",
|
||||||
|
"description": "Draw rectangles with <kbd><rect></kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd><line></kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!",
|
||||||
|
"task": "Create an SVG with:<br>1. An <kbd><svg></kbd> (200x150)<br>2. A <kbd><rect></kbd> at position (20,20) with size 80x60<br>3. A <kbd><line></kbd> from (120,30) to (180,90) with a stroke color",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "rect",
|
||||||
|
"message": "Add a <kbd><rect></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "line",
|
||||||
|
"message": "Add a <kbd><line></kbd> element"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "svg-shapes",
|
||||||
|
"title": "Multiple Shapes",
|
||||||
|
"description": "Combine shapes to create simple graphics! Add <kbd>stroke</kbd> for outlines and <kbd>stroke-width</kbd> for thickness.<br><br>Use <kbd>fill=\"none\"</kbd> for hollow shapes. Shapes stack in order - later elements appear on top.",
|
||||||
|
"task": "Create a simple face:<br>1. An <kbd><svg></kbd> (200x200)<br>2. A large <kbd><circle></kbd> for the face<br>3. Two small <kbd><circle></kbd> elements for eyes<br>4. A <kbd><line></kbd> for the smile",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "circle", "min": 3 },
|
||||||
|
"message": "Add at least 3 circles (1 face + 2 eyes)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "line",
|
||||||
|
"message": "Add a <kbd><line></kbd> for the smile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
173
lessons/pl/flexbox.json
Normal file
173
lessons/pl/flexbox.json
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "flexbox",
|
||||||
|
"title": "CSS Flexbox",
|
||||||
|
"description": "Master the flexible box layout model for modern responsive designs",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "flexbox-1",
|
||||||
|
"title": "Container",
|
||||||
|
"description": "Learn how to create a flex container and understand the main and cross axes.",
|
||||||
|
"task": "Add <kbd>display: flex</kbd> to <kbd>.wrap</kbd> to create a flexbox layout.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "display: flex;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "display",
|
||||||
|
"expected": "flex"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>display: flex</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-2",
|
||||||
|
"title": "Direction & Wrap",
|
||||||
|
"description": "Control the direction and wrapping of flex items within a container.",
|
||||||
|
"task": "Add <kbd>flex-direction: column</kbd> and <kbd>flex-wrap: wrap</kbd> to <kbd>.wrap</kbd>.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div><div class='box'>4</div><div class='box'>5</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; height: 16rem; display: flex; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "flex-direction: column;\n flex-wrap: wrap;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex-direction",
|
||||||
|
"expected": "column"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex-direction: column</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex-wrap",
|
||||||
|
"expected": "wrap"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex-wrap: wrap</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-3",
|
||||||
|
"title": "Justify Content",
|
||||||
|
"description": "Learn how to align flex items along the main axis of the flex container.",
|
||||||
|
"task": "Add <kbd>justify-content: space-between</kbd> to <kbd>.wrap</kbd> to distribute the boxes evenly.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "justify-content: space-between;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "justify-content",
|
||||||
|
"expected": "space-between"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>justify-content: space-between</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-4",
|
||||||
|
"title": "Align Items",
|
||||||
|
"description": "Control how flex items are aligned along the cross axis of the flex container.",
|
||||||
|
"task": "Add <kbd>align-items: center</kbd> to <kbd>.wrap</kbd> to vertically center the boxes.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box tall'>1</div><div class='box'>2</div><div class='box short'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; display: flex; justify-content: center; } .tall { height: 6rem; } .short { height: 3rem; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 10rem; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "align-items: center;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "align-items",
|
||||||
|
"expected": "center"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>align-items: center</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-5",
|
||||||
|
"title": "Flex Grow",
|
||||||
|
"description": "The <kbd>flex</kbd> property controls how much an item grows relative to others.",
|
||||||
|
"task": "Add <kbd>flex: 2</kbd> to <kbd>.box2</kbd> to make it grow twice as wide.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box box1'>1</div><div class='box box2'>2</div><div class='box box3'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; display: flex; align-items: center; justify-content: center; } .box1 { background: coral; flex: 1; } .box2 { background: mediumseagreen; } .box3 { background: gold; flex: 1; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
|
||||||
|
"codePrefix": ".box2 {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "flex: 2;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex",
|
||||||
|
"expected": "2"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex: 2</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-6",
|
||||||
|
"title": "Align Self",
|
||||||
|
"description": "Use <kbd>align-self</kbd> to override alignment for a single flex item.",
|
||||||
|
"task": "Add <kbd>align-self: flex-start</kbd> to <kbd>.middle</kbd> to move it to the top.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box middle'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; } .middle { background: mediumseagreen; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 12rem; align-items: center; }",
|
||||||
|
"codePrefix": ".middle {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "align-self: flex-start;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "align-self",
|
||||||
|
"expected": "flex-start"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>align-self: flex-start</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
550
lessons/uk/00-basic-selectors.json
Normal file
550
lessons/uk/00-basic-selectors.json
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "css-basic-selectors",
|
||||||
|
"title": "CSS Selectors",
|
||||||
|
"description": "CSS selectors are the foundation of styling web pages, allowing you to target specific HTML elements for styling. This module introduces fundamental selector types including element type selectors, class selectors, ID selectors, and the universal selector.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "introduction-to-selectors",
|
||||||
|
"title": "What's a Selector?",
|
||||||
|
"description": "A CSS selector is the first part of a CSS rule that tells the browser which HTML elements should receive the styles defined in the declaration block. Selectors are essentially patterns that match against elements in your HTML document. Understanding selectors is fundamental because they determine which elements your CSS rules will affect. The element or elements targeted by a selector are referred to as the 'subject of the selector.' When writing a CSS rule, you first specify the selector, followed by curly braces that contain the style declarations.<br/>For example, to change the text color of elements, you can use the <kbd>color</kbd> property within your declaration block.<br><br><pre>/* Element selector */\np {\n color: orangered;\n /* │ └─── Indicates the value of the expression\n │ \n └─────────── Indicates the property of the expression */\n}</pre>",
|
||||||
|
"task": "Write a CSS rule using a type selector that targets all paragraph elements <kbd>p</kbd> in the document. Make the text blue by setting the <kbd>color</kbd> property to <kbd>blue</kbd>.",
|
||||||
|
"previewHTML": "<h1>Introduction to CSS Selectors</h1>\n<p>This paragraph should turn blue.</p>\n<div>This div element should remain unchanged.</div>\n<p>This second paragraph should also turn blue.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Write a type selector to target all paragraph elements */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p { color: blue }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>p { … }</kbd> to select all paragraph elements",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color:</kbd> property in your CSS rule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "blue",
|
||||||
|
"message": "Set the color value to <kbd>blue</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "blue"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>color: blue</kbd> to set the text color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type-selectors",
|
||||||
|
"title": "Type Selectors",
|
||||||
|
"description": "Type selectors (also called tag name selectors or element selectors) target HTML elements based on their tag name. For example, <kbd>p</kbd> selects all paragraph elements, <kbd>h1</kbd> selects all level-one headings, and <kbd>div</kbd> selects all division elements. Type selectors are the most fundamental way to select elements, applying styles consistently to all instances of a particular HTML element throughout your document. You can define a variety of CSS properties with type selectors, such as <kbd>color</kbd> for text color, <kbd>background-color</kbd> for the background, and <kbd>font-weight</kbd> for text emphasis. They provide a broad approach for styling your page and are often the starting point for more specific styling using other selector types.",
|
||||||
|
"task": "Write three separate CSS rules using type selectors to target specific HTML elements: make <kbd>h2</kbd> headings <kbd>purple</kbd>, give <kbd>span</kbd> elements a <kbd>yellow</kbd> background, and make <kbd>strong</kbd> elements <kbd>red</kbd>.",
|
||||||
|
"previewHTML": "<h2>Type Selectors Example</h2>\n<p>Regular paragraph text <span>with a highlighted span</span> that should have a yellow background.</p>\n<p>Another paragraph with <strong>strong important text</strong> that should be red.</p>\n<h2>Another Heading</h2>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
|
||||||
|
"codePrefix": "/* Write three separate type selectors below */\n\n",
|
||||||
|
"initialCode": "/* 1. Make h2 headings purple */\n\n\n/* 2. Give span elements a yellow background */\n\n\n/* 3. Make strong elements red */\n",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "/* 1. Make h2 headings purple */\nh2 {\n color: purple;\n}\n\n/* 2. Give span elements a yellow background */\nspan {\n background-color: yellow;\n}\n\n/* 3. Make strong elements red */\nstrong {\n color: red;\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^h2\\s*{",
|
||||||
|
"message": "Include an <kbd>h2 { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Set the <kbd>color</kbd> property to <kbd>purple</kbd> for h2 elements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "h2\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your h2 rule with a closing brace <kbd>}</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\s*{",
|
||||||
|
"message": "Include a <kbd>span { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Set a <kbd>background-color: yellow</kbd> for span elements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your span rule with a closing brace <kbd>}</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^strong\\s*{",
|
||||||
|
"message": "Include a <kbd>strong { … }</kbd> selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
|
||||||
|
"message": "Set the <kbd>color: red</kbd> for strong elements"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-selectors",
|
||||||
|
"title": "Class Selectors",
|
||||||
|
"description": "Class selectors target elements with a specific class attribute value. They begin with a dot (.) followed by the class name. Classes are powerful because they allow you to apply the same styles to multiple elements regardless of their type. An HTML element can have multiple classes (separated by spaces in the class attribute), and a class can be applied to any number of elements. When using class selectors, you can apply properties like <kbd>background-color</kbd> to set the background color of elements, and <kbd>font-weight</kbd> to control text thickness, making text bold or normal. This flexibility makes class selectors one of the most commonly used methods for applying styles in CSS, allowing for modular and reusable styling across your website.",
|
||||||
|
"task": "Create a CSS rule using a class selector that targets elements with the class <kbd>highlight</kbd>. Give these elements a <kbd>yellow</kbd> background and <kbd>bold</kbd> text.",
|
||||||
|
"previewHTML": "<h2>Using Class Selectors</h2>\n<p>This is a regular paragraph, but <span class=\"highlight\">this span has the highlight class</span> applied to it.</p>\n<p class=\"highlight\">This entire paragraph has the highlight class.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"highlight\">This list item is highlighted</li>\n</ul>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
|
||||||
|
"codePrefix": "/* Create a class selector for elements with the 'highlight' class */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.highlight\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>.highlight { … }</kbd> to create a class selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color:</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "yellow"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>yellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-weight:",
|
||||||
|
"message": "Include the <kbd>font-weight:</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-weight",
|
||||||
|
"expected": "bold"
|
||||||
|
},
|
||||||
|
"message": "Set the font-weight to <kbd>bold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "multiple-classes",
|
||||||
|
"title": "Multiple Classes",
|
||||||
|
"description": "HTML elements can have multiple classes applied simultaneously, allowing for composable and modular CSS designs. When an element has multiple classes, it will receive styles from all matching class selectors. This approach enables you to build a library of reusable CSS classes that can be combined in different ways. You can also target elements that have a specific combination of classes by chaining class selectors together without spaces (e.g., <kbd>.class1.class2</kbd>). When styling these elements, you might use properties like <kbd>border-color</kbd> to change the color of element borders, and <kbd>background-color</kbd> to set the background color of elements. This technique lets you create conditional styles that only apply when certain classes appear together.",
|
||||||
|
"task": "Complete the CSS rule that targets elements with both <kbd>card</kbd> and <kbd>featured</kbd> classes by chaining the selectors. Set the border-color to gold and the background-color to lemonchiffon to make featured cards stand out.",
|
||||||
|
"previewHTML": "<h2>Multiple Class Combinations</h2>\n<div class=\"card\">Regular Card</div>\n<div class=\"card featured\">Featured Card</div>\n<div class=\"featured\">Just Featured (not a card)</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* The .card class already has basic styling */\n/* Now target elements with BOTH classes: 'card' AND 'featured' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon }",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.card\\.featured\\s*{",
|
||||||
|
"message": "Chain the selectors as <kbd>.card.featured</kbd> (no space between them)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-color:",
|
||||||
|
"message": "Include the <kbd>border-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-color",
|
||||||
|
"expected": "gold"
|
||||||
|
},
|
||||||
|
"message": "Set the border color to <kbd>gold</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*;",
|
||||||
|
"message": "Make sure to end your CSS rule with a semicolon <kbd>;</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lemonchiffon"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>lemonchiffon</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "\\.card\\.featured\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class-with-type",
|
||||||
|
"title": "Combining Types",
|
||||||
|
"description": "You can combine type selectors with class selectors to target specific HTML elements that have a certain class. This creates a more specific selector that only matches when both conditions are true: the element is of the specified type AND it has the specified class. For example, <kbd>p.note</kbd> would select paragraph elements with the class <kbd>note</kbd>, but would not select divs or spans with that same class. You can style these combined selections using properties like <kbd>background-color</kbd> to set a colored background for your elements. This approach allows you to apply different styles to the same class when it appears on different element types.",
|
||||||
|
"task": "Create a CSS rule that specifically targets <kbd><span></kbd> elements with the class <kbd>highlight</kbd>. Make those elements have an orange background, while other elements with the highlight class remain untouched.",
|
||||||
|
"previewHTML": "<h2>Type and Class Combinations</h2>\n<p>This paragraph has a <span class=\"highlight\">highlighted span</span> that should have an orange background.</p>\n<p class=\"highlight\">This paragraph has the highlight class but should NOT have an orange background.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
|
||||||
|
"sandboxCSS": "h2, p, span { padding: 5px; }",
|
||||||
|
"codePrefix": "/* The .highlight class already sets font-weight to bold */\n/* Now target ONLY span elements with the highlight class */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^span\\.highlight\\s*{",
|
||||||
|
"message": "Use <kbd>span.highlight</kbd> selector (no space between element and class)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "orange"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>orange</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "span\\.highlight\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-selectors",
|
||||||
|
"title": "ID Selectors",
|
||||||
|
"description": "ID selectors target elements with a specific id attribute. They begin with a hash/pound sign (#) followed by the ID name. Unlike classes, IDs must be unique within a document—each ID value should be used only once per page. ID selectors have higher specificity than class or element selectors, meaning they override those selectors when conflicts arise. When styling with ID selectors, you can use properties like <kbd>color</kbd> to define text color, and <kbd>text-decoration</kbd> to control the appearance of text, such as adding underlines to elements. Because of their uniqueness requirement, IDs are best used for one-of-a-kind elements like page headers, main navigation, or specific unique components that appear only once on a page.",
|
||||||
|
"task": "Create a CSS rule with an ID selector that targets the element with the ID <kbd>main-title</kbd>. Set its color to purple and add an underline with <kbd>text-decoration: underline</kbd>.",
|
||||||
|
"previewHTML": "<h1 id=\"main-title\">Main Page Title</h1>\n<p>Regular paragraph content.</p>\n<h2>Secondary Heading</h2>\n<p id=\"intro\">Introduction paragraph (different ID).</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Create an ID selector to target the element with id=\"main-title\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^#main-title\\s*{",
|
||||||
|
"message": "Start your rule with <kbd>#main-title</kbd> to create an ID selector",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "color",
|
||||||
|
"expected": "purple"
|
||||||
|
},
|
||||||
|
"message": "Set the color to <kbd>purple</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-decoration:",
|
||||||
|
"message": "Include the <kbd>text-decoration</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "text-decoration",
|
||||||
|
"expected": "underline"
|
||||||
|
},
|
||||||
|
"message": "Set the text-decoration to <kbd>underline</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "#main-title\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id-with-type",
|
||||||
|
"title": "Type + ID",
|
||||||
|
"description": "Similar to how you can combine type and class selectors, you can also combine type selectors with ID selectors. For example, <kbd>h1#title</kbd> targets an h1 element with the ID 'title'. When using this combined approach, you can apply CSS properties like <kbd>font-style</kbd> to control the slant of the text, making it italic or normal. While this selector combination is more specific than using just the ID selector, it's often unnecessary since IDs should already be unique in a document. However, this technique can be useful for improving code readability or when you want to emphasize that a particular ID should only appear on a specific element type.",
|
||||||
|
"task": "Create a CSS rule that combines a type selector with an ID selector to target specifically a paragraph element with the ID <kbd>special</kbd>. Set its font style to italic.",
|
||||||
|
"previewHTML": "<h2 id=\"special\">Heading with ID \"special\" (should NOT be affected)</h2>\n<p id=\"special\">Paragraph with ID \"special\" (should become italic)</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
|
||||||
|
"codePrefix": "/* Create a combined type+ID selector for a paragraph with id=\"special\" */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^p#special\\s*{",
|
||||||
|
"message": "Use <kbd>p#special</kbd> to target paragraphs with ID <kbd>special</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "font-style:",
|
||||||
|
"message": "Include the <kbd>font-style</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "font-style",
|
||||||
|
"expected": "italic"
|
||||||
|
},
|
||||||
|
"message": "Set the font-style to <kbd>italic</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "p#special\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "selector-lists",
|
||||||
|
"title": "Selector Lists",
|
||||||
|
"description": "When multiple elements need the same styling, you can group them together using a selector list (also known as grouping selectors). Selector lists are created by separating individual selectors with commas. This approach reduces repetition in your CSS, making it more maintainable and efficient. For example, <kbd>h1, h2, h3 { color: blue; }</kbd> applies the same blue color to all three heading levels. When styling multiple selectors at once, you can apply properties like <kbd>background-color</kbd> to set the background, <kbd>border-left</kbd> to create a left border with a specific thickness, style, and color, and <kbd>padding-left</kbd> to create space between the content and the left border. Whitespace around commas is optional, and each selector in the list can be any valid selector type-elements, classes, IDs, or even more complex selectors.",
|
||||||
|
"task": "Create a selector list that applies the same styles to three different elements: paragraphs with class <kbd>note</kbd>, list items with class <kbd>important</kbd>, and the element with ID <kbd>summary</kbd>. Give them a <kbd>lightyellow</kbd> background, a <kbd>gold</kbd> left border, and some left <kbd>padding</kbd>.",
|
||||||
|
"previewHTML": "<p class=\"note\">This is a note paragraph.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"important\">Important list item</li>\n</ul>\n<div id=\"summary\">Summary section</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
|
||||||
|
"codePrefix": "/* Create a selector list to apply the same styles to multiple different elements */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px\n}",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "p.note",
|
||||||
|
"message": "Include <kbd>p.note</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "li.important",
|
||||||
|
"message": "Include <kbd>li.important</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "#summary",
|
||||||
|
"message": "Include <kbd>#summary</kbd> in your selector list",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
|
||||||
|
"message": "Create a comma-separated list with all three selectors",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color:",
|
||||||
|
"message": "Include the <kbd>background-color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "background-color",
|
||||||
|
"expected": "lightyellow"
|
||||||
|
},
|
||||||
|
"message": "Set the background color to <kbd>lightyellow</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "border-left:",
|
||||||
|
"message": "Include the <kbd>border-left</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "border-left",
|
||||||
|
"expected": "3px solid gold"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>border-left: 3px solid gold</kbd> to create a left border"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "padding-left:",
|
||||||
|
"message": "Include the <kbd>padding-left</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "padding-left",
|
||||||
|
"expected": "10px"
|
||||||
|
},
|
||||||
|
"message": "Use <kbd>padding-left: 10px</kbd> to add left padding"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "universal-selector",
|
||||||
|
"title": "Universal (*)",
|
||||||
|
"description": "The universal selector is denoted by an asterisk (*) and matches any element of any type. It selects everything in the document or, when combined with other selectors, everything within a specific context. For example, <kbd>* { margin: 0; }</kbd> removes margins from all elements, while <kbd>article *</kbd> selects all elements inside article elements. When using the universal selector in combination with other selectors, you can apply properties like <kbd>margin</kbd> to control the spacing around elements. The universal selector is powerful but should be used carefully due to its broad impact. It's commonly used in CSS resets, to override default browser styling, or to target all children of a particular element.",
|
||||||
|
"task": "Use the universal selector to remove margins from all elements inside the container div. Create a rule using <kbd>div.container *</kbd> as the selector and set <kbd>margin: 0</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"container\">\n <h2>Inside Container</h2>\n <p>This paragraph is inside the container.</p>\n <ul>\n <li>List item inside container</li>\n </ul>\n</div>\n<p>This paragraph is outside the container and should not be affected.</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use the universal selector to target all elements inside the container */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^div\\.container\\s+\\*\\s*{",
|
||||||
|
"message": "Use <kbd>div.container *</kbd> selector (with a space between container and *)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "margin:",
|
||||||
|
"message": "Include the <kbd>margin</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "margin",
|
||||||
|
"expected": "0"
|
||||||
|
},
|
||||||
|
"message": "Set margin to <kbd>0</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
|
||||||
|
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "specificity-basics",
|
||||||
|
"title": "Specificity",
|
||||||
|
"description": "CSS specificity determines which styles take precedence when multiple conflicting rules target the same element. Specificity follows a hierarchical system: inline styles have the highest specificity, followed by ID selectors, then class/attribute/pseudo-class selectors, and finally element/pseudo-element selectors. This can be conceptualized as a four-part score (inline, ID, class, element). When creating multiple rules that may target the same elements, you can use the <kbd>color</kbd> property to set text colors, and specificity will determine which color is actually applied. Understanding specificity is crucial for predictable styling and debugging CSS conflicts. When two selectors have equal specificity, the one that comes last in the stylesheet wins.",
|
||||||
|
"task": "Examine the existing CSS rules and add a new rule with higher specificity to override the text color of the paragraph. Create a rule using '.content p' as the selector and set color: green.",
|
||||||
|
"previewHTML": "<div class=\"content\">\n <p>What color will this paragraph be? Look at the CSS rules and their specificity.</p>\n</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
|
||||||
|
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
|
||||||
|
"codePrefix": "/* These CSS rules target the same paragraph but have different specificity */\n\n/* Rule 1: Element selector (lowest specificity) */\np {\n color: red;\n}\n\n/* Rule 2: Descendant selector (higher specificity than just 'p') */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "^\\.content\\s+p\\s*{",
|
||||||
|
"message": "Use <kbd>.content p</kbd> as your selector (note the space between)",
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "color:",
|
||||||
|
"message": "Include the <kbd>color</kbd> property"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "green",
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
62
lessons/uk/00-welcome.json
Normal file
62
lessons/uk/00-welcome.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "welcome",
|
||||||
|
"title": "Code Crispies",
|
||||||
|
"description": "Welcome to Code Crispies - your interactive web development learning platform",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "get-started",
|
||||||
|
"title": "Get Started",
|
||||||
|
"description": "<strong>Code Crispies</strong> is a free, open-source platform for learning web development through hands-on exercises. No account required!<br><br><strong>What you'll learn:</strong><br>• <strong>HTML</strong> - Semantic elements, forms, tables, SVG (<em>HTML Block & Inline</em>, <em>HTML Forms</em>, <em>HTML Tables</em>)<br>• <strong>CSS</strong> - Selectors, box model, flexbox, animations (<em>CSS Selectors</em>, <em>CSS Box Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br><strong>How it works:</strong><br>1. Read the task in the left panel<br>2. Write code in the editor<br>3. See live results in the preview<br>4. Get instant feedback with hints<br><br><strong>Keyboard shortcuts:</strong> <kbd>Ctrl+Z</kbd> to undo, <kbd>Ctrl+Shift+Z</kbd> to redo<br><br><strong>More resources:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript solutions<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript technology roadmap",
|
||||||
|
"task": "Hello World",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; } h1 { color: #6366f1; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<h1>Hello World</h1>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "Hello",
|
||||||
|
"message": "Write 'Hello World'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "overview",
|
||||||
|
"title": "Overview",
|
||||||
|
"description": "<strong>You're ready!</strong> Open the menu (☰) to explore all modules.<br><br><strong>Recommended learning path:</strong><br>1. <em>HTML Block & Inline</em> - Understand container vs inline elements<br>2. <em>HTML Forms</em> - Build interactive forms with validation<br>3. <em>CSS Selectors</em> - Target elements precisely<br>4. <em>CSS Box Model</em> - Master padding, margin, borders<br>5. <em>CSS Flexbox</em> - Create flexible layouts<br>6. <em>CSS Animations</em> - Add motion and transitions<br><br><strong>Tips:</strong><br>• Use <em>Show Expected</em> to see the target result<br>• Your progress is saved automatically<br>• Try Emmet in HTML mode: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Made by <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>",
|
||||||
|
"task": "Click Next to continue",
|
||||||
|
"previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"solution": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "Hello",
|
||||||
|
"message": "Click Next to continue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "playground",
|
||||||
|
"title": "Playground",
|
||||||
|
"mode": "playground",
|
||||||
|
"description": "",
|
||||||
|
"task": "",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Start coding!</p>",
|
||||||
|
"solution": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
180
lessons/uk/01-box-model.json
Normal file
180
lessons/uk/01-box-model.json
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "box-model",
|
||||||
|
"title": "CSS Box Model",
|
||||||
|
"description": "Master the fundamental principles of space management in web design through the CSS box model. This module explores how content, padding, borders, and margins combine to create layout structures that are both visually appealing and structurally sound.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "box-model-1",
|
||||||
|
"title": "Box Model Components",
|
||||||
|
"description": "The CSS box model consists of four concentric layers: content area (innermost), padding, border, and margin (outermost). Understanding how these components interact is essential for precise layout control.",
|
||||||
|
"task": "Add <kbd>padding: 1rem</kbd> to <kbd>.box</kbd> to create space between its content and border.",
|
||||||
|
"previewHTML": "<div class=\"box\">Box Model Components</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".box {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "padding: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "padding", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>padding: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-2",
|
||||||
|
"title": "Adding Borders",
|
||||||
|
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.",
|
||||||
|
"task": "Add <kbd>border: 2px solid darkslategray</kbd> to <kbd>.box</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">This box needs a border</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".box {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "border: 2px solid darkslategray;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border:\\s*2px\\s+solid\\s+darkslategray",
|
||||||
|
"message": "Set <kbd>border: 2px solid darkslategray</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-3",
|
||||||
|
"title": "Adding Margins",
|
||||||
|
"description": "Margins create space between elements, controlling how they relate to one another within a layout. Unlike padding (which affects internal spacing), margins exist outside the element's border.",
|
||||||
|
"task": "Add <kbd>margin: 1rem</kbd> to <kbd>.outer</kbd> to create space between it and the adjacent element.",
|
||||||
|
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".outer {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "margin", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>margin: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-4",
|
||||||
|
"title": "Box Sizing: Border-Box",
|
||||||
|
"description": "The <kbd>box-sizing</kbd> property determines how element dimensions are calculated. The default <kbd>content-box</kbd> excludes padding and border from width/height, while <kbd>border-box</kbd> includes them, making layout calculations more intuitive.",
|
||||||
|
"task": "Add <kbd>box-sizing: border-box</kbd> to <kbd>.sized</kbd> so padding and border are included in its width.",
|
||||||
|
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".sized {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "box-sizing: border-box;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||||
|
"message": "Set <kbd>box-sizing: border-box</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-5",
|
||||||
|
"title": "Margin Collapse",
|
||||||
|
"description": "When two vertical margins meet, they collapse to the larger value instead of adding up. Understanding this behavior is crucial for consistent vertical spacing.",
|
||||||
|
"task": "Add <kbd>margin-bottom: 2rem</kbd> to <kbd>.first</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.",
|
||||||
|
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">This paragraph has a bottom margin.</p><p class=\"second\">This paragraph has a top margin of 1rem.</p></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".first {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin-bottom: 2rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "margin-bottom", "expected": "2rem" },
|
||||||
|
"message": "Set <kbd>margin-bottom: 2rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-6",
|
||||||
|
"title": "Margin Shorthand Notation",
|
||||||
|
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.",
|
||||||
|
"task": "Add <kbd>margin: 1rem 2rem</kbd> to <kbd>.spaced</kbd> for 1rem top/bottom and 2rem left/right.",
|
||||||
|
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".spaced {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "margin: 1rem 2rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "margin:\\s*1rem\\s+2rem",
|
||||||
|
"message": "Set <kbd>margin: 1rem 2rem</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-7",
|
||||||
|
"title": "Padding Shorthand Notation",
|
||||||
|
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.",
|
||||||
|
"task": "Add <kbd>padding: 1.5rem</kbd> to <kbd>.padded</kbd> to add equal padding on all sides.",
|
||||||
|
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".padded {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "padding: 1.5rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "padding", "expected": "1.5rem" },
|
||||||
|
"message": "Set <kbd>padding: 1.5rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-8",
|
||||||
|
"title": "Border on Specific Sides",
|
||||||
|
"description": "For granular control, you can target specific sides with <kbd>border-top</kbd>, <kbd>border-right</kbd>, <kbd>border-bottom</kbd>, or <kbd>border-left</kbd>.",
|
||||||
|
"task": "Add <kbd>border-bottom: 4px solid dodgerblue</kbd> to <kbd>.line</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": ".line {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "border-bottom: 4px solid dodgerblue;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue",
|
||||||
|
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
116
lessons/uk/05-units-variables.json
Normal file
116
lessons/uk/05-units-variables.json
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "units-variables",
|
||||||
|
"title": "CSS Units & Variables",
|
||||||
|
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "units-1",
|
||||||
|
"title": "Absolute vs. Relative Units",
|
||||||
|
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.",
|
||||||
|
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">Resize me!</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set flexible sizing */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: 80%;\n max-width: 37.5rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "width", "message": "Use <kbd>width</kbd> property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to <kbd>80%</kbd>" },
|
||||||
|
{ "type": "contains", "value": "max-width", "message": "Use <kbd>max-width</kbd> property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "max-width", "expected": "37.5rem" },
|
||||||
|
"message": "Set max-width to <kbd>37.5rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-2",
|
||||||
|
"title": "CSS Custom Properties",
|
||||||
|
"description": "Define and reuse variables (--custom properties) to centralize your theme values.",
|
||||||
|
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>#6200ee</kbd> and apply it as the border color on <kbd>.themed</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"themed\">Variable Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define and use a CSS variable */\n:root {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.themed { }",
|
||||||
|
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "--main-color",
|
||||||
|
"message": "Define <kbd>--main-color</kbd> in :root",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "var(--main-color)",
|
||||||
|
"message": "Use <kbd>var(--main-color)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "border", "expected": "var(--main-color)" },
|
||||||
|
"message": "Apply variable to border color",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-3",
|
||||||
|
"title": "Unit Calculations (calc)",
|
||||||
|
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.",
|
||||||
|
"task": "Set the <kbd>width</kbd> of <kbd>.sized</kbd> to <kbd>calc(100% - 2rem)</kbd> and <kbd>min-height</kbd> to <kbd>calc(10vh + 1rem)</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"sized\">Calc Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "width:\\s*calc\\(100% - 2rem\\)",
|
||||||
|
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
|
||||||
|
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-4",
|
||||||
|
"title": "Viewport & Responsive Units",
|
||||||
|
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.",
|
||||||
|
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"view\">Viewport Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use viewport units */\n.view {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " width: 50vw;\n height: 20vh;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "vw", "message": "Use <kbd>vw</kbd> unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "vh", "message": "Use <kbd>vh</kbd> unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" },
|
||||||
|
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
145
lessons/uk/06-transitions-animations.json
Normal file
145
lessons/uk/06-transitions-animations.json
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "transitions-animations",
|
||||||
|
"title": "CSS Animations",
|
||||||
|
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "transitions-1",
|
||||||
|
"title": "Transitions",
|
||||||
|
"description": "Learn how to apply <kbd>transition</kbd> to properties for smooth changes on state changes.",
|
||||||
|
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Hover Me</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add transition */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " transition: background-color 0.3s;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition",
|
||||||
|
"message": "Use the <kbd>transition</kbd> property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||||
|
"message": "Set <kbd>transition: background-color 0.3s</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-2",
|
||||||
|
"title": "Timing Funcs",
|
||||||
|
"description": "Explore easing functions like <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> to control animation pacing.",
|
||||||
|
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Timing</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set timing function */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " transition-timing-function: ease-in-out;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition-timing-function",
|
||||||
|
"message": "Use <kbd>transition-timing-function</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||||
|
"message": "Set timing to <kbd>ease-in-out</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-3",
|
||||||
|
"title": "Keyframes",
|
||||||
|
"description": "Create named animations using <kbd>@keyframes</kbd> and apply them via the <kbd>animation</kbd> shorthand.",
|
||||||
|
"task": "Define a keyframe at <kbd>50%</kbd> with <kbd>transform: translateY(-20px)</kbd> and apply <kbd>animation: bounce 1s infinite</kbd> to <kbd>.ball</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"ball\"></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define keyframes and apply animation */\n@keyframes bounce {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.ball { }",
|
||||||
|
"solution": " 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "@keyframes bounce",
|
||||||
|
"message": "Define <kbd>@keyframes bounce</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "50%.*transform: translateY\\(-20px\\)",
|
||||||
|
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "animation",
|
||||||
|
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "animation:.*bounce.*1s.*infinite",
|
||||||
|
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-4",
|
||||||
|
"title": "Anim Properties",
|
||||||
|
"description": "Fine-tune animations with <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd>, and <kbd>animation-fill-mode</kbd>.",
|
||||||
|
"task": "Apply the <kbd>pulse</kbd> animation to <kbd>.box</kbd> with <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd>, and <kbd>animation-fill-mode: forwards</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"box\">Pulse</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply animation properties */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " animation-name: pulse;\n animation-duration: 2s;\n animation-delay: 1s;\n animation-iteration-count: 2;\n animation-fill-mode: forwards;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-name", "expected": "pulse" },
|
||||||
|
"message": "Set <kbd>animation-name: pulse</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-duration", "expected": "2s" },
|
||||||
|
"message": "Set <kbd>animation-duration: 2s</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-delay", "expected": "1s" },
|
||||||
|
"message": "Set <kbd>animation-delay: 1s</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||||
|
"message": "Set <kbd>animation-iteration-count: 2</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||||
|
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
126
lessons/uk/08-responsive.json
Normal file
126
lessons/uk/08-responsive.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "responsive-design",
|
||||||
|
"title": "CSS Responsive Design",
|
||||||
|
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "responsive-1",
|
||||||
|
"title": "Media Queries",
|
||||||
|
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.",
|
||||||
|
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"panel\">Resize the window</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add your media query below */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"solution": "@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||||
|
"message": "Use <kbd>@media (max-width: 600px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".panel",
|
||||||
|
"message": "Target <kbd>.panel</kbd> inside the media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "background", "expected": "lightcoral" },
|
||||||
|
"message": "Set <kbd>background: lightcoral</kbd>",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-2",
|
||||||
|
"title": "Fluid Type",
|
||||||
|
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.",
|
||||||
|
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.",
|
||||||
|
"previewHTML": "<p class=\"text\">Fluid Typography</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply fluid font sizing */\n.text {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " font-size: 5vw;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-3",
|
||||||
|
"title": "Flex Grids",
|
||||||
|
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.",
|
||||||
|
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.cards</kbd>.",
|
||||||
|
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Create a responsive grid */\n.cards {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"solution": " display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "display", "expected": "grid" },
|
||||||
|
"message": "Set <kbd>display: grid</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||||
|
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "gap", "expected": "1rem" },
|
||||||
|
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-4",
|
||||||
|
"title": "Mobile-First",
|
||||||
|
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.",
|
||||||
|
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.",
|
||||||
|
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add mobile-first enhancement */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"solution": "@media (min-width: 768px) {\n .sidebar {\n width: 250px;\n }\n}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||||
|
"message": "Use <kbd>@media (min-width: 768px)</kbd>",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".sidebar",
|
||||||
|
"message": "Target <kbd>.sidebar</kbd> inside media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "width", "expected": "250px" },
|
||||||
|
"message": "Set <kbd>width: 250px</kbd>",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/uk/20-html-elements.json
Normal file
97
lessons/uk/20-html-elements.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-elements",
|
||||||
|
"title": "HTML Block & 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><div></kbd>, <kbd><p></kbd>, <kbd><h1></kbd>, <kbd><section></kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd><span></kbd>, <kbd><a></kbd>, <kbd><strong></kbd>, <kbd><em></kbd>",
|
||||||
|
"task": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (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>",
|
||||||
|
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Add a <kbd><p></kbd> paragraph element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "p", "child": "strong" },
|
||||||
|
"message": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "semantic-containers",
|
||||||
|
"title": "Semantic Tags",
|
||||||
|
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd><header></kbd> - Page or section header<br><kbd><nav></kbd> - Navigation links<br><kbd><main></kbd> - Main content area<br><kbd><section></kbd> - Thematic grouping<br><kbd><article></kbd> - Self-contained content<br><kbd><footer></kbd> - Page or section footer",
|
||||||
|
"task": "Create a basic page structure:<br>1. Add a <kbd><header></kbd> with an <kbd><h1></kbd> containing the text 'My Website'<br>2. Add a <kbd><main></kbd> element with a paragraph saying 'Welcome to my site!'<br>3. Add a <kbd><footer></kbd> with a paragraph saying 'Copyright 2025'",
|
||||||
|
"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": "",
|
||||||
|
"solution": "<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 <kbd><header></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "main",
|
||||||
|
"message": "Add a <kbd><main></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "footer",
|
||||||
|
"message": "Add a <kbd><footer></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "header", "child": "h1" },
|
||||||
|
"message": "Add an <kbd><h1></kbd> heading inside your header"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "div-vs-span",
|
||||||
|
"title": "div & span",
|
||||||
|
"description": "When you need a container without semantic meaning:<br><br><kbd><div></kbd> - Generic block container (for layout/grouping)<br><kbd><span></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><span></kbd> to style it differently. Wrap the whole quote in a <kbd><div></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.",
|
||||||
|
"solution": "<div>The most <span>highlighted</span> moment was unforgettable.</div>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "div",
|
||||||
|
"message": "Wrap everything in a <kbd><div></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "span",
|
||||||
|
"message": "Add a <kbd><span></kbd> around the word <kbd>highlighted</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_text",
|
||||||
|
"value": { "selector": "span", "text": "highlighted" },
|
||||||
|
"message": "The <kbd><span></kbd> should contain the word <kbd>highlighted</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/uk/21-html-forms-basic.json
Normal file
102
lessons/uk/21-html-forms-basic.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-basic",
|
||||||
|
"title": "HTML Forms",
|
||||||
|
"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><form></kbd> wrapper. Inside, use <kbd><label></kbd> to describe inputs and <kbd><input></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:<br>1. A <kbd><label></kbd> with the text 'Name:' and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd><input></kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> 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": "",
|
||||||
|
"solution": "<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 <kbd><form></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for your input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "input",
|
||||||
|
"message": "Add an <kbd><input></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "label", "attr": "for", "value": null },
|
||||||
|
"message": "Add a <kbd>for</kbd> attribute to your label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input", "attr": "id", "value": null },
|
||||||
|
"message": "Add an <kbd>id</kbd> 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 two fields:<br>1. An email field: <kbd><label for=\"email\">Email:</label></kbd> and <kbd><input type=\"email\" id=\"email\"></kbd><br>2. A password field: <kbd><label for=\"password\">Password:</label></kbd> and <kbd><input type=\"password\" id=\"password\"></kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n \n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\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><button type=\"submit\"></kbd> - Preferred, flexible content<br><kbd><input type=\"submit\"></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; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \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 <kbd>Sign In</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
112
lessons/uk/22-html-forms-validation.json
Normal file
112
lessons/uk/22-html-forms-validation.json
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-forms-validation",
|
||||||
|
"title": "HTML 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><input type=\"text\" required></kbd><br><br>The browser shows a validation message automatically.",
|
||||||
|
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Submit</button>\n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \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 <kbd>required</kbd> attribute to the name input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
|
||||||
|
"message": "Add the <kbd>required</kbd> attribute to the email input"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "input-constraints",
|
||||||
|
"title": "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 to the password input:<br>1. Add <kbd>minlength=\"8\"</kbd> for minimum length<br>2. Add <kbd>maxlength=\"20\"</kbd> for maximum length<br>3. Add <kbd>placeholder=\"Enter password\"</kbd> as a hint",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } 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; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
|
||||||
|
"solution": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \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 <kbd>minlength</kbd>=\"8\" to the password input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
|
||||||
|
"message": "Add <kbd>maxlength</kbd>=\"20\" to the password input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
|
||||||
|
"message": "Add a <kbd>placeholder</kbd> to hint what to enter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "complete-registration",
|
||||||
|
"title": "Full 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; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } 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; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; 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 <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">Email *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Password *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
|
||||||
|
"solution": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Password *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n I agree to the Terms of Service *\n </label>\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 <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "type", "value": "email" },
|
||||||
|
"message": "Set the email input <kbd>type=\"email\"</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#email", "attr": "required", "value": true },
|
||||||
|
"message": "Make the email field <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "type", "value": "password" },
|
||||||
|
"message": "Set the password input <kbd>type=\"password\"</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "required", "value": true },
|
||||||
|
"message": "Make the password field <kbd>required</kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
|
||||||
|
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "#terms", "attr": "required", "value": true },
|
||||||
|
"message": "Make the terms checkbox <kbd>required</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/uk/23-html-details-summary.json
Normal file
97
lessons/uk/23-html-details-summary.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-details-summary",
|
||||||
|
"title": "HTML Details & Summary",
|
||||||
|
"description": "Create expandable content sections without JavaScript",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "details-summary-basic",
|
||||||
|
"title": "First Widget",
|
||||||
|
"description": "The <kbd><details></kbd> element creates a collapsible section. The <kbd><summary></kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!",
|
||||||
|
"task": "Create a <kbd><details></kbd> element with:<br>1. A <kbd><summary></kbd> saying 'Click to reveal'<br>2. A <kbd><p></kbd> with the text 'This content was hidden!'",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<details>\n <summary>Click to reveal</summary>\n <p>This content was hidden!</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "details",
|
||||||
|
"message": "Add a <kbd><details></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "summary",
|
||||||
|
"message": "Add a <kbd><summary></kbd> inside the details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "details", "child": "summary" },
|
||||||
|
"message": "The <kbd><summary></kbd> must be inside <kbd><details></kbd>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parent_child",
|
||||||
|
"value": { "parent": "details", "child": "p" },
|
||||||
|
"message": "Add a <kbd><p></kbd> inside <kbd><details></kbd> for the hidden content"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "details-open-attribute",
|
||||||
|
"title": "Pre-expanded Details",
|
||||||
|
"description": "By default, <kbd><details></kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.",
|
||||||
|
"task": "Add the <kbd>open</kbd> attribute to the <kbd><details></kbd> element to show the content by default.",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "<details>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
|
||||||
|
"solution": "<details open>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "details", "attr": "open", "value": true },
|
||||||
|
"message": "Add the <kbd>open</kbd> attribute to <kbd><details></kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "faq-accordion",
|
||||||
|
"title": "FAQ Accordion",
|
||||||
|
"description": "Multiple <kbd><details></kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3>summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>></kbd> nests inside, <kbd>+</kbd> adds siblings.",
|
||||||
|
"task": "Create an FAQ section with:<br>1. An <kbd><h1></kbd> saying 'Frequently Asked Questions'<br>2. Three <kbd><details></kbd> elements, each with a question in <kbd><summary></kbd> and an answer in <kbd><p></kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<h1>Frequently Asked Questions</h1>\n\n<details>\n <summary>What is HTML5?</summary>\n <p>HTML5 is the latest version of HTML with new semantic elements and APIs.</p>\n</details>\n\n<details>\n <summary>Do I need JavaScript?</summary>\n <p>Many interactive features work with pure HTML5, no JavaScript required!</p>\n</details>\n\n<details>\n <summary>Is this accessible?</summary>\n <p>Yes! Native HTML elements have built-in keyboard and screen reader support.</p>\n</details>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "h1",
|
||||||
|
"message": "Add an <kbd><h1></kbd> heading for the FAQ title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "details", "min": 3 },
|
||||||
|
"message": "Create at least 3 <kbd><details></kbd> elements for the FAQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "summary", "min": 3 },
|
||||||
|
"message": "Each <kbd><details></kbd> needs a <kbd><summary></kbd> for the question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "details p", "min": 3 },
|
||||||
|
"message": "Each <kbd><details></kbd> needs a <kbd><p></kbd> for the answer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/uk/24-html-progress-meter.json
Normal file
102
lessons/uk/24-html-progress-meter.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-progress-meter",
|
||||||
|
"title": "HTML Progress & Meter",
|
||||||
|
"description": "Display completion status and scalar measurements natively",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "progress-basic",
|
||||||
|
"title": "Progress Bars",
|
||||||
|
"description": "The <kbd><progress></kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd><progress>...</progress></kbd> with fallback text inside for older browsers.",
|
||||||
|
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd><label></kbd> saying 'Download:'<br>2. Add a <kbd><progress></kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<label for=\"download\">Download:</label>\n<progress id=\"download\" value=\"70\" max=\"100\">70%</progress>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "progress",
|
||||||
|
"message": "Add a <kbd><progress></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "progress", "attr": "value", "value": "70" },
|
||||||
|
"message": "Set <kbd>value=</kbd>\"70\" on the progress element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "progress", "attr": "max", "value": "100" },
|
||||||
|
"message": "Set <kbd>max=</kbd>\"100\" on the progress element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for the progress bar"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "progress-indeterminate",
|
||||||
|
"title": "Indeterminate Progress",
|
||||||
|
"description": "When progress is unknown (like loading), omit the <kbd>value</kbd> attribute. This creates an animated indeterminate state.<br><br>Useful for network requests or processes with unknown duration.",
|
||||||
|
"task": "Create a loading indicator:<br>1. Add a <kbd><p></kbd> saying 'Loading...'<br>2. Add a <kbd><progress></kbd> without a value attribute",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<p>Loading...</p>\n<progress></progress>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "progress",
|
||||||
|
"message": "Add a <kbd><progress></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "p",
|
||||||
|
"message": "Add a <kbd><p></kbd> with loading text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "meter-gauge",
|
||||||
|
"title": "Meter Gauges",
|
||||||
|
"description": "The <kbd><meter></kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!",
|
||||||
|
"task": "Create a battery level meter:<br>1. Add a <kbd><label></kbd> saying 'Battery:'<br>2. Add a <kbd><meter></kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<label for=\"battery\">Battery:</label>\n<meter id=\"battery\" value=\"0.8\" min=\"0\" max=\"1\" low=\"0.2\" high=\"0.8\" optimum=\"1\">80%</meter>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "meter",
|
||||||
|
"message": "Add a <kbd><meter></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "meter", "attr": "value", "value": "0.8" },
|
||||||
|
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "meter", "attr": "low", "value": "0.2" },
|
||||||
|
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "label",
|
||||||
|
"message": "Add a <kbd><label></kbd> for the meter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
127
lessons/uk/30-html-tables.json
Normal file
127
lessons/uk/30-html-tables.json
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-tables",
|
||||||
|
"title": "HTML Tables",
|
||||||
|
"description": "Create structured data tables with headers and captions",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "table-basic",
|
||||||
|
"title": "Basic Table Structure",
|
||||||
|
"description": "Tables use <kbd><table></kbd> with <kbd><tr></kbd> for rows. Inside rows, use <kbd><th></kbd> for headers and <kbd><td></kbd> for data cells.<br><br>The <kbd><caption></kbd> element provides an accessible title for the table.",
|
||||||
|
"task": "Create a simple table with:<br>1. A <kbd><caption></kbd> saying 'Fruit Prices'<br>2. A header row with 'Fruit' and 'Price' columns<br>3. At least 2 data rows",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Fruit Prices</caption>\n <tr>\n <th>Fruit</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Apple</td>\n <td>$1.50</td>\n </tr>\n <tr>\n <td>Banana</td>\n <td>$0.75</td>\n </tr>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> for the table title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "th", "min": 2 },
|
||||||
|
"message": "Add at least 2 header cells (th)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tr", "min": 3 },
|
||||||
|
"message": "Add at least 3 rows (1 header + 2 data rows)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table-thead-tbody",
|
||||||
|
"title": "Table Head & Body",
|
||||||
|
"description": "Use <kbd><thead></kbd> to group header rows and <kbd><tbody></kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd><tfoot></kbd> for footer rows like totals.",
|
||||||
|
"task": "Create a structured table:<br>1. A <kbd><caption></kbd> with 'Monthly Sales'<br>2. A <kbd><thead></kbd> with Month and Revenue headers<br>3. A <kbd><tbody></kbd> with at least 2 data rows",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Monthly Sales</caption>\n <thead>\n <tr>\n <th>Month</th>\n <th>Revenue</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>January</td>\n <td>$12,500</td>\n </tr>\n <tr>\n <td>February</td>\n <td>$14,200</td>\n </tr>\n </tbody>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "thead",
|
||||||
|
"message": "Add a <kbd><thead></kbd> for the header section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tbody",
|
||||||
|
"message": "Add a <kbd><tbody></kbd> for the data rows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tbody tr", "min": 2 },
|
||||||
|
"message": "Add at least 2 data rows in tbody"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "table-complete",
|
||||||
|
"title": "Complete Table with Footer",
|
||||||
|
"description": "Add <kbd><tfoot></kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
|
||||||
|
"task": "Create a complete table:<br>1. A <kbd><caption></kbd> with 'Order Summary'<br>2. A <kbd><thead></kbd> with Item and Price headers<br>3. A <kbd><tbody></kbd> with 2 items<br>4. A <kbd><tfoot></kbd> with a Total row",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<table>\n <caption>Order Summary</caption>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>$25.00</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>$35.00</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Total</td>\n <td>$60.00</td>\n </tr>\n </tfoot>\n</table>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "table",
|
||||||
|
"message": "Add a <kbd><table></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "caption",
|
||||||
|
"message": "Add a <kbd><caption></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "thead",
|
||||||
|
"message": "Add a <kbd><thead></kbd> section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tbody",
|
||||||
|
"message": "Add a <kbd><tbody></kbd> section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "tfoot",
|
||||||
|
"message": "Add a <kbd><tfoot></kbd> section for the total"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "tbody tr", "min": 2 },
|
||||||
|
"message": "Add at least 2 item rows in tbody"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
82
lessons/uk/31-html-marquee.json
Normal file
82
lessons/uk/31-html-marquee.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-marquee",
|
||||||
|
"title": "HTML Marquee",
|
||||||
|
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "marquee-basic",
|
||||||
|
"title": "Scrolling Text",
|
||||||
|
"description": "The <kbd><marquee></kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!",
|
||||||
|
"task": "Create a simple marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Put some text inside like 'Welcome to my website!'",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee>Welcome to my website!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marquee-direction",
|
||||||
|
"title": "Direction & Behavior",
|
||||||
|
"description": "Control the marquee with attributes:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (default), slide (stops at edge), alternate (bounces)<br>• <kbd>scrollamount</kbd>: speed (default is 6)",
|
||||||
|
"task": "Create a bouncing marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee behavior=\"alternate\">Bounce! Bounce! Bounce!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
|
||||||
|
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marquee-retro",
|
||||||
|
"title": "Retro News Ticker",
|
||||||
|
"description": "Combine multiple marquee attributes for a classic news ticker effect. You can even put multiple elements inside!<br><br>Remember: This is deprecated HTML. Modern sites use CSS animations, but marquee is great for understanding web history.",
|
||||||
|
"task": "Create a news ticker:<br>1. A <kbd><marquee></kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<marquee direction=\"left\" scrollamount=\"5\">BREAKING NEWS: Marquee element still works in browsers!</marquee>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "marquee",
|
||||||
|
"message": "Add a <kbd><marquee></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "direction", "value": "left" },
|
||||||
|
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
|
||||||
|
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/uk/32-html-svg.json
Normal file
102
lessons/uk/32-html-svg.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "html-svg",
|
||||||
|
"title": "HTML SVG",
|
||||||
|
"description": "Draw scalable vector graphics directly in HTML",
|
||||||
|
"mode": "html",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "svg-circle",
|
||||||
|
"title": "Drawing Circles",
|
||||||
|
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd><svg></kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd><circle></kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.",
|
||||||
|
"task": "Create an SVG with a circle:<br>1. An <kbd><svg></kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd><circle></kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "circle",
|
||||||
|
"message": "Add a <kbd><circle></kbd> element inside the SVG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "circle", "attr": "cx", "value": "100" },
|
||||||
|
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "attribute_value",
|
||||||
|
"value": { "selector": "circle", "attr": "cy", "value": "100" },
|
||||||
|
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "svg-rect-line",
|
||||||
|
"title": "Rectangles & Lines",
|
||||||
|
"description": "Draw rectangles with <kbd><rect></kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd><line></kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!",
|
||||||
|
"task": "Create an SVG with:<br>1. An <kbd><svg></kbd> (200x150)<br>2. A <kbd><rect></kbd> at position (20,20) with size 80x60<br>3. A <kbd><line></kbd> from (120,30) to (180,90) with a stroke color",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "rect",
|
||||||
|
"message": "Add a <kbd><rect></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "line",
|
||||||
|
"message": "Add a <kbd><line></kbd> element"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "svg-shapes",
|
||||||
|
"title": "Multiple Shapes",
|
||||||
|
"description": "Combine shapes to create simple graphics! Add <kbd>stroke</kbd> for outlines and <kbd>stroke-width</kbd> for thickness.<br><br>Use <kbd>fill=\"none\"</kbd> for hollow shapes. Shapes stack in order - later elements appear on top.",
|
||||||
|
"task": "Create a simple face:<br>1. An <kbd><svg></kbd> (200x200)<br>2. A large <kbd><circle></kbd> for the face<br>3. Two small <kbd><circle></kbd> elements for eyes<br>4. A <kbd><line></kbd> for the smile",
|
||||||
|
"previewHTML": "",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "svg",
|
||||||
|
"message": "Add an <kbd><svg></kbd> element"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_count",
|
||||||
|
"value": { "selector": "circle", "min": 3 },
|
||||||
|
"message": "Add at least 3 circles (1 face + 2 eyes)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element_exists",
|
||||||
|
"value": "line",
|
||||||
|
"message": "Add a <kbd><line></kbd> for the smile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
173
lessons/uk/flexbox.json
Normal file
173
lessons/uk/flexbox.json
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||||
|
"id": "flexbox",
|
||||||
|
"title": "CSS Flexbox",
|
||||||
|
"description": "Master the flexible box layout model for modern responsive designs",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "flexbox-1",
|
||||||
|
"title": "Container",
|
||||||
|
"description": "Learn how to create a flex container and understand the main and cross axes.",
|
||||||
|
"task": "Add <kbd>display: flex</kbd> to <kbd>.wrap</kbd> to create a flexbox layout.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "display: flex;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "display",
|
||||||
|
"expected": "flex"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>display: flex</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-2",
|
||||||
|
"title": "Direction & Wrap",
|
||||||
|
"description": "Control the direction and wrapping of flex items within a container.",
|
||||||
|
"task": "Add <kbd>flex-direction: column</kbd> and <kbd>flex-wrap: wrap</kbd> to <kbd>.wrap</kbd>.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div><div class='box'>4</div><div class='box'>5</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; height: 16rem; display: flex; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "flex-direction: column;\n flex-wrap: wrap;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex-direction",
|
||||||
|
"expected": "column"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex-direction: column</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex-wrap",
|
||||||
|
"expected": "wrap"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex-wrap: wrap</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-3",
|
||||||
|
"title": "Justify Content",
|
||||||
|
"description": "Learn how to align flex items along the main axis of the flex container.",
|
||||||
|
"task": "Add <kbd>justify-content: space-between</kbd> to <kbd>.wrap</kbd> to distribute the boxes evenly.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "justify-content: space-between;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "justify-content",
|
||||||
|
"expected": "space-between"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>justify-content: space-between</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-4",
|
||||||
|
"title": "Align Items",
|
||||||
|
"description": "Control how flex items are aligned along the cross axis of the flex container.",
|
||||||
|
"task": "Add <kbd>align-items: center</kbd> to <kbd>.wrap</kbd> to vertically center the boxes.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box tall'>1</div><div class='box'>2</div><div class='box short'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; display: flex; justify-content: center; } .tall { height: 6rem; } .short { height: 3rem; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 10rem; }",
|
||||||
|
"codePrefix": ".wrap {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "align-items: center;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "align-items",
|
||||||
|
"expected": "center"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>align-items: center</kbd>",
|
||||||
|
"options": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-5",
|
||||||
|
"title": "Flex Grow",
|
||||||
|
"description": "The <kbd>flex</kbd> property controls how much an item grows relative to others.",
|
||||||
|
"task": "Add <kbd>flex: 2</kbd> to <kbd>.box2</kbd> to make it grow twice as wide.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box box1'>1</div><div class='box box2'>2</div><div class='box box3'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; display: flex; align-items: center; justify-content: center; } .box1 { background: coral; flex: 1; } .box2 { background: mediumseagreen; } .box3 { background: gold; flex: 1; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
|
||||||
|
"codePrefix": ".box2 {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "flex: 2;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "flex",
|
||||||
|
"expected": "2"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>flex: 2</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flexbox-6",
|
||||||
|
"title": "Align Self",
|
||||||
|
"description": "Use <kbd>align-self</kbd> to override alignment for a single flex item.",
|
||||||
|
"task": "Add <kbd>align-self: flex-start</kbd> to <kbd>.middle</kbd> to move it to the top.",
|
||||||
|
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box middle'>2</div><div class='box'>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; } .middle { background: mediumseagreen; }",
|
||||||
|
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 12rem; align-items: center; }",
|
||||||
|
"codePrefix": ".middle {\n ",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "\n}",
|
||||||
|
"solution": "align-self: flex-start;",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": {
|
||||||
|
"property": "align-self",
|
||||||
|
"expected": "flex-start"
|
||||||
|
},
|
||||||
|
"message": "Set <kbd>align-self: flex-start</kbd>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { LessonEngine } from "./impl/LessonEngine.js";
|
|||||||
import { CodeEditor } from "./impl/CodeEditor.js";
|
import { CodeEditor } from "./impl/CodeEditor.js";
|
||||||
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
|
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
|
||||||
import { loadModules } from "./config/lessons.js";
|
import { loadModules } from "./config/lessons.js";
|
||||||
import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js";
|
import { initI18n, t, getLanguage, setLanguage, getNextLanguage, applyTranslations } from "./i18n.js";
|
||||||
|
|
||||||
// Simplified state - LessonEngine now manages lesson state and progress
|
// Simplified state - LessonEngine now manages lesson state and progress
|
||||||
const state = {
|
const state = {
|
||||||
@@ -119,7 +119,7 @@ function toggleExpectedResult() {
|
|||||||
|
|
||||||
function toggleLanguage() {
|
function toggleLanguage() {
|
||||||
const currentLang = getLanguage();
|
const currentLang = getLanguage();
|
||||||
const newLang = currentLang === "en" ? "de" : "en";
|
const newLang = getNextLanguage(currentLang);
|
||||||
|
|
||||||
// Add transition class before any updates
|
// Add transition class before any updates
|
||||||
elements.editorSection?.classList.add("transitioning");
|
elements.editorSection?.classList.add("transitioning");
|
||||||
|
|||||||
@@ -37,6 +37,74 @@ import htmlMarqueeDE from "../../lessons/de/31-html-marquee.json";
|
|||||||
import htmlSvgDE from "../../lessons/de/32-html-svg.json";
|
import htmlSvgDE from "../../lessons/de/32-html-svg.json";
|
||||||
import flexboxDE from "../../lessons/de/flexbox.json";
|
import flexboxDE from "../../lessons/de/flexbox.json";
|
||||||
|
|
||||||
|
// Polish lesson imports
|
||||||
|
import welcomePL from "../../lessons/pl/00-welcome.json";
|
||||||
|
import basicSelectorsPL from "../../lessons/pl/00-basic-selectors.json";
|
||||||
|
import boxModelPL from "../../lessons/pl/01-box-model.json";
|
||||||
|
import unitsVariablesPL from "../../lessons/pl/05-units-variables.json";
|
||||||
|
import transitionsAnimationsPL from "../../lessons/pl/06-transitions-animations.json";
|
||||||
|
import responsivePL from "../../lessons/pl/08-responsive.json";
|
||||||
|
import htmlElementsPL from "../../lessons/pl/20-html-elements.json";
|
||||||
|
import htmlFormsBasicPL from "../../lessons/pl/21-html-forms-basic.json";
|
||||||
|
import htmlFormsValidationPL from "../../lessons/pl/22-html-forms-validation.json";
|
||||||
|
import htmlDetailsSummaryPL from "../../lessons/pl/23-html-details-summary.json";
|
||||||
|
import htmlProgressMeterPL from "../../lessons/pl/24-html-progress-meter.json";
|
||||||
|
import htmlTablesPL from "../../lessons/pl/30-html-tables.json";
|
||||||
|
import htmlMarqueePL from "../../lessons/pl/31-html-marquee.json";
|
||||||
|
import htmlSvgPL from "../../lessons/pl/32-html-svg.json";
|
||||||
|
import flexboxPL from "../../lessons/pl/flexbox.json";
|
||||||
|
|
||||||
|
// Spanish lesson imports
|
||||||
|
import welcomeES from "../../lessons/es/00-welcome.json";
|
||||||
|
import basicSelectorsES from "../../lessons/es/00-basic-selectors.json";
|
||||||
|
import boxModelES from "../../lessons/es/01-box-model.json";
|
||||||
|
import unitsVariablesES from "../../lessons/es/05-units-variables.json";
|
||||||
|
import transitionsAnimationsES from "../../lessons/es/06-transitions-animations.json";
|
||||||
|
import responsiveES from "../../lessons/es/08-responsive.json";
|
||||||
|
import htmlElementsES from "../../lessons/es/20-html-elements.json";
|
||||||
|
import htmlFormsBasicES from "../../lessons/es/21-html-forms-basic.json";
|
||||||
|
import htmlFormsValidationES from "../../lessons/es/22-html-forms-validation.json";
|
||||||
|
import htmlDetailsSummaryES from "../../lessons/es/23-html-details-summary.json";
|
||||||
|
import htmlProgressMeterES from "../../lessons/es/24-html-progress-meter.json";
|
||||||
|
import htmlTablesES from "../../lessons/es/30-html-tables.json";
|
||||||
|
import htmlMarqueeES from "../../lessons/es/31-html-marquee.json";
|
||||||
|
import htmlSvgES from "../../lessons/es/32-html-svg.json";
|
||||||
|
import flexboxES from "../../lessons/es/flexbox.json";
|
||||||
|
|
||||||
|
// Arabic lesson imports
|
||||||
|
import welcomeAR from "../../lessons/ar/00-welcome.json";
|
||||||
|
import basicSelectorsAR from "../../lessons/ar/00-basic-selectors.json";
|
||||||
|
import boxModelAR from "../../lessons/ar/01-box-model.json";
|
||||||
|
import unitsVariablesAR from "../../lessons/ar/05-units-variables.json";
|
||||||
|
import transitionsAnimationsAR from "../../lessons/ar/06-transitions-animations.json";
|
||||||
|
import responsiveAR from "../../lessons/ar/08-responsive.json";
|
||||||
|
import htmlElementsAR from "../../lessons/ar/20-html-elements.json";
|
||||||
|
import htmlFormsBasicAR from "../../lessons/ar/21-html-forms-basic.json";
|
||||||
|
import htmlFormsValidationAR from "../../lessons/ar/22-html-forms-validation.json";
|
||||||
|
import htmlDetailsSummaryAR from "../../lessons/ar/23-html-details-summary.json";
|
||||||
|
import htmlProgressMeterAR from "../../lessons/ar/24-html-progress-meter.json";
|
||||||
|
import htmlTablesAR from "../../lessons/ar/30-html-tables.json";
|
||||||
|
import htmlMarqueeAR from "../../lessons/ar/31-html-marquee.json";
|
||||||
|
import htmlSvgAR from "../../lessons/ar/32-html-svg.json";
|
||||||
|
import flexboxAR from "../../lessons/ar/flexbox.json";
|
||||||
|
|
||||||
|
// Ukrainian lesson imports
|
||||||
|
import welcomeUK from "../../lessons/uk/00-welcome.json";
|
||||||
|
import basicSelectorsUK from "../../lessons/uk/00-basic-selectors.json";
|
||||||
|
import boxModelUK from "../../lessons/uk/01-box-model.json";
|
||||||
|
import unitsVariablesUK from "../../lessons/uk/05-units-variables.json";
|
||||||
|
import transitionsAnimationsUK from "../../lessons/uk/06-transitions-animations.json";
|
||||||
|
import responsiveUK from "../../lessons/uk/08-responsive.json";
|
||||||
|
import htmlElementsUK from "../../lessons/uk/20-html-elements.json";
|
||||||
|
import htmlFormsBasicUK from "../../lessons/uk/21-html-forms-basic.json";
|
||||||
|
import htmlFormsValidationUK from "../../lessons/uk/22-html-forms-validation.json";
|
||||||
|
import htmlDetailsSummaryUK from "../../lessons/uk/23-html-details-summary.json";
|
||||||
|
import htmlProgressMeterUK from "../../lessons/uk/24-html-progress-meter.json";
|
||||||
|
import htmlTablesUK from "../../lessons/uk/30-html-tables.json";
|
||||||
|
import htmlMarqueeUK from "../../lessons/uk/31-html-marquee.json";
|
||||||
|
import htmlSvgUK from "../../lessons/uk/32-html-svg.json";
|
||||||
|
import flexboxUK from "../../lessons/uk/flexbox.json";
|
||||||
|
|
||||||
// English module store - ordered by learning path
|
// English module store - ordered by learning path
|
||||||
const moduleStoreEN = [
|
const moduleStoreEN = [
|
||||||
// Welcome
|
// Welcome
|
||||||
@@ -89,18 +157,104 @@ const moduleStoreDE = [
|
|||||||
transitionsAnimationsDE
|
transitionsAnimationsDE
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Polish module store - ordered by learning path
|
||||||
|
const moduleStorePL = [
|
||||||
|
welcomePL,
|
||||||
|
htmlElementsPL,
|
||||||
|
htmlFormsBasicPL,
|
||||||
|
htmlFormsValidationPL,
|
||||||
|
htmlDetailsSummaryPL,
|
||||||
|
htmlProgressMeterPL,
|
||||||
|
htmlTablesPL,
|
||||||
|
htmlSvgPL,
|
||||||
|
htmlMarqueePL,
|
||||||
|
basicSelectorsPL,
|
||||||
|
boxModelPL,
|
||||||
|
unitsVariablesPL,
|
||||||
|
flexboxPL,
|
||||||
|
responsivePL,
|
||||||
|
transitionsAnimationsPL
|
||||||
|
];
|
||||||
|
|
||||||
|
// Spanish module store - ordered by learning path
|
||||||
|
const moduleStoreES = [
|
||||||
|
welcomeES,
|
||||||
|
htmlElementsES,
|
||||||
|
htmlFormsBasicES,
|
||||||
|
htmlFormsValidationES,
|
||||||
|
htmlDetailsSummaryES,
|
||||||
|
htmlProgressMeterES,
|
||||||
|
htmlTablesES,
|
||||||
|
htmlSvgES,
|
||||||
|
htmlMarqueeES,
|
||||||
|
basicSelectorsES,
|
||||||
|
boxModelES,
|
||||||
|
unitsVariablesES,
|
||||||
|
flexboxES,
|
||||||
|
responsiveES,
|
||||||
|
transitionsAnimationsES
|
||||||
|
];
|
||||||
|
|
||||||
|
// Arabic module store - ordered by learning path
|
||||||
|
const moduleStoreAR = [
|
||||||
|
welcomeAR,
|
||||||
|
htmlElementsAR,
|
||||||
|
htmlFormsBasicAR,
|
||||||
|
htmlFormsValidationAR,
|
||||||
|
htmlDetailsSummaryAR,
|
||||||
|
htmlProgressMeterAR,
|
||||||
|
htmlTablesAR,
|
||||||
|
htmlSvgAR,
|
||||||
|
htmlMarqueeAR,
|
||||||
|
basicSelectorsAR,
|
||||||
|
boxModelAR,
|
||||||
|
unitsVariablesAR,
|
||||||
|
flexboxAR,
|
||||||
|
responsiveAR,
|
||||||
|
transitionsAnimationsAR
|
||||||
|
];
|
||||||
|
|
||||||
|
// Ukrainian module store - ordered by learning path
|
||||||
|
const moduleStoreUK = [
|
||||||
|
welcomeUK,
|
||||||
|
htmlElementsUK,
|
||||||
|
htmlFormsBasicUK,
|
||||||
|
htmlFormsValidationUK,
|
||||||
|
htmlDetailsSummaryUK,
|
||||||
|
htmlProgressMeterUK,
|
||||||
|
htmlTablesUK,
|
||||||
|
htmlSvgUK,
|
||||||
|
htmlMarqueeUK,
|
||||||
|
basicSelectorsUK,
|
||||||
|
boxModelUK,
|
||||||
|
unitsVariablesUK,
|
||||||
|
flexboxUK,
|
||||||
|
responsiveUK,
|
||||||
|
transitionsAnimationsUK
|
||||||
|
];
|
||||||
|
|
||||||
|
// Map of language codes to module stores
|
||||||
|
const moduleStores = {
|
||||||
|
en: moduleStoreEN,
|
||||||
|
de: moduleStoreDE,
|
||||||
|
pl: moduleStorePL,
|
||||||
|
es: moduleStoreES,
|
||||||
|
ar: moduleStoreAR,
|
||||||
|
uk: moduleStoreUK
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all available modules for a given language
|
* Load all available modules for a given language
|
||||||
* @param {string} language - Language code ('en' or 'de')
|
* @param {string} language - Language code ('en', 'de', 'pl', 'es', 'ar', 'uk')
|
||||||
* @returns {Array} Array of modules
|
* @returns {Array} Array of modules
|
||||||
*/
|
*/
|
||||||
export function loadModules(language = "en") {
|
export function loadModules(language = "en") {
|
||||||
const store = language === "de" ? moduleStoreDE : moduleStoreEN;
|
const store = moduleStores[language] || moduleStoreEN;
|
||||||
return store.map((module) => ({
|
return store.map((module) => ({
|
||||||
...module,
|
...module,
|
||||||
lessons: module.lessons.map((lesson) => ({
|
lessons: module.lessons.map((lesson) => ({
|
||||||
...lesson,
|
...lesson,
|
||||||
mode: module.mode || "css"
|
mode: lesson.mode || module.mode || "css"
|
||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
427
src/i18n.js
427
src/i18n.js
@@ -11,7 +11,7 @@ const translations = {
|
|||||||
// Header
|
// Header
|
||||||
menuOpen: "Open menu",
|
menuOpen: "Open menu",
|
||||||
langSwitch: "DE",
|
langSwitch: "DE",
|
||||||
langSwitchLabel: "Sprache wechseln: Deutsch",
|
langSwitchLabel: "Switch language: Deutsch",
|
||||||
help: "Help",
|
help: "Help",
|
||||||
|
|
||||||
// Instructions
|
// Instructions
|
||||||
@@ -109,8 +109,8 @@ const translations = {
|
|||||||
|
|
||||||
// Header
|
// Header
|
||||||
menuOpen: "Menü öffnen",
|
menuOpen: "Menü öffnen",
|
||||||
langSwitch: "EN",
|
langSwitch: "PL",
|
||||||
langSwitchLabel: "Switch language: English",
|
langSwitchLabel: "Zmień język: Polski",
|
||||||
help: "Hilfe",
|
help: "Hilfe",
|
||||||
|
|
||||||
// Instructions
|
// Instructions
|
||||||
@@ -199,11 +199,432 @@ const translations = {
|
|||||||
tailwindPlaceholder: "Tailwind-Klassen eingeben (z.B. bg-blue-500 text-white p-4)",
|
tailwindPlaceholder: "Tailwind-Klassen eingeben (z.B. bg-blue-500 text-white p-4)",
|
||||||
lessonFallback: "Lektion {index}",
|
lessonFallback: "Lektion {index}",
|
||||||
untitledLesson: "Unbenannte Lektion"
|
untitledLesson: "Unbenannte Lektion"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Polish
|
||||||
|
pl: {
|
||||||
|
// Page
|
||||||
|
pageTitle: "Code Crispies - Nauka HTML i CSS interaktywnie",
|
||||||
|
skipLink: "Przejdź do głównej treści",
|
||||||
|
|
||||||
|
// Header
|
||||||
|
menuOpen: "Otwórz menu",
|
||||||
|
langSwitch: "ES",
|
||||||
|
langSwitchLabel: "Cambiar idioma: Español",
|
||||||
|
help: "Pomoc",
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
loading: "Ładowanie...",
|
||||||
|
selectLesson: "Wybierz lekcję, aby rozpocząć.",
|
||||||
|
editorLabel: "Edytor CSS",
|
||||||
|
undoTitle: "Cofnij (Ctrl+Z)",
|
||||||
|
redoTitle: "Ponów (Ctrl+Shift+Z)",
|
||||||
|
resetCodeTitle: "Przywróć kod początkowy",
|
||||||
|
run: "Uruchom",
|
||||||
|
rerun: "Uruchom ponownie",
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
yourOutput: "Twój wynik",
|
||||||
|
showExpected: "Pokaż oczekiwane",
|
||||||
|
hideExpected: "Ukryj oczekiwane",
|
||||||
|
previous: "Poprzednia",
|
||||||
|
next: "Następna",
|
||||||
|
levelIndicator: "Lekcja {current} z {total}",
|
||||||
|
lessonLabel: "Lekcja",
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
menu: "Menu",
|
||||||
|
closeMenu: "Zamknij menu",
|
||||||
|
progress: "Postęp",
|
||||||
|
progressText: "{percent}% ukończone ({completed}/{total})",
|
||||||
|
lessons: "Lekcje",
|
||||||
|
settings: "Ustawienia",
|
||||||
|
showHints: "Pokaż podpowiedzi",
|
||||||
|
resetAllProgress: "Resetuj cały postęp",
|
||||||
|
openSource: "Open Source:",
|
||||||
|
by: "przez",
|
||||||
|
|
||||||
|
// Help dialog
|
||||||
|
helpTitle: "Pomoc",
|
||||||
|
aboutTitle: "O Code Crispies",
|
||||||
|
aboutText: "Code Crispies to darmowa platforma open-source do nauki tworzenia stron internetowych poprzez praktyczne ćwiczenia. Nie wymaga konta - po prostu zacznij kodować!",
|
||||||
|
learningModesTitle: "Tryby nauki",
|
||||||
|
modeCss: "<strong>CSS</strong> - Pisz reguły CSS do stylizowania elementów",
|
||||||
|
modeTailwind: "<strong>Tailwind</strong> - Stosuj klasy utility bezpośrednio w HTML",
|
||||||
|
modeHtml: "<strong>HTML</strong> - Ćwicz semantyczne znaczniki i natywne elementy",
|
||||||
|
gettingStartedTitle: "Pierwsze kroki",
|
||||||
|
gettingStartedText: "Otwórz menu (☰), aby przeglądać moduły lekcji. Każdy moduł obejmuje konkretny temat z progresywnymi ćwiczeniami.",
|
||||||
|
completingLessonsTitle: "Ukończanie lekcji",
|
||||||
|
completingStep1: "Przeczytaj instrukcje zadania po lewej stronie",
|
||||||
|
completingStep2: "Napisz swój kod w edytorze",
|
||||||
|
completingStep3: "Obserwuj podgląd na żywo podczas pisania",
|
||||||
|
completingStep4: "Postępuj zgodnie z podpowiedziami, aby naprawić problemy",
|
||||||
|
completingStep5: "Kliknij <strong>Następna</strong> po ukończeniu",
|
||||||
|
editorToolsTitle: "Narzędzia edytora",
|
||||||
|
editorToolUndo: "<strong>↶ Cofnij</strong> / <strong>↷ Ponów</strong> - Nawiguj historię edycji",
|
||||||
|
editorToolReset: "<strong>⟲ Resetuj</strong> - Przywróć początkowy kod",
|
||||||
|
editorToolExpected: "<strong>Pokaż oczekiwane</strong> - Przełącz nakładkę wyniku docelowego",
|
||||||
|
keyboardShortcutsTitle: "Skróty klawiszowe",
|
||||||
|
shortcutRun: "<kbd>Ctrl+Enter</kbd> - Natychmiastowa walidacja",
|
||||||
|
shortcutUndo: "<kbd>Ctrl+Z</kbd> - Cofnij",
|
||||||
|
shortcutRedo: "<kbd>Ctrl+Shift+Z</kbd> - Ponów",
|
||||||
|
emmetTitle: "Skróty Emmet (tryb HTML)",
|
||||||
|
emmetText: "Wpisz skróty i naciśnij <kbd>Tab</kbd>, aby rozwinąć:",
|
||||||
|
emmetClass: "<kbd>div.box</kbd> → div z klasą",
|
||||||
|
emmetChildren: "<kbd>ul>li*3</kbd> → ul z 3 elementami li",
|
||||||
|
emmetNested: "<kbd>form>input+button</kbd> → zagnieżdżona struktura",
|
||||||
|
emmetContent: "<kbd>p{Cześć}</kbd> → p z tekstem",
|
||||||
|
|
||||||
|
// More Projects
|
||||||
|
moreProjectsTitle: "Więcej projektów",
|
||||||
|
htmlOverJsDesc: " - Naucz się wykorzystywać natywne elementy HTML zamiast niestandardowych rozwiązań JavaScript",
|
||||||
|
mandalaDesc: " - Interaktywna wizualizacja technologii JavaScript uporządkowanych według złożoności",
|
||||||
|
|
||||||
|
// Contact
|
||||||
|
contactTitle: "Kontakt i linki",
|
||||||
|
contactText: "Code Crispies jest rozwijany przez <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a>",
|
||||||
|
|
||||||
|
// Reset dialog
|
||||||
|
resetDialogTitle: "Resetuj postęp",
|
||||||
|
resetDialogText: "Czy na pewno chcesz zresetować cały postęp? Tej operacji nie można cofnąć.",
|
||||||
|
cancel: "Anuluj",
|
||||||
|
resetAll: "Resetuj wszystko",
|
||||||
|
|
||||||
|
// Dynamic content
|
||||||
|
loadingFallbackText: "Nie można załadować lekcji. Wybierz jedną z menu lub sprawdź pomoc.",
|
||||||
|
completed: "Ukończono",
|
||||||
|
successMessage: "CRISPY! ٩(◕‿◕)۶ Twój kod działa poprawnie.",
|
||||||
|
keepTrying: "Próbuj dalej!",
|
||||||
|
failedToLoad: "Nie udało się załadować modułów. Odśwież stronę.",
|
||||||
|
tailwindPlaceholder: "Wprowadź klasy Tailwind (np. bg-blue-500 text-white p-4)",
|
||||||
|
lessonFallback: "Lekcja {index}",
|
||||||
|
untitledLesson: "Lekcja bez tytułu"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Spanish
|
||||||
|
es: {
|
||||||
|
// Page
|
||||||
|
pageTitle: "Code Crispies - Aprende HTML y CSS de forma interactiva",
|
||||||
|
skipLink: "Saltar al contenido principal",
|
||||||
|
|
||||||
|
// Header
|
||||||
|
menuOpen: "Abrir menú",
|
||||||
|
langSwitch: "AR",
|
||||||
|
langSwitchLabel: "تغيير اللغة: العربية",
|
||||||
|
help: "Ayuda",
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
loading: "Cargando...",
|
||||||
|
selectLesson: "Selecciona una lección para comenzar.",
|
||||||
|
editorLabel: "Editor CSS",
|
||||||
|
undoTitle: "Deshacer (Ctrl+Z)",
|
||||||
|
redoTitle: "Rehacer (Ctrl+Shift+Z)",
|
||||||
|
resetCodeTitle: "Restaurar código inicial",
|
||||||
|
run: "Ejecutar",
|
||||||
|
rerun: "Volver a ejecutar",
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
yourOutput: "Tu resultado",
|
||||||
|
showExpected: "Mostrar esperado",
|
||||||
|
hideExpected: "Ocultar esperado",
|
||||||
|
previous: "Anterior",
|
||||||
|
next: "Siguiente",
|
||||||
|
levelIndicator: "Lección {current} de {total}",
|
||||||
|
lessonLabel: "Lección",
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
menu: "Menú",
|
||||||
|
closeMenu: "Cerrar menú",
|
||||||
|
progress: "Progreso",
|
||||||
|
progressText: "{percent}% completado ({completed}/{total})",
|
||||||
|
lessons: "Lecciones",
|
||||||
|
settings: "Configuración",
|
||||||
|
showHints: "Mostrar pistas",
|
||||||
|
resetAllProgress: "Reiniciar todo el progreso",
|
||||||
|
openSource: "Código abierto:",
|
||||||
|
by: "por",
|
||||||
|
|
||||||
|
// Help dialog
|
||||||
|
helpTitle: "Ayuda",
|
||||||
|
aboutTitle: "Acerca de Code Crispies",
|
||||||
|
aboutText: "Code Crispies es una plataforma gratuita de código abierto para aprender desarrollo web a través de ejercicios prácticos. No se requiere cuenta, ¡solo empieza a programar!",
|
||||||
|
learningModesTitle: "Modos de aprendizaje",
|
||||||
|
modeCss: "<strong>CSS</strong> - Escribe reglas CSS para estilizar elementos",
|
||||||
|
modeTailwind: "<strong>Tailwind</strong> - Aplica clases de utilidad directamente en HTML",
|
||||||
|
modeHtml: "<strong>HTML</strong> - Practica marcado semántico y elementos nativos",
|
||||||
|
gettingStartedTitle: "Primeros pasos",
|
||||||
|
gettingStartedText: "Abre el menú (☰) para explorar los módulos de lecciones. Cada módulo cubre un tema específico con ejercicios progresivos.",
|
||||||
|
completingLessonsTitle: "Completar lecciones",
|
||||||
|
completingStep1: "Lee las instrucciones de la tarea a la izquierda",
|
||||||
|
completingStep2: "Escribe tu código en el editor",
|
||||||
|
completingStep3: "Observa la vista previa en vivo mientras escribes",
|
||||||
|
completingStep4: "Sigue las pistas para corregir problemas",
|
||||||
|
completingStep5: "Haz clic en <strong>Siguiente</strong> cuando termines",
|
||||||
|
editorToolsTitle: "Herramientas del editor",
|
||||||
|
editorToolUndo: "<strong>↶ Deshacer</strong> / <strong>↷ Rehacer</strong> - Navegar historial de edición",
|
||||||
|
editorToolReset: "<strong>⟲ Reiniciar</strong> - Restaurar código inicial",
|
||||||
|
editorToolExpected: "<strong>Mostrar esperado</strong> - Alternar superposición del resultado objetivo",
|
||||||
|
keyboardShortcutsTitle: "Atajos de teclado",
|
||||||
|
shortcutRun: "<kbd>Ctrl+Enter</kbd> - Validar inmediatamente",
|
||||||
|
shortcutUndo: "<kbd>Ctrl+Z</kbd> - Deshacer",
|
||||||
|
shortcutRedo: "<kbd>Ctrl+Shift+Z</kbd> - Rehacer",
|
||||||
|
emmetTitle: "Atajos Emmet (modo HTML)",
|
||||||
|
emmetText: "Escribe abreviaturas y presiona <kbd>Tab</kbd> para expandir:",
|
||||||
|
emmetClass: "<kbd>div.box</kbd> → div con clase",
|
||||||
|
emmetChildren: "<kbd>ul>li*3</kbd> → ul con 3 hijos li",
|
||||||
|
emmetNested: "<kbd>form>input+button</kbd> → estructura anidada",
|
||||||
|
emmetContent: "<kbd>p{Hola}</kbd> → p con contenido de texto",
|
||||||
|
|
||||||
|
// More Projects
|
||||||
|
moreProjectsTitle: "Más proyectos",
|
||||||
|
htmlOverJsDesc: " - Aprende a aprovechar elementos HTML nativos en lugar de soluciones JavaScript personalizadas",
|
||||||
|
mandalaDesc: " - Visualización interactiva de tecnologías JavaScript organizadas por complejidad",
|
||||||
|
|
||||||
|
// Contact
|
||||||
|
contactTitle: "Contacto y enlaces",
|
||||||
|
contactText: "Code Crispies es desarrollado por <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a>",
|
||||||
|
|
||||||
|
// Reset dialog
|
||||||
|
resetDialogTitle: "Reiniciar progreso",
|
||||||
|
resetDialogText: "¿Estás seguro de que quieres reiniciar todo tu progreso? Esta acción no se puede deshacer.",
|
||||||
|
cancel: "Cancelar",
|
||||||
|
resetAll: "Reiniciar todo",
|
||||||
|
|
||||||
|
// Dynamic content
|
||||||
|
loadingFallbackText: "No se pudo cargar la lección. Selecciona una del menú o consulta la ayuda.",
|
||||||
|
completed: "Completado",
|
||||||
|
successMessage: "¡CRISPY! ٩(◕‿◕)۶ Tu código funciona correctamente.",
|
||||||
|
keepTrying: "¡Sigue intentando!",
|
||||||
|
failedToLoad: "No se pudieron cargar los módulos. Actualiza la página.",
|
||||||
|
tailwindPlaceholder: "Ingresa clases de Tailwind (ej. bg-blue-500 text-white p-4)",
|
||||||
|
lessonFallback: "Lección {index}",
|
||||||
|
untitledLesson: "Lección sin título"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Arabic
|
||||||
|
ar: {
|
||||||
|
// Page
|
||||||
|
pageTitle: "Code Crispies - تعلم HTML و CSS بشكل تفاعلي",
|
||||||
|
skipLink: "انتقل إلى المحتوى الرئيسي",
|
||||||
|
|
||||||
|
// Header
|
||||||
|
menuOpen: "افتح القائمة",
|
||||||
|
langSwitch: "UK",
|
||||||
|
langSwitchLabel: "Змінити мову: Українська",
|
||||||
|
help: "مساعدة",
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
loading: "جاري التحميل...",
|
||||||
|
selectLesson: "اختر درسًا للبدء.",
|
||||||
|
editorLabel: "محرر CSS",
|
||||||
|
undoTitle: "تراجع (Ctrl+Z)",
|
||||||
|
redoTitle: "إعادة (Ctrl+Shift+Z)",
|
||||||
|
resetCodeTitle: "استعادة الكود الأولي",
|
||||||
|
run: "تشغيل",
|
||||||
|
rerun: "إعادة التشغيل",
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
yourOutput: "نتيجتك",
|
||||||
|
showExpected: "إظهار المتوقع",
|
||||||
|
hideExpected: "إخفاء المتوقع",
|
||||||
|
previous: "السابق",
|
||||||
|
next: "التالي",
|
||||||
|
levelIndicator: "الدرس {current} من {total}",
|
||||||
|
lessonLabel: "درس",
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
menu: "القائمة",
|
||||||
|
closeMenu: "إغلاق القائمة",
|
||||||
|
progress: "التقدم",
|
||||||
|
progressText: "{percent}% مكتمل ({completed}/{total})",
|
||||||
|
lessons: "الدروس",
|
||||||
|
settings: "الإعدادات",
|
||||||
|
showHints: "إظهار التلميحات",
|
||||||
|
resetAllProgress: "إعادة تعيين كل التقدم",
|
||||||
|
openSource: "مفتوح المصدر:",
|
||||||
|
by: "بواسطة",
|
||||||
|
|
||||||
|
// Help dialog
|
||||||
|
helpTitle: "مساعدة",
|
||||||
|
aboutTitle: "عن Code Crispies",
|
||||||
|
aboutText: "Code Crispies هي منصة مجانية مفتوحة المصدر لتعلم تطوير الويب من خلال تمارين عملية. لا يلزم حساب - فقط ابدأ البرمجة!",
|
||||||
|
learningModesTitle: "أوضاع التعلم",
|
||||||
|
modeCss: "<strong>CSS</strong> - اكتب قواعد CSS لتنسيق العناصر",
|
||||||
|
modeTailwind: "<strong>Tailwind</strong> - طبق فئات الأدوات مباشرة في HTML",
|
||||||
|
modeHtml: "<strong>HTML</strong> - تدرب على الترميز الدلالي والعناصر الأصلية",
|
||||||
|
gettingStartedTitle: "البداية",
|
||||||
|
gettingStartedText: "افتح القائمة (☰) لتصفح وحدات الدروس. كل وحدة تغطي موضوعًا محددًا مع تمارين تدريجية.",
|
||||||
|
completingLessonsTitle: "إكمال الدروس",
|
||||||
|
completingStep1: "اقرأ تعليمات المهمة على اليسار",
|
||||||
|
completingStep2: "اكتب الكود في المحرر",
|
||||||
|
completingStep3: "شاهد المعاينة المباشرة أثناء الكتابة",
|
||||||
|
completingStep4: "اتبع التلميحات لإصلاح أي مشاكل",
|
||||||
|
completingStep5: "انقر على <strong>التالي</strong> عند الانتهاء",
|
||||||
|
editorToolsTitle: "أدوات المحرر",
|
||||||
|
editorToolUndo: "<strong>↶ تراجع</strong> / <strong>↷ إعادة</strong> - التنقل في سجل التحرير",
|
||||||
|
editorToolReset: "<strong>⟲ إعادة تعيين</strong> - استعادة الكود الأولي",
|
||||||
|
editorToolExpected: "<strong>إظهار المتوقع</strong> - تبديل طبقة النتيجة المستهدفة",
|
||||||
|
keyboardShortcutsTitle: "اختصارات لوحة المفاتيح",
|
||||||
|
shortcutRun: "<kbd>Ctrl+Enter</kbd> - التحقق فورًا",
|
||||||
|
shortcutUndo: "<kbd>Ctrl+Z</kbd> - تراجع",
|
||||||
|
shortcutRedo: "<kbd>Ctrl+Shift+Z</kbd> - إعادة",
|
||||||
|
emmetTitle: "اختصارات Emmet (وضع HTML)",
|
||||||
|
emmetText: "اكتب الاختصارات واضغط <kbd>Tab</kbd> للتوسيع:",
|
||||||
|
emmetClass: "<kbd>div.box</kbd> ← div مع فئة",
|
||||||
|
emmetChildren: "<kbd>ul>li*3</kbd> ← ul مع 3 عناصر li",
|
||||||
|
emmetNested: "<kbd>form>input+button</kbd> ← هيكل متداخل",
|
||||||
|
emmetContent: "<kbd>p{مرحبا}</kbd> ← p مع محتوى نصي",
|
||||||
|
|
||||||
|
// More Projects
|
||||||
|
moreProjectsTitle: "مشاريع أخرى",
|
||||||
|
htmlOverJsDesc: " - تعلم استخدام عناصر HTML الأصلية بدلاً من حلول JavaScript المخصصة",
|
||||||
|
mandalaDesc: " - تصور تفاعلي لتقنيات JavaScript مرتبة حسب التعقيد",
|
||||||
|
|
||||||
|
// Contact
|
||||||
|
contactTitle: "التواصل والروابط",
|
||||||
|
contactText: "Code Crispies تم تطويره بواسطة <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a>",
|
||||||
|
|
||||||
|
// Reset dialog
|
||||||
|
resetDialogTitle: "إعادة تعيين التقدم",
|
||||||
|
resetDialogText: "هل أنت متأكد أنك تريد إعادة تعيين كل تقدمك؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||||
|
cancel: "إلغاء",
|
||||||
|
resetAll: "إعادة تعيين الكل",
|
||||||
|
|
||||||
|
// Dynamic content
|
||||||
|
loadingFallbackText: "تعذر تحميل الدرس. اختر واحدًا من القائمة أو تحقق من المساعدة.",
|
||||||
|
completed: "مكتمل",
|
||||||
|
successMessage: "CRISPY! ٩(◕‿◕)۶ الكود يعمل بشكل صحيح.",
|
||||||
|
keepTrying: "استمر في المحاولة!",
|
||||||
|
failedToLoad: "فشل تحميل الوحدات. قم بتحديث الصفحة.",
|
||||||
|
tailwindPlaceholder: "أدخل فئات Tailwind (مثل bg-blue-500 text-white p-4)",
|
||||||
|
lessonFallback: "درس {index}",
|
||||||
|
untitledLesson: "درس بدون عنوان"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ukrainian
|
||||||
|
uk: {
|
||||||
|
// Page
|
||||||
|
pageTitle: "Code Crispies - Вивчай HTML та CSS інтерактивно",
|
||||||
|
skipLink: "Перейти до основного вмісту",
|
||||||
|
|
||||||
|
// Header
|
||||||
|
menuOpen: "Відкрити меню",
|
||||||
|
langSwitch: "EN",
|
||||||
|
langSwitchLabel: "Switch language: English",
|
||||||
|
help: "Допомога",
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
loading: "Завантаження...",
|
||||||
|
selectLesson: "Оберіть урок, щоб почати.",
|
||||||
|
editorLabel: "Редактор CSS",
|
||||||
|
undoTitle: "Скасувати (Ctrl+Z)",
|
||||||
|
redoTitle: "Повторити (Ctrl+Shift+Z)",
|
||||||
|
resetCodeTitle: "Відновити початковий код",
|
||||||
|
run: "Запустити",
|
||||||
|
rerun: "Запустити знову",
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
yourOutput: "Ваш результат",
|
||||||
|
showExpected: "Показати очікуване",
|
||||||
|
hideExpected: "Сховати очікуване",
|
||||||
|
previous: "Попередній",
|
||||||
|
next: "Наступний",
|
||||||
|
levelIndicator: "Урок {current} з {total}",
|
||||||
|
lessonLabel: "Урок",
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
menu: "Меню",
|
||||||
|
closeMenu: "Закрити меню",
|
||||||
|
progress: "Прогрес",
|
||||||
|
progressText: "{percent}% завершено ({completed}/{total})",
|
||||||
|
lessons: "Уроки",
|
||||||
|
settings: "Налаштування",
|
||||||
|
showHints: "Показувати підказки",
|
||||||
|
resetAllProgress: "Скинути весь прогрес",
|
||||||
|
openSource: "Відкритий код:",
|
||||||
|
by: "від",
|
||||||
|
|
||||||
|
// Help dialog
|
||||||
|
helpTitle: "Допомога",
|
||||||
|
aboutTitle: "Про Code Crispies",
|
||||||
|
aboutText: "Code Crispies — це безкоштовна платформа з відкритим кодом для вивчення веб-розробки через практичні вправи. Обліковий запис не потрібен — просто починайте кодувати!",
|
||||||
|
learningModesTitle: "Режими навчання",
|
||||||
|
modeCss: "<strong>CSS</strong> - Пишіть правила CSS для стилізації елементів",
|
||||||
|
modeTailwind: "<strong>Tailwind</strong> - Застосовуйте утилітарні класи безпосередньо в HTML",
|
||||||
|
modeHtml: "<strong>HTML</strong> - Практикуйте семантичну розмітку та нативні елементи",
|
||||||
|
gettingStartedTitle: "Початок роботи",
|
||||||
|
gettingStartedText: "Відкрийте меню (☰), щоб переглянути модулі уроків. Кожен модуль охоплює конкретну тему з прогресивними вправами.",
|
||||||
|
completingLessonsTitle: "Завершення уроків",
|
||||||
|
completingStep1: "Прочитайте інструкції завдання зліва",
|
||||||
|
completingStep2: "Напишіть свій код у редакторі",
|
||||||
|
completingStep3: "Спостерігайте за попереднім переглядом під час введення",
|
||||||
|
completingStep4: "Слідуйте підказкам, щоб виправити проблеми",
|
||||||
|
completingStep5: "Натисніть <strong>Наступний</strong> після завершення",
|
||||||
|
editorToolsTitle: "Інструменти редактора",
|
||||||
|
editorToolUndo: "<strong>↶ Скасувати</strong> / <strong>↷ Повторити</strong> - Навігація історією редагування",
|
||||||
|
editorToolReset: "<strong>⟲ Скинути</strong> - Відновити початковий код",
|
||||||
|
editorToolExpected: "<strong>Показати очікуване</strong> - Перемкнути накладення цільового результату",
|
||||||
|
keyboardShortcutsTitle: "Гарячі клавіші",
|
||||||
|
shortcutRun: "<kbd>Ctrl+Enter</kbd> - Негайна перевірка",
|
||||||
|
shortcutUndo: "<kbd>Ctrl+Z</kbd> - Скасувати",
|
||||||
|
shortcutRedo: "<kbd>Ctrl+Shift+Z</kbd> - Повторити",
|
||||||
|
emmetTitle: "Скорочення Emmet (режим HTML)",
|
||||||
|
emmetText: "Введіть скорочення та натисніть <kbd>Tab</kbd> для розгортання:",
|
||||||
|
emmetClass: "<kbd>div.box</kbd> → div з класом",
|
||||||
|
emmetChildren: "<kbd>ul>li*3</kbd> → ul з 3 дочірніми li",
|
||||||
|
emmetNested: "<kbd>form>input+button</kbd> → вкладена структура",
|
||||||
|
emmetContent: "<kbd>p{Привіт}</kbd> → p з текстовим вмістом",
|
||||||
|
|
||||||
|
// More Projects
|
||||||
|
moreProjectsTitle: "Більше проектів",
|
||||||
|
htmlOverJsDesc: " - Навчіться використовувати нативні HTML-елементи замість власних JavaScript-рішень",
|
||||||
|
mandalaDesc: " - Інтерактивна візуалізація JavaScript-технологій, впорядкованих за складністю",
|
||||||
|
|
||||||
|
// Contact
|
||||||
|
contactTitle: "Контакти та посилання",
|
||||||
|
contactText: "Code Crispies розроблено <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a>",
|
||||||
|
|
||||||
|
// Reset dialog
|
||||||
|
resetDialogTitle: "Скинути прогрес",
|
||||||
|
resetDialogText: "Ви впевнені, що хочете скинути весь свій прогрес? Цю дію неможливо скасувати.",
|
||||||
|
cancel: "Скасувати",
|
||||||
|
resetAll: "Скинути все",
|
||||||
|
|
||||||
|
// Dynamic content
|
||||||
|
loadingFallbackText: "Не вдалося завантажити урок. Виберіть один з меню або перевірте допомогу.",
|
||||||
|
completed: "Завершено",
|
||||||
|
successMessage: "CRISPY! ٩(◕‿◕)۶ Ваш код працює правильно.",
|
||||||
|
keepTrying: "Продовжуйте спроби!",
|
||||||
|
failedToLoad: "Не вдалося завантажити модулі. Оновіть сторінку.",
|
||||||
|
tailwindPlaceholder: "Введіть класи Tailwind (напр. bg-blue-500 text-white p-4)",
|
||||||
|
lessonFallback: "Урок {index}",
|
||||||
|
untitledLesson: "Урок без назви"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentLang = "en";
|
let currentLang = "en";
|
||||||
|
|
||||||
|
// Available languages in cycle order
|
||||||
|
const availableLanguages = ["en", "de", "pl", "es", "ar", "uk"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get array of available language codes
|
||||||
|
*/
|
||||||
|
export function getAvailableLanguages() {
|
||||||
|
return availableLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next language in the cycle
|
||||||
|
* @param {string} currentLang - Current language code
|
||||||
|
* @returns {string} Next language code
|
||||||
|
*/
|
||||||
|
export function getNextLanguage(current = currentLang) {
|
||||||
|
const currentIndex = availableLanguages.indexOf(current);
|
||||||
|
const nextIndex = (currentIndex + 1) % availableLanguages.length;
|
||||||
|
return availableLanguages[nextIndex];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect initial language from localStorage or browser
|
* Detect initial language from localStorage or browser
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user