Compare commits
5 Commits
004-valida
...
009-colors
| Author | SHA1 | Date | |
|---|---|---|---|
| 1baff9075c | |||
| 3d6ff645fe | |||
| dc048eba4e | |||
| 05a683388b | |||
| ae8f9fef45 |
@@ -9,7 +9,7 @@
|
||||
"id": "box-model-1",
|
||||
"title": "Padding",
|
||||
"description": "Every element in CSS is a box with four layers: content, padding, border, and margin. <strong>Padding</strong> creates breathing room between your content and the box's edge.<br><br>Without padding, text presses against borders awkwardly. Padding makes content readable and visually balanced.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
|
||||
"task": "This profile card looks cramped. Add <kbd>padding: 1rem</kbd> to <kbd>.card</kbd> so the text has room to breathe.",
|
||||
"task": "The text inside this profile card is pressed right against the edges. Give it some inner breathing room.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": "box-model-2",
|
||||
"title": "Borders",
|
||||
"description": "Borders create visual boundaries around elements. The <kbd>border</kbd> shorthand takes three values: width, style, and color.<br><br>Common styles: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
|
||||
"task": "Add a subtle left accent to the card with <kbd>border-left: 4px solid steelblue</kbd>.",
|
||||
"task": "This card could use a colored accent line along its left edge.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -52,7 +52,7 @@
|
||||
"id": "box-model-3",
|
||||
"title": "Margins",
|
||||
"description": "Margins create space <em>outside</em> the element, separating it from neighbors. While padding pushes content inward, margins push other elements away.",
|
||||
"task": "Add space between these two profile cards with <kbd>margin-bottom: 1rem</kbd> on <kbd>.card</kbd>.",
|
||||
"task": "These two profile cards are touching each other. Add some space below each card to separate them.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -73,7 +73,7 @@
|
||||
"id": "box-model-4",
|
||||
"title": "Box Sizing",
|
||||
"description": "By default, <kbd>width</kbd> only sets the content width. Padding and borders add to the total. This causes layout headaches.<br><br><kbd>box-sizing: border-box</kbd> includes padding and border in the width, making sizing predictable. Most developers apply this to all elements.",
|
||||
"task": "Both cards have <kbd>width: 200px</kbd>. The left uses default sizing (content-box), making it wider than expected. Fix the right card with <kbd>box-sizing: border-box</kbd>.",
|
||||
"task": "Both cards are set to the same width, but the left one overflows because padding and border are added on top. Fix the right card so its size includes padding and border.",
|
||||
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -94,7 +94,7 @@
|
||||
"id": "box-model-5",
|
||||
"title": "Padding Shorthand",
|
||||
"description": "Padding accepts 1-4 values:<br>• 1 value: all sides<br>• 2 values: vertical | horizontal<br>• 4 values: top | right | bottom | left",
|
||||
"task": "This button needs more horizontal space than vertical. Set <kbd>padding: 8px 1rem</kbd> (8px top/bottom, 1rem left/right).",
|
||||
"task": "This button feels too tight. Give it more space on the sides than on top and bottom, using the two-value shorthand.",
|
||||
"previewHTML": "<button class=\"btn\">Follow</button>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -116,7 +116,7 @@
|
||||
"id": "box-model-6",
|
||||
"title": "Margin Shorthand",
|
||||
"description": "Margin uses the same shorthand pattern as padding. A common pattern is centering block elements horizontally with <kbd>margin: 0 auto</kbd>.",
|
||||
"task": "Center this card horizontally. Set <kbd>margin: 0 auto</kbd> to auto-calculate equal left/right margins.",
|
||||
"task": "This card is stuck to the left. Center it horizontally using the margin shorthand with auto side margins.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -138,7 +138,7 @@
|
||||
"id": "box-model-7",
|
||||
"title": "Border Radius",
|
||||
"description": "While not part of the classic box model, <kbd>border-radius</kbd> rounds the corners of an element's border box. Use <kbd>50%</kbd> on a square element to create a circle.",
|
||||
"task": "Make the avatar image circular with <kbd>border-radius: 50%</kbd>.",
|
||||
"task": "The square avatar image should appear as a perfect circle.",
|
||||
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -159,7 +159,7 @@
|
||||
"id": "box-model-8",
|
||||
"title": "Complete Card",
|
||||
"description": "Let's combine everything. This notification card needs styling to look polished.",
|
||||
"task": "Style the notification: add <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd>, and <kbd>border-radius: 4px</kbd>.",
|
||||
"task": "This notification needs three things: inner spacing so text isn't cramped, a colored accent on the left edge, and slightly rounded corners.",
|
||||
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"id": "colors-1",
|
||||
"title": "Background Color",
|
||||
"description": "Color is one of the most powerful tools in web design. It creates visual hierarchy, conveys meaning, and establishes brand identity. CSS provides multiple ways to specify colors.<br><br><strong>CSS named colors:</strong> CSS includes 147 named colors like <kbd>steelblue</kbd>, <kbd>coral</kbd>, <kbd>gold</kbd>, and <kbd>tomato</kbd>. These are easy to remember and read.<br><br><strong>The background-color property:</strong> Sets the fill color behind an element's content and padding areas.<br><br><pre>.card {\n background-color: lightblue;\n}</pre>",
|
||||
"task": "This notification card needs a subtle background. Add <kbd>background-color: seashell</kbd>.",
|
||||
"task": "This notification card looks bare. Give it a soft, warm background color.",
|
||||
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { padding: 1rem; border-left: 4px solid coral; border-radius: 4px; } .alert strong { display: block; margin-bottom: 4px; } .alert p { margin: 0; color: #666; font-size: 0.9rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -20,9 +20,10 @@
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background-color", "expected": "seashell" },
|
||||
"message": "Which property fills the area behind the content?"
|
||||
"type": "regex",
|
||||
"value": "background-color:\\s*(seashell|linen|mistyrose|lavenderblush|cornsilk|oldlace|papayawhip|antiquewhite|bisque|peachpuff)",
|
||||
"message": "Which property fills the area behind the content? Try a warm, soft color name",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -30,7 +31,7 @@
|
||||
"id": "colors-2",
|
||||
"title": "Text Color",
|
||||
"description": "The <kbd>color</kbd> property sets the color of text content. Good contrast between text and background is essential for readability and accessibility.",
|
||||
"task": "Make the alert title stand out. Add <kbd>color: coral</kbd>.",
|
||||
"task": "The alert title blends in with the body text. Make it pop with a warm accent color.",
|
||||
"previewHTML": "<div class=\"alert\"><strong class=\"title\">Warning</strong><p>Your session will expire in 5 minutes</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { padding: 1rem; background-color: seashell; border-left: 4px solid coral; border-radius: 4px; } .alert .title { display: block; margin-bottom: 4px; } .alert p { margin: 0; color: #666; font-size: 0.9rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -41,9 +42,10 @@
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Which property changes the text color?"
|
||||
"type": "regex",
|
||||
"value": "color:\\s*(coral|tomato|orangered|indianred|salmon|darksalmon)",
|
||||
"message": "Which property changes the text color? Try a warm, vibrant color name",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -51,7 +53,7 @@
|
||||
"id": "colors-3",
|
||||
"title": "Border Color",
|
||||
"description": "Borders can have their own color using <kbd>border-color</kbd>. This is useful when you want to change just the color without redefining the entire border.",
|
||||
"task": "This card needs an accent border. Add <kbd>border-color: coral</kbd>.",
|
||||
"task": "The card border is dull gray. Give it a warm accent color.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Premium Plan</h3><p>Unlimited access to all features</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { padding: 1rem; background: white; border: 4px solid #ddd; border-radius: 8px; } .card h3 { margin: 0 0 8px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -62,9 +64,10 @@
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-color", "expected": "coral" },
|
||||
"message": "Which property changes just the border's color without redefining the whole border?"
|
||||
"type": "regex",
|
||||
"value": "border-color:\\s*(coral|tomato|orangered|indianred|salmon|darksalmon|crimson)",
|
||||
"message": "Which property changes just the border's color? Try a warm, vibrant name",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -72,7 +75,7 @@
|
||||
"id": "colors-4",
|
||||
"title": "Hex Colors",
|
||||
"description": "Beyond named colors, CSS supports hex codes (<kbd>#ff6347</kbd>), RGB (<kbd>rgb(255, 99, 71)</kbd>), and HSL (<kbd>hsl(9, 100%, 64%)</kbd>). Hex codes are the most common format in professional projects.",
|
||||
"task": "Set the badge background to gold using its hex code. Add <kbd>background-color: #ffd700</kbd>.",
|
||||
"task": "This badge needs a golden background. Use a hex color code to set it.",
|
||||
"previewHTML": "<span class=\"badge\">NEW</span>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .badge { display: inline-block; padding: 4px 12px; border-radius: 999px; font-size: 0.75rem; font-weight: bold; text-transform: uppercase; color: #333; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -83,9 +86,10 @@
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background-color", "expected": "#ffd700" },
|
||||
"message": "Use the same background property, but with a hex code this time"
|
||||
"type": "regex",
|
||||
"value": "background-color:\\s*(#ffd700|#ffcc00|#ffc107|#f0c000|gold)",
|
||||
"message": "Use a hex code for background-color — something in the gold/yellow family",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
139
lessons/50-js-variables.json
Normal file
139
lessons/50-js-variables.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"$schema": "../schemas/code-crispies-module-schema.json",
|
||||
"id": "js-variables",
|
||||
"title": "JS Variables",
|
||||
"description": "Learn to declare variables with let and const, and work with basic data types in JavaScript.",
|
||||
"mode": "javascript",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "js-const",
|
||||
"title": "Constants",
|
||||
"description": "Use <kbd>const</kbd> to declare a variable that cannot be reassigned. Constants are the default choice for most values in modern JavaScript.",
|
||||
"task": "Declare a constant named <kbd>name</kbd> with the value <kbd>\"Alice\"</kbd>",
|
||||
"previewHTML": "<p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "\ndocument.getElementById('out').textContent = name;",
|
||||
"solution": "const name = \"Alice\";",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "const",
|
||||
"message": "Use <kbd>const</kbd> to declare the variable"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "const\\s+name\\s*=",
|
||||
"message": "Declare a constant called <kbd>name</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "\"Alice\"|'Alice'|`Alice`",
|
||||
"message": "Set the value to <kbd>\"Alice\"</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-let",
|
||||
"title": "Let Variables",
|
||||
"description": "Use <kbd>let</kbd> to declare variables that you plan to reassign later. Unlike <kbd>const</kbd>, a <kbd>let</kbd> variable can change its value.",
|
||||
"task": "Declare a variable <kbd>count</kbd> with <kbd>let</kbd> set to <kbd>0</kbd>, then reassign it to <kbd>5</kbd>",
|
||||
"previewHTML": "<p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "\ndocument.getElementById('out').textContent = count;",
|
||||
"solution": "let count = 0;\ncount = 5;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "let\\s+count\\s*=\\s*0",
|
||||
"message": "Start with <kbd>let count = 0;</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "count\\s*=\\s*5",
|
||||
"message": "Reassign count to <kbd>5</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-string",
|
||||
"title": "Template Literals",
|
||||
"description": "Template literals use backticks <kbd>`</kbd> and <kbd>${}</kbd> to embed expressions inside strings. This makes building dynamic text much easier than string concatenation.",
|
||||
"task": "Create a constant <kbd>msg</kbd> using a template literal: <kbd>`Hello, ${name}!`</kbd>",
|
||||
"previewHTML": "<p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "const name = \"World\";\n",
|
||||
"codeSuffix": "\ndocument.getElementById('out').textContent = msg;",
|
||||
"solution": "const msg = `Hello, ${name}!`;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "const\\s+msg\\s*=",
|
||||
"message": "Declare a constant called <kbd>msg</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "${name}",
|
||||
"message": "Use <kbd>${name}</kbd> inside backticks to embed the variable"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "`.*\\$\\{name\\}.*`",
|
||||
"message": "Wrap the whole string in backticks <kbd>`</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-array",
|
||||
"title": "Arrays",
|
||||
"description": "Arrays store ordered lists of values in square brackets. Access items by index (starting at 0) and use <kbd>.length</kbd> to get the count.",
|
||||
"task": "Create a constant <kbd>colors</kbd> with an array: <kbd>[\"red\", \"green\", \"blue\"]</kbd>",
|
||||
"previewHTML": "<p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "\ndocument.getElementById('out').textContent = colors.join(', ');",
|
||||
"solution": "const colors = [\"red\", \"green\", \"blue\"];",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "const\\s+colors\\s*=",
|
||||
"message": "Declare a constant called <kbd>colors</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "[",
|
||||
"message": "Use square brackets <kbd>[</kbd> to create an array"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(\"red\"|'red'|`red`)",
|
||||
"message": "Include <kbd>\"red\"</kbd> in the array"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(\"green\"|'green'|`green`)",
|
||||
"message": "Include <kbd>\"green\"</kbd> in the array"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(\"blue\"|'blue'|`blue`)",
|
||||
"message": "Include <kbd>\"blue\"</kbd> in the array"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
139
lessons/51-js-dom.json
Normal file
139
lessons/51-js-dom.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"$schema": "../schemas/code-crispies-module-schema.json",
|
||||
"id": "js-dom",
|
||||
"title": "JS DOM",
|
||||
"description": "Learn to select and modify HTML elements using JavaScript DOM methods like querySelector and textContent.",
|
||||
"mode": "javascript",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "js-query",
|
||||
"title": "querySelector",
|
||||
"description": "Use <kbd>document.querySelector()</kbd> to find the first element matching a CSS selector. It returns a single element you can then modify.",
|
||||
"task": "Select the <kbd>h1</kbd> element and store it in a constant called <kbd>title</kbd>",
|
||||
"previewHTML": "<h1>Hello</h1><p id=\"out\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "\ndocument.getElementById('out').textContent = title.tagName;",
|
||||
"solution": "const title = document.querySelector('h1');",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "querySelector",
|
||||
"message": "Use <kbd>document.querySelector()</kbd> to select an element"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "querySelector\\(['\"`]h1['\"`]\\)",
|
||||
"message": "Pass <kbd>'h1'</kbd> as the selector"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "const\\s+title\\s*=",
|
||||
"message": "Store the result in a constant called <kbd>title</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-text",
|
||||
"title": "textContent",
|
||||
"description": "The <kbd>textContent</kbd> property lets you read or change the text inside an element. Setting it replaces all existing text.",
|
||||
"task": "Select the <kbd>.msg</kbd> element and set its <kbd>textContent</kbd> to <kbd>\"Done!\"</kbd>",
|
||||
"previewHTML": "<p class=\"msg\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "",
|
||||
"solution": "document.querySelector('.msg').textContent = \"Done!\";",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "querySelector",
|
||||
"message": "Use <kbd>querySelector</kbd> to find the element"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "textContent",
|
||||
"message": "Use the <kbd>textContent</kbd> property to change the text"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(\"Done!\"|'Done!'|`Done!`)",
|
||||
"message": "Set the text to <kbd>\"Done!\"</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-style",
|
||||
"title": "Inline Styles",
|
||||
"description": "Access the <kbd>style</kbd> property to set inline CSS on an element. CSS properties with dashes become camelCase: <kbd>background-color</kbd> becomes <kbd>backgroundColor</kbd>.",
|
||||
"task": "Select the <kbd>.box</kbd> element and set its <kbd>style.color</kbd> to <kbd>\"coral\"</kbd>",
|
||||
"previewHTML": "<p class=\"box\">Style me!</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { font-size: 1.5rem; font-weight: bold; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "",
|
||||
"solution": "document.querySelector('.box').style.color = \"coral\";",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "querySelector",
|
||||
"message": "Use <kbd>querySelector</kbd> to find the element"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": ".style.",
|
||||
"message": "Use the <kbd>.style</kbd> property to set CSS"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "style\\.color\\s*=",
|
||||
"message": "Set <kbd>style.color</kbd> on the element"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(\"coral\"|'coral'|`coral`)",
|
||||
"message": "Set the color to <kbd>\"coral\"</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-classlist",
|
||||
"title": "classList",
|
||||
"description": "The <kbd>classList</kbd> property provides methods to add, remove, or toggle CSS classes on an element without touching other classes.",
|
||||
"task": "Select the <kbd>.card</kbd> element and add the class <kbd>\"active\"</kbd> using <kbd>classList.add()</kbd>",
|
||||
"previewHTML": "<div class=\"card\">Toggle me</div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .card { padding: 1rem; border: 2px solid gray; border-radius: 8px; } .active { border-color: coral; background: #fff0ee; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "",
|
||||
"codeSuffix": "",
|
||||
"solution": "document.querySelector('.card').classList.add(\"active\");",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "classList",
|
||||
"message": "Use the <kbd>classList</kbd> property"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "classList\\.add\\(",
|
||||
"message": "Call <kbd>classList.add()</kbd> to add a class"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(\"active\"|'active'|`active`)",
|
||||
"message": "Add the class <kbd>\"active\"</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
118
lessons/52-js-events.json
Normal file
118
lessons/52-js-events.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"$schema": "../schemas/code-crispies-module-schema.json",
|
||||
"id": "js-events",
|
||||
"title": "JS Events",
|
||||
"description": "Learn to respond to user interactions with addEventListener for clicks, input changes, and keyboard events.",
|
||||
"mode": "javascript",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "js-click",
|
||||
"title": "Click Events",
|
||||
"description": "Use <kbd>addEventListener('click', ...)</kbd> to run code when a user clicks an element. The first argument is the event name, the second is a callback function.",
|
||||
"task": "Add a click listener to the <kbd>.btn</kbd> element that sets the <kbd>.msg</kbd> text to <kbd>\"Clicked!\"</kbd>",
|
||||
"previewHTML": "<button class=\"btn\">Click me</button><p class=\"msg\">Waiting...</p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { padding: 0.5rem 1rem; border: none; background: steelblue; color: white; border-radius: 4px; cursor: pointer; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "const btn = document.querySelector('.btn');\nconst msg = document.querySelector('.msg');\n\n",
|
||||
"codeSuffix": "",
|
||||
"solution": "btn.addEventListener('click', () => {\n msg.textContent = \"Clicked!\";\n});",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "addEventListener",
|
||||
"message": "Use <kbd>addEventListener</kbd> to listen for events"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "addEventListener\\(['\"`]click['\"`]",
|
||||
"message": "Listen for the <kbd>'click'</kbd> event"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "textContent",
|
||||
"message": "Use <kbd>textContent</kbd> to update the text"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(\"Clicked!\"|'Clicked!'|`Clicked!`)",
|
||||
"message": "Set the text to <kbd>\"Clicked!\"</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-toggle",
|
||||
"title": "Toggle Classes",
|
||||
"description": "Combine events with <kbd>classList.toggle()</kbd> to switch a class on and off. Each click adds the class if missing, or removes it if present.",
|
||||
"task": "Add a click listener to <kbd>.btn</kbd> that toggles the class <kbd>\"on\"</kbd> on <kbd>.lamp</kbd>",
|
||||
"previewHTML": "<button class=\"btn\">Toggle</button><div class=\"lamp\">💡</div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; text-align: center; } .btn { padding: 0.5rem 1rem; border: none; background: steelblue; color: white; border-radius: 4px; cursor: pointer; } .lamp { font-size: 3rem; margin-top: 1rem; opacity: 0.3; transition: opacity 0.3s; } .lamp.on { opacity: 1; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "const btn = document.querySelector('.btn');\nconst lamp = document.querySelector('.lamp');\n\n",
|
||||
"codeSuffix": "",
|
||||
"solution": "btn.addEventListener('click', () => {\n lamp.classList.toggle('on');\n});",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "addEventListener",
|
||||
"message": "Use <kbd>addEventListener</kbd> to listen for events"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "addEventListener\\(['\"`]click['\"`]",
|
||||
"message": "Listen for the <kbd>'click'</kbd> event"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "classList\\.toggle\\(",
|
||||
"message": "Use <kbd>classList.toggle()</kbd> to switch the class"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(\"on\"|'on'|`on`)",
|
||||
"message": "Toggle the class <kbd>\"on\"</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "js-input",
|
||||
"title": "Input Events",
|
||||
"description": "The <kbd>input</kbd> event fires every time the value of an input field changes. Use <kbd>event.target.value</kbd> to read the current value.",
|
||||
"task": "Add an input listener to <kbd>.field</kbd> that sets <kbd>.out</kbd> text to the input's value",
|
||||
"previewHTML": "<input class=\"field\" placeholder=\"Type here...\"><p class=\"out\">Echo: </p>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .field { padding: 0.5rem; border: 2px solid #ccc; border-radius: 4px; font-size: 1rem; width: 100%; box-sizing: border-box; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "",
|
||||
"codePrefix": "const field = document.querySelector('.field');\nconst out = document.querySelector('.out');\n\n",
|
||||
"codeSuffix": "",
|
||||
"solution": "field.addEventListener('input', (event) => {\n out.textContent = event.target.value;\n});",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "addEventListener",
|
||||
"message": "Use <kbd>addEventListener</kbd> to listen for events"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "addEventListener\\(['\"`]input['\"`]",
|
||||
"message": "Listen for the <kbd>'input'</kbd> event"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "textContent",
|
||||
"message": "Use <kbd>textContent</kbd> to update the output"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "(event|e|evt)\\.target\\.value",
|
||||
"message": "Read the input value with <kbd>event.target.value</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
"id": "box-model-1",
|
||||
"title": "Padding",
|
||||
"description": "كل عنصر في CSS هو صندوق بأربع طبقات: المحتوى، الحشو (padding)، الحدود، والهامش. <strong>Padding</strong> يخلق مساحة تنفس بين محتواك وحافة الصندوق.<br><br>بدون padding، يضغط النص بشكل محرج على الحدود. Padding يجعل المحتوى قابلاً للقراءة ومتوازناً بصرياً.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
|
||||
"task": "بطاقة الملف الشخصي هذه تبدو ضيقة. أضف <kbd>padding: 1rem</kbd> ليكون للنص مجال للتنفس.",
|
||||
"task": "النص داخل بطاقة الملف الشخصي ملتصق بالحواف. امنحه بعض المساحة الداخلية للتنفس.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": "box-model-2",
|
||||
"title": "Borders",
|
||||
"description": "الحدود تنشئ حدوداً مرئية حول العناصر. اختصار <kbd>border</kbd> يقبل ثلاث قيم: العرض، النمط، واللون.<br><br>الأنماط الشائعة: <kbd>solid</kbd>، <kbd>dashed</kbd>، <kbd>dotted</kbd>، <kbd>none</kbd>",
|
||||
"task": "أضف لمسة يسارية خفيفة للبطاقة باستخدام <kbd>border-left: 4px solid steelblue</kbd>.",
|
||||
"task": "هذه البطاقة تحتاج خطاً ملوناً كلمسة على حافتها اليسرى.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -52,7 +52,7 @@
|
||||
"id": "box-model-3",
|
||||
"title": "Margins",
|
||||
"description": "الهوامش تنشئ مساحة <em>خارج</em> العنصر، تفصله عن جيرانه. بينما يدفع padding المحتوى للداخل، الهوامش تدفع العناصر الأخرى بعيداً.",
|
||||
"task": "أضف مساحة بين بطاقتي الملف الشخصي هاتين باستخدام <kbd>margin-bottom: 1rem</kbd> على <kbd>.card</kbd>.",
|
||||
"task": "بطاقتا الملف الشخصي ملتصقتان ببعضهما. أضف مساحة أسفل كل بطاقة للفصل بينهما.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -73,7 +73,7 @@
|
||||
"id": "box-model-4",
|
||||
"title": "Box Sizing",
|
||||
"description": "افتراضياً، <kbd>width</kbd> يحدد فقط عرض المحتوى. Padding والحدود تُضاف للمجموع. هذا يسبب مشاكل في التخطيط.<br><br><kbd>box-sizing: border-box</kbd> يشمل padding والحدود في العرض، مما يجعل التحجيم متوقعاً. معظم المطورين يطبقون هذا على جميع العناصر.",
|
||||
"task": "كلا البطاقتين لهما <kbd>width: 200px</kbd>. اليسرى تستخدم التحجيم الافتراضي (content-box)، مما يجعلها أعرض من المتوقع. أصلح البطاقة اليمنى باستخدام <kbd>box-sizing: border-box</kbd>.",
|
||||
"task": "كلا البطاقتين بنفس العرض، لكن اليسرى تتجاوز لأن الحشو والحدود تُضاف فوق العرض. أصلح البطاقة اليمنى لتشمل الحشو والحدود في حجمها.",
|
||||
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -94,7 +94,7 @@
|
||||
"id": "box-model-5",
|
||||
"title": "Padding Shorthand",
|
||||
"description": "Padding يقبل 1-4 قيم:<br>• قيمة واحدة: جميع الجوانب<br>• قيمتان: عمودي | أفقي<br>• 4 قيم: أعلى | يمين | أسفل | يسار",
|
||||
"task": "هذا الزر يحتاج مساحة أفقية أكثر من العمودية. اضبط <kbd>padding: 8px 1rem</kbd> (8px أعلى/أسفل، 1rem يسار/يمين).",
|
||||
"task": "هذا الزر ضيق جداً. امنحه مساحة على الجوانب أكثر من الأعلى والأسفل، باستخدام اختصار القيمتين.",
|
||||
"previewHTML": "<button class=\"btn\">Follow</button>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -116,7 +116,7 @@
|
||||
"id": "box-model-6",
|
||||
"title": "Margin Shorthand",
|
||||
"description": "Margin يستخدم نفس نمط الاختصار مثل padding. نمط شائع هو توسيط عناصر الكتلة أفقياً باستخدام <kbd>margin: 0 auto</kbd>.",
|
||||
"task": "وسّط هذه البطاقة أفقياً. اضبط <kbd>margin: 0 auto</kbd> لحساب هوامش يسار/يمين متساوية تلقائياً.",
|
||||
"task": "هذه البطاقة ملتصقة باليسار. وسّطها أفقياً باستخدام اختصار الهوامش مع هوامش جانبية تلقائية.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -138,7 +138,7 @@
|
||||
"id": "box-model-7",
|
||||
"title": "Border Radius",
|
||||
"description": "على الرغم من أنه ليس جزءاً من نموذج الصندوق الكلاسيكي، <kbd>border-radius</kbd> يُدوّر زوايا صندوق حدود العنصر. استخدم <kbd>50%</kbd> على عنصر مربع لإنشاء دائرة.",
|
||||
"task": "اجعل صورة الأفاتار دائرية باستخدام <kbd>border-radius: 50%</kbd>.",
|
||||
"task": "صورة الأفاتار المربعة يجب أن تظهر كدائرة مثالية.",
|
||||
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -159,7 +159,7 @@
|
||||
"id": "box-model-8",
|
||||
"title": "Complete Card",
|
||||
"description": "لنجمع كل شيء معاً. بطاقة الإشعار هذه تحتاج تنسيقاً لتبدو احترافية.",
|
||||
"task": "نسّق الإشعار: أضف <kbd>padding: 1rem</kbd>، <kbd>border-left: 4px solid coral</kbd>، و<kbd>border-radius: 4px</kbd>.",
|
||||
"task": "هذا الإشعار يحتاج ثلاثة أشياء: مساحة داخلية حتى لا يكون النص مزدحماً، لمسة ملونة على الحافة اليسرى، وزوايا مستديرة قليلاً.",
|
||||
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"id": "box-model-1",
|
||||
"title": "Padding",
|
||||
"description": "Jedes Element in CSS ist eine Box mit vier Schichten: Inhalt, Padding, Rahmen und Margin. <strong>Padding</strong> schafft Freiraum zwischen deinem Inhalt und dem Rand der Box.<br><br>Ohne Padding drückt sich Text unangenehm gegen Rahmen. Padding macht Inhalte lesbar und visuell ausgewogen.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
|
||||
"task": "Diese Profilkarte sieht beengt aus. Füge <kbd>padding: 1rem</kbd> hinzu, damit der Text Platz zum Atmen hat.",
|
||||
"task": "Der Text in dieser Profilkarte klebt direkt an den Rändern. Gib ihm etwas inneren Freiraum zum Atmen.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": "box-model-2",
|
||||
"title": "Borders",
|
||||
"description": "Rahmen erstellen visuelle Grenzen um Elemente. Die <kbd>border</kbd>-Kurzschreibweise akzeptiert drei Werte: Breite, Stil und Farbe.<br><br>Häufige Stile: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
|
||||
"task": "Füge der Karte einen dezenten linken Akzent hinzu mit <kbd>border-left: 4px solid steelblue</kbd>.",
|
||||
"task": "Diese Karte könnte eine farbige Akzentlinie an der linken Seite gebrauchen.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -52,7 +52,7 @@
|
||||
"id": "box-model-3",
|
||||
"title": "Margins",
|
||||
"description": "Margins schaffen Abstand <em>außerhalb</em> des Elements und trennen es von Nachbarn. Während Padding den Inhalt nach innen drückt, drücken Margins andere Elemente weg.",
|
||||
"task": "Füge Abstand zwischen diesen beiden Profilkarten hinzu mit <kbd>margin-bottom: 1rem</kbd> auf <kbd>.card</kbd>.",
|
||||
"task": "Diese beiden Profilkarten berühren sich. Füge etwas Abstand unterhalb jeder Karte hinzu, um sie zu trennen.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -73,7 +73,7 @@
|
||||
"id": "box-model-4",
|
||||
"title": "Box Sizing",
|
||||
"description": "Standardmäßig setzt <kbd>width</kbd> nur die Inhaltsbreite. Padding und Rahmen werden addiert. Das verursacht Layout-Probleme.<br><br><kbd>box-sizing: border-box</kbd> bezieht Padding und Rahmen in die Breite ein, was das Sizing vorhersehbar macht. Die meisten Entwickler wenden dies auf alle Elemente an.",
|
||||
"task": "Beide Karten haben <kbd>width: 200px</kbd>. Die linke nutzt Standard-Sizing (content-box) und wird breiter als erwartet. Korrigiere die rechte Karte mit <kbd>box-sizing: border-box</kbd>.",
|
||||
"task": "Beide Karten haben die gleiche Breite, aber die linke läuft über, weil Padding und Rahmen obendrauf addiert werden. Korrigiere die rechte Karte, damit ihre Größe Padding und Rahmen einschließt.",
|
||||
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -94,7 +94,7 @@
|
||||
"id": "box-model-5",
|
||||
"title": "Padding Shorthand",
|
||||
"description": "Padding akzeptiert 1-4 Werte:<br>• 1 Wert: alle Seiten<br>• 2 Werte: vertikal | horizontal<br>• 4 Werte: oben | rechts | unten | links",
|
||||
"task": "Dieser Button braucht mehr horizontalen als vertikalen Platz. Setze <kbd>padding: 8px 1rem</kbd> (8px oben/unten, 1rem links/rechts).",
|
||||
"task": "Dieser Button fühlt sich zu eng an. Gib ihm mehr Platz an den Seiten als oben und unten, mit der Zwei-Werte-Kurzschreibweise.",
|
||||
"previewHTML": "<button class=\"btn\">Follow</button>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -116,7 +116,7 @@
|
||||
"id": "box-model-6",
|
||||
"title": "Margin Shorthand",
|
||||
"description": "Margin nutzt das gleiche Kurzschreibweisen-Muster wie Padding. Ein häufiges Muster ist das horizontale Zentrieren von Block-Elementen mit <kbd>margin: 0 auto</kbd>.",
|
||||
"task": "Zentriere diese Karte horizontal. Setze <kbd>margin: 0 auto</kbd>, um automatisch gleiche links/rechts-Margins zu berechnen.",
|
||||
"task": "Diese Karte klebt links. Zentriere sie horizontal mit der Margin-Kurzschreibweise und automatischen Seitenabständen.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -138,7 +138,7 @@
|
||||
"id": "box-model-7",
|
||||
"title": "Border Radius",
|
||||
"description": "Obwohl nicht Teil des klassischen Box-Modells, rundet <kbd>border-radius</kbd> die Ecken der Rahmen-Box eines Elements. Verwende <kbd>50%</kbd> bei einem quadratischen Element, um einen Kreis zu erstellen.",
|
||||
"task": "Mache das Avatar-Bild rund mit <kbd>border-radius: 50%</kbd>.",
|
||||
"task": "Das quadratische Avatar-Bild soll als perfekter Kreis erscheinen.",
|
||||
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -159,7 +159,7 @@
|
||||
"id": "box-model-8",
|
||||
"title": "Complete Card",
|
||||
"description": "Kombinieren wir alles. Diese Benachrichtigungskarte braucht Styling, um professionell auszusehen.",
|
||||
"task": "Style die Benachrichtigung: füge <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> und <kbd>border-radius: 4px</kbd> hinzu.",
|
||||
"task": "Diese Benachrichtigung braucht drei Dinge: inneren Abstand damit der Text nicht gedrängt wirkt, einen farbigen Akzent an der linken Kante und leicht abgerundete Ecken.",
|
||||
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"id": "box-model-1",
|
||||
"title": "Padding",
|
||||
"description": "Cada elemento en CSS es una caja con cuatro capas: contenido, padding, borde y margen. <strong>Padding</strong> crea espacio entre tu contenido y el borde de la caja.<br><br>Sin padding, el texto se aprieta incómodamente contra los bordes. El padding hace que el contenido sea legible y visualmente equilibrado.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
|
||||
"task": "Esta tarjeta de perfil se ve apretada. Añade <kbd>padding: 1rem</kbd> para que el texto tenga espacio para respirar.",
|
||||
"task": "El texto dentro de esta tarjeta de perfil está pegado a los bordes. Dale algo de espacio interior para respirar.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": "box-model-2",
|
||||
"title": "Borders",
|
||||
"description": "Los bordes crean límites visuales alrededor de los elementos. El atajo <kbd>border</kbd> acepta tres valores: ancho, estilo y color.<br><br>Estilos comunes: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
|
||||
"task": "Añade un acento sutil a la izquierda de la tarjeta con <kbd>border-left: 4px solid steelblue</kbd>.",
|
||||
"task": "Esta tarjeta necesita una línea de acento de color en su borde izquierdo.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -52,7 +52,7 @@
|
||||
"id": "box-model-3",
|
||||
"title": "Margins",
|
||||
"description": "Los márgenes crean espacio <em>fuera</em> del elemento, separándolo de sus vecinos. Mientras que el padding empuja el contenido hacia adentro, los márgenes empujan otros elementos hacia afuera.",
|
||||
"task": "Añade espacio entre estas dos tarjetas de perfil con <kbd>margin-bottom: 1rem</kbd> en <kbd>.card</kbd>.",
|
||||
"task": "Estas dos tarjetas de perfil se están tocando. Añade algo de espacio debajo de cada tarjeta para separarlas.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -73,7 +73,7 @@
|
||||
"id": "box-model-4",
|
||||
"title": "Box Sizing",
|
||||
"description": "Por defecto, <kbd>width</kbd> solo establece el ancho del contenido. Padding y bordes se suman al total. Esto causa problemas de diseño.<br><br><kbd>box-sizing: border-box</kbd> incluye padding y borde en el ancho, haciendo el dimensionamiento predecible. La mayoría de desarrolladores aplican esto a todos los elementos.",
|
||||
"task": "Ambas tarjetas tienen <kbd>width: 200px</kbd>. La izquierda usa el tamaño predeterminado (content-box), haciéndola más ancha de lo esperado. Corrige la tarjeta derecha con <kbd>box-sizing: border-box</kbd>.",
|
||||
"task": "Ambas tarjetas tienen el mismo ancho, pero la izquierda se desborda porque el padding y el borde se suman encima. Corrige la tarjeta derecha para que su tamaño incluya el padding y el borde.",
|
||||
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -94,7 +94,7 @@
|
||||
"id": "box-model-5",
|
||||
"title": "Padding Shorthand",
|
||||
"description": "Padding acepta 1-4 valores:<br>• 1 valor: todos los lados<br>• 2 valores: vertical | horizontal<br>• 4 valores: arriba | derecha | abajo | izquierda",
|
||||
"task": "Este botón necesita más espacio horizontal que vertical. Establece <kbd>padding: 8px 1rem</kbd> (8px arriba/abajo, 1rem izquierda/derecha).",
|
||||
"task": "Este botón se siente muy apretado. Dale más espacio en los lados que arriba y abajo, usando el atajo de dos valores.",
|
||||
"previewHTML": "<button class=\"btn\">Follow</button>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -116,7 +116,7 @@
|
||||
"id": "box-model-6",
|
||||
"title": "Margin Shorthand",
|
||||
"description": "Margin usa el mismo patrón de atajo que padding. Un patrón común es centrar elementos de bloque horizontalmente con <kbd>margin: 0 auto</kbd>.",
|
||||
"task": "Centra esta tarjeta horizontalmente. Establece <kbd>margin: 0 auto</kbd> para calcular automáticamente márgenes iguales izquierda/derecha.",
|
||||
"task": "Esta tarjeta está pegada a la izquierda. Céntrala horizontalmente usando el atajo de margen con márgenes laterales automáticos.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -138,7 +138,7 @@
|
||||
"id": "box-model-7",
|
||||
"title": "Border Radius",
|
||||
"description": "Aunque no es parte del modelo de caja clásico, <kbd>border-radius</kbd> redondea las esquinas de la caja de borde de un elemento. Usa <kbd>50%</kbd> en un elemento cuadrado para crear un círculo.",
|
||||
"task": "Haz la imagen del avatar circular con <kbd>border-radius: 50%</kbd>.",
|
||||
"task": "La imagen cuadrada del avatar debería aparecer como un círculo perfecto.",
|
||||
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -159,7 +159,7 @@
|
||||
"id": "box-model-8",
|
||||
"title": "Complete Card",
|
||||
"description": "Combinemos todo. Esta tarjeta de notificación necesita estilo para verse profesional.",
|
||||
"task": "Estiliza la notificación: añade <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> y <kbd>border-radius: 4px</kbd>.",
|
||||
"task": "Esta notificación necesita tres cosas: espacio interior para que el texto no esté apretado, un acento de color en el borde izquierdo y esquinas ligeramente redondeadas.",
|
||||
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"id": "box-model-1",
|
||||
"title": "Padding",
|
||||
"description": "Każdy element w CSS to pudełko z czterema warstwami: treść, padding, ramka i margines. <strong>Padding</strong> tworzy przestrzeń między treścią a krawędzią pudełka.<br><br>Bez paddingu tekst przylega niezręcznie do ramek. Padding sprawia, że treść jest czytelna i wizualnie zbalansowana.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
|
||||
"task": "Ta karta profilu wygląda na ciasną. Dodaj <kbd>padding: 1rem</kbd>, aby tekst miał miejsce do oddychania.",
|
||||
"task": "Tekst wewnątrz tej karty profilu jest przyciśnięty do krawędzi. Daj mu trochę wewnętrznej przestrzeni do oddychania.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": "box-model-2",
|
||||
"title": "Borders",
|
||||
"description": "Ramki tworzą wizualne granice wokół elementów. Skrót <kbd>border</kbd> przyjmuje trzy wartości: szerokość, styl i kolor.<br><br>Popularne style: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
|
||||
"task": "Dodaj subtelny lewy akcent do karty za pomocą <kbd>border-left: 4px solid steelblue</kbd>.",
|
||||
"task": "Ta karta mogłaby mieć kolorową linię akcentową wzdłuż lewej krawędzi.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -52,7 +52,7 @@
|
||||
"id": "box-model-3",
|
||||
"title": "Margins",
|
||||
"description": "Marginesy tworzą przestrzeń <em>na zewnątrz</em> elementu, oddzielając go od sąsiadów. Podczas gdy padding przesuwa treść do wewnątrz, marginesy odpychają inne elementy.",
|
||||
"task": "Dodaj przestrzeń między tymi dwiema kartami profilu za pomocą <kbd>margin-bottom: 1rem</kbd> na <kbd>.card</kbd>.",
|
||||
"task": "Te dwie karty profilu stykają się ze sobą. Dodaj trochę przestrzeni pod każdą kartą, aby je rozdzielić.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -73,7 +73,7 @@
|
||||
"id": "box-model-4",
|
||||
"title": "Box Sizing",
|
||||
"description": "Domyślnie <kbd>width</kbd> ustawia tylko szerokość treści. Padding i ramki są dodawane. To powoduje problemy z układem.<br><br><kbd>box-sizing: border-box</kbd> włącza padding i ramkę do szerokości, czyniąc rozmiary przewidywalnymi. Większość programistów stosuje to do wszystkich elementów.",
|
||||
"task": "Obie karty mają <kbd>width: 200px</kbd>. Lewa używa domyślnego rozmiaru (content-box), stając się szersza niż oczekiwano. Napraw prawą kartę za pomocą <kbd>box-sizing: border-box</kbd>.",
|
||||
"task": "Obie karty mają tę samą szerokość, ale lewa wychodzi poza, bo padding i ramka są dodawane na wierzch. Napraw prawą kartę, aby jej rozmiar obejmował padding i ramkę.",
|
||||
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -94,7 +94,7 @@
|
||||
"id": "box-model-5",
|
||||
"title": "Padding Shorthand",
|
||||
"description": "Padding przyjmuje 1-4 wartości:<br>• 1 wartość: wszystkie strony<br>• 2 wartości: pionowo | poziomo<br>• 4 wartości: góra | prawo | dół | lewo",
|
||||
"task": "Ten przycisk potrzebuje więcej miejsca poziomego niż pionowego. Ustaw <kbd>padding: 8px 1rem</kbd> (8px góra/dół, 1rem lewo/prawo).",
|
||||
"task": "Ten przycisk jest zbyt ciasny. Daj mu więcej przestrzeni po bokach niż na górze i dole, używając skrótu dwuwartościowego.",
|
||||
"previewHTML": "<button class=\"btn\">Follow</button>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -116,7 +116,7 @@
|
||||
"id": "box-model-6",
|
||||
"title": "Margin Shorthand",
|
||||
"description": "Margines używa tego samego wzorca skrótu co padding. Typowym wzorcem jest poziome centrowanie elementów blokowych za pomocą <kbd>margin: 0 auto</kbd>.",
|
||||
"task": "Wycentruj tę kartę poziomo. Ustaw <kbd>margin: 0 auto</kbd>, aby automatycznie obliczyć równe marginesy lewo/prawo.",
|
||||
"task": "Ta karta jest przyklejona do lewej. Wycentruj ją poziomo, używając skrótu marginesu z automatycznymi marginesami bocznymi.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -138,7 +138,7 @@
|
||||
"id": "box-model-7",
|
||||
"title": "Border Radius",
|
||||
"description": "Chociaż nie jest częścią klasycznego modelu pudełkowego, <kbd>border-radius</kbd> zaokrągla rogi ramki elementu. Użyj <kbd>50%</kbd> na kwadratowym elemencie, aby utworzyć koło.",
|
||||
"task": "Zrób okrągły obrazek awatara za pomocą <kbd>border-radius: 50%</kbd>.",
|
||||
"task": "Kwadratowy obrazek awatara powinien wyglądać jak idealne koło.",
|
||||
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -159,7 +159,7 @@
|
||||
"id": "box-model-8",
|
||||
"title": "Complete Card",
|
||||
"description": "Połączmy wszystko razem. Ta karta powiadomienia potrzebuje stylowania, żeby wyglądać profesjonalnie.",
|
||||
"task": "Ostyluj powiadomienie: dodaj <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> i <kbd>border-radius: 4px</kbd>.",
|
||||
"task": "To powiadomienie potrzebuje trzech rzeczy: wewnętrznej przestrzeni, żeby tekst nie był ściśnięty, kolorowego akcentu na lewej krawędzi i lekko zaokrąglonych rogów.",
|
||||
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"id": "box-model-1",
|
||||
"title": "Padding",
|
||||
"description": "Кожен елемент у CSS - це блок з чотирма шарами: контент, відступ (padding), межа та поле. <strong>Padding</strong> створює простір для дихання між вашим контентом і краєм блоку.<br><br>Без padding текст незручно притискається до меж. Padding робить контент читабельним і візуально збалансованим.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
|
||||
"task": "Ця картка профілю виглядає тісною. Додайте <kbd>padding: 1rem</kbd>, щоб текст мав простір для дихання.",
|
||||
"task": "Текст всередині цієї картки профілю притиснутий до країв. Дайте йому трохи внутрішнього простору для дихання.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": "box-model-2",
|
||||
"title": "Borders",
|
||||
"description": "Межі створюють візуальні границі навколо елементів. Скорочення <kbd>border</kbd> приймає три значення: ширину, стиль і колір.<br><br>Поширені стилі: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
|
||||
"task": "Додайте тонкий лівий акцент до картки за допомогою <kbd>border-left: 4px solid steelblue</kbd>.",
|
||||
"task": "Ця картка потребує кольорової акцентної лінії вздовж лівого краю.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -52,7 +52,7 @@
|
||||
"id": "box-model-3",
|
||||
"title": "Margins",
|
||||
"description": "Поля створюють простір <em>зовні</em> елемента, відділяючи його від сусідів. Тоді як padding штовхає контент всередину, поля відштовхують інші елементи.",
|
||||
"task": "Додайте простір між цими двома картками профілю за допомогою <kbd>margin-bottom: 1rem</kbd> на <kbd>.card</kbd>.",
|
||||
"task": "Ці дві картки профілю торкаються одна одної. Додайте трохи простору під кожною карткою, щоб розділити їх.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -73,7 +73,7 @@
|
||||
"id": "box-model-4",
|
||||
"title": "Box Sizing",
|
||||
"description": "За замовчуванням <kbd>width</kbd> встановлює лише ширину контенту. Padding і межі додаються до загальної суми. Це спричиняє проблеми з макетом.<br><br><kbd>box-sizing: border-box</kbd> включає padding і межу у ширину, роблячи розмір передбачуваним. Більшість розробників застосовують це до всіх елементів.",
|
||||
"task": "Обидві картки мають <kbd>width: 200px</kbd>. Ліва використовує стандартний розмір (content-box), стаючи ширшою за очікуване. Виправте праву картку за допомогою <kbd>box-sizing: border-box</kbd>.",
|
||||
"task": "Обидві картки мають однакову ширину, але ліва виходить за межі, бо відступи та межі додаються зверху. Виправте праву картку, щоб її розмір включав відступи та межі.",
|
||||
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -94,7 +94,7 @@
|
||||
"id": "box-model-5",
|
||||
"title": "Padding Shorthand",
|
||||
"description": "Padding приймає 1-4 значення:<br>• 1 значення: всі сторони<br>• 2 значення: вертикально | горизонтально<br>• 4 значення: верх | право | низ | ліво",
|
||||
"task": "Ця кнопка потребує більше горизонтального простору, ніж вертикального. Встановіть <kbd>padding: 8px 1rem</kbd> (8px верх/низ, 1rem ліво/право).",
|
||||
"task": "Ця кнопка занадто тісна. Дайте їй більше простору з боків, ніж зверху та знизу, використовуючи скорочення з двома значеннями.",
|
||||
"previewHTML": "<button class=\"btn\">Follow</button>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -116,7 +116,7 @@
|
||||
"id": "box-model-6",
|
||||
"title": "Margin Shorthand",
|
||||
"description": "Margin використовує той самий шаблон скорочення, що й padding. Поширений шаблон - горизонтальне центрування блокових елементів за допомогою <kbd>margin: 0 auto</kbd>.",
|
||||
"task": "Відцентруйте цю картку горизонтально. Встановіть <kbd>margin: 0 auto</kbd>, щоб автоматично обчислити рівні ліві/праві поля.",
|
||||
"task": "Ця картка приліпла до лівого краю. Відцентруйте її горизонтально, використовуючи скорочення полів з автоматичними бічними полями.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -138,7 +138,7 @@
|
||||
"id": "box-model-7",
|
||||
"title": "Border Radius",
|
||||
"description": "Хоча не є частиною класичної блокової моделі, <kbd>border-radius</kbd> заокруглює кути межі елемента. Використовуйте <kbd>50%</kbd> на квадратному елементі, щоб створити коло.",
|
||||
"task": "Зробіть зображення аватара круглим за допомогою <kbd>border-radius: 50%</kbd>.",
|
||||
"task": "Квадратне зображення аватара має виглядати як ідеальне коло.",
|
||||
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -159,7 +159,7 @@
|
||||
"id": "box-model-8",
|
||||
"title": "Complete Card",
|
||||
"description": "Об'єднаймо все разом. Ця картка сповіщення потребує стилізації, щоб виглядати професійно.",
|
||||
"task": "Стилізуйте сповіщення: додайте <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> та <kbd>border-radius: 4px</kbd>.",
|
||||
"task": "Це сповіщення потребує трьох речей: внутрішнього простору, щоб текст не був стиснутий, кольорового акценту на лівому краю та злегка заокруглених кутів.",
|
||||
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
@@ -208,9 +209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
|
||||
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["css", "tailwind", "html", "markdown"],
|
||||
"description": "Whether this module teaches CSS, Tailwind, HTML, or Markdown"
|
||||
"enum": ["css", "tailwind", "html", "markdown", "javascript"],
|
||||
"description": "Whether this module teaches CSS, Tailwind, HTML, Markdown, or JavaScript"
|
||||
},
|
||||
"difficulty": {
|
||||
"type": "string",
|
||||
@@ -60,7 +60,7 @@
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["css", "tailwind", "html", "markdown"],
|
||||
"enum": ["css", "tailwind", "html", "markdown", "javascript"],
|
||||
"description": "Override module mode for individual lessons"
|
||||
},
|
||||
"tailwindConfig": {
|
||||
|
||||
106
specs/009-colors-boxmodel-tasks/plan.md
Normal file
106
specs/009-colors-boxmodel-tasks/plan.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Implementation Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Rewrite task descriptions in the Colors (4 lessons) and Box Model (8 lessons x 6 locales) modules so they describe desired visual outcomes rather than giving exact CSS declarations. For colors, also update validations to accept multiple valid color values.
|
||||
|
||||
## Approach
|
||||
|
||||
This follows the same pattern as the flexbox fix (PR #5). Two types of changes:
|
||||
|
||||
1. **Colors module**: Rewrite tasks AND update validations from `property_value` (single answer) to `regex` (multiple valid colors). This is because the issue explicitly says "accept multiple valid solutions" and colors naturally have many equivalent options.
|
||||
2. **Box Model module**: Rewrite tasks only. Validation messages already use pedagogical hints. Box model properties have specific correct answers (e.g., `box-sizing: border-box` has no alternative), so validations stay as-is.
|
||||
|
||||
## File Mapping
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `lessons/03-colors.json` | modify | Rewrite 4 tasks + change 4 validations from `property_value` to `regex` |
|
||||
| `lessons/01-box-model.json` | modify | Rewrite 8 task fields |
|
||||
| `lessons/ar/01-box-model.json` | modify | Rewrite 8 task fields (Arabic) |
|
||||
| `lessons/de/01-box-model.json` | modify | Rewrite 8 task fields (German) |
|
||||
| `lessons/es/01-box-model.json` | modify | Rewrite 8 task fields (Spanish) |
|
||||
| `lessons/pl/01-box-model.json` | modify | Rewrite 8 task fields (Polish) |
|
||||
| `lessons/uk/01-box-model.json` | modify | Rewrite 8 task fields (Ukrainian) |
|
||||
|
||||
No validator code changes needed — existing `regex` type already supports multi-value patterns.
|
||||
|
||||
## Detailed Changes
|
||||
|
||||
### Colors Module
|
||||
|
||||
#### colors-1 (Background Color)
|
||||
- **Task**: Describe that notification card looks bare, needs a soft warm background
|
||||
- **Validation**: Change from `property_value` (seashell only) to `regex` accepting warm named colors (seashell, linen, mistyrose, lavenderblush, cornsilk, oldlace, papayawhip, antiquewhite, bisque, peachpuff)
|
||||
- **Message**: Hint at background-color property
|
||||
|
||||
#### colors-2 (Text Color)
|
||||
- **Task**: Describe that title needs to pop with a warm accent color
|
||||
- **Validation**: Change from `property_value` (coral only) to `regex` accepting warm accent colors (coral, tomato, orangered, indianred, salmon, darksalmon)
|
||||
- **Message**: Hint at color property
|
||||
|
||||
#### colors-3 (Border Color)
|
||||
- **Task**: Describe that card border needs a warm accent color
|
||||
- **Validation**: Change from `property_value` (coral only) to `regex` accepting warm accent colors (coral, tomato, orangered, indianred, salmon, darksalmon, crimson)
|
||||
- **Message**: Hint at border-color property
|
||||
|
||||
#### colors-4 (Hex Colors)
|
||||
- **Task**: Describe wanting a gold/yellow badge background, mentioning hex format since that's the lesson's teaching point
|
||||
- **Validation**: Change from `property_value` (#ffd700 only) to `regex` accepting gold hex variants (#ffd700, #ffcc00, #ffc107, #f0c000) and also the named color `gold`
|
||||
- **Message**: Hint at using a hex code for background-color
|
||||
|
||||
### Box Model Module (per-lesson, applied across all 6 locales)
|
||||
|
||||
#### box-model-1 (Padding)
|
||||
- **Current**: "Add `padding: 1rem`..."
|
||||
- **New**: Describe that text is pressed against the edges and needs inner breathing room
|
||||
|
||||
#### box-model-2 (Borders)
|
||||
- **Current**: "Add `border-left: 4px solid steelblue`"
|
||||
- **New**: Describe wanting a colored accent line on the left side of the card
|
||||
|
||||
#### box-model-3 (Margins)
|
||||
- **Current**: "Add `margin-bottom: 1rem`"
|
||||
- **New**: Describe that the two cards are touching and need space between them
|
||||
|
||||
#### box-model-4 (Box Sizing)
|
||||
- **Current**: "Fix with `box-sizing: border-box`"
|
||||
- **New**: Describe the visual problem (right card overflows) and ask to fix its sizing model
|
||||
|
||||
#### box-model-5 (Padding Shorthand)
|
||||
- **Current**: "Set `padding: 8px 1rem`"
|
||||
- **New**: Describe the button needing more horizontal than vertical space, mention the two-value shorthand concept
|
||||
|
||||
#### box-model-6 (Margin Shorthand)
|
||||
- **Current**: "Set `margin: 0 auto`"
|
||||
- **New**: Describe the card being left-aligned and needing to be horizontally centered
|
||||
|
||||
#### box-model-7 (Border Radius)
|
||||
- **Current**: "Make with `border-radius: 50%`"
|
||||
- **New**: Describe the square avatar needing to appear as a circle
|
||||
|
||||
#### box-model-8 (Complete Card)
|
||||
- **Current**: Lists all 3 exact property declarations
|
||||
- **New**: Describe three visual goals: inner spacing, left accent line, softened corners
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
1. **No validator code changes**: The existing `regex` validation type handles multi-value matching.
|
||||
2. **Colors get multi-value validations**: Colors naturally have equivalents (coral vs tomato). Accept a curated set of named colors per lesson.
|
||||
3. **Box model keeps exact validations**: Properties like `padding: 1rem` or `box-sizing: border-box` have only one correct answer. The task text changes are sufficient.
|
||||
4. **Solution fields unchanged**: The `solution` field shows the canonical answer and is unrelated to copy-paste behavior.
|
||||
5. **codePrefix unchanged**: Already shows the selector context.
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|-----------|------------|
|
||||
| Color regex too permissive/restrictive | Medium | Curate a small set of 6-10 named colors per lesson that visually work in the preview |
|
||||
| Locale translations lose nuance | Low | Follow the same structure: describe the visual outcome in each language |
|
||||
| Box model tasks become too vague | Low | Keep mentioning the visual problem — students have the description for concept reference |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. Run `npm run test` — all existing tests should pass
|
||||
2. Run `npm run format.lessons` — ensure JSON files are properly formatted
|
||||
3. Verify JSON schema conformance for all modified files
|
||||
50
specs/009-colors-boxmodel-tasks/spec.md
Normal file
50
specs/009-colors-boxmodel-tasks/spec.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# fix: rewrite colors and box-model task descriptions to remove copy-paste answers
|
||||
|
||||
**Issue**: [libretech/code-crispies#9](https://git.librete.ch/libretech/code-crispies/issues/9)
|
||||
**State**: open
|
||||
**Author**: libretech
|
||||
**Labels**: none
|
||||
**Complexity**: medium
|
||||
|
||||
## Issue Body
|
||||
|
||||
Pedagogy audit Runde 3: Colors (copy-paste 90%) and Box Model (copy-paste 85%) are the next worst modules after flexbox was fixed. Same pattern — task says 'Add background-color: coral' and student just types it. Rewrite to describe desired outcome: 'The card background should be a warm color.' Accept multiple valid solutions.
|
||||
|
||||
## Current State
|
||||
|
||||
### Colors Module (`lessons/03-colors.json`) — English only, 4 lessons
|
||||
|
||||
| Lesson | Current Task (gives away answer) |
|
||||
|--------|----------------------------------|
|
||||
| colors-1 | "Add `background-color: seashell`" |
|
||||
| colors-2 | "Add `color: coral`" |
|
||||
| colors-3 | "Add `border-color: coral`" |
|
||||
| colors-4 | "Add `background-color: #ffd700`" |
|
||||
|
||||
All 4 validations use `property_value` with exact expected values — only one answer accepted.
|
||||
|
||||
### Box Model Module (`lessons/01-box-model.json`) — 6 locales (en, ar, de, es, pl, uk), 8 lessons
|
||||
|
||||
| Lesson | Current Task (gives away answer) |
|
||||
|--------|----------------------------------|
|
||||
| box-model-1 | "Add `padding: 1rem`" |
|
||||
| box-model-2 | "Add `border-left: 4px solid steelblue`" |
|
||||
| box-model-3 | "Add `margin-bottom: 1rem`" |
|
||||
| box-model-4 | "Fix with `box-sizing: border-box`" |
|
||||
| box-model-5 | "Set `padding: 8px 1rem`" |
|
||||
| box-model-6 | "Set `margin: 0 auto`" |
|
||||
| box-model-7 | "Make with `border-radius: 50%`" |
|
||||
| box-model-8 | Lists all 3 properties verbatim |
|
||||
|
||||
Box model validation messages are already well-written (hint without revealing). The `task` fields contain `<kbd>` tags with exact answers.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. All 4 colors task descriptions rewritten to describe desired visual outcomes
|
||||
2. All 8 box-model task descriptions rewritten to describe desired visual outcomes
|
||||
3. Students cannot copy-paste from the task into the editor to pass
|
||||
4. Colors validations accept multiple valid CSS color values where appropriate
|
||||
5. Box-model validation messages remain as-is (already hint without revealing)
|
||||
6. All 5 localized box-model files updated to match the English rewrite pattern
|
||||
7. Existing tests continue to pass
|
||||
8. Lesson descriptions (which teach the concepts) remain unchanged
|
||||
22
specs/009-colors-boxmodel-tasks/tasks.md
Normal file
22
specs/009-colors-boxmodel-tasks/tasks.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Tasks
|
||||
|
||||
## Phase 1: Colors Module
|
||||
- [X] Task 1.1: Rewrite task text for all 4 colors lessons to describe visual outcomes
|
||||
- [X] Task 1.2: Change colors validations from property_value to regex accepting multiple valid color names
|
||||
- [X] Task 1.3: Update colors validation error messages to hint without revealing answers
|
||||
|
||||
## Phase 2: Box Model Module (English)
|
||||
- [X] Task 2.1: Rewrite task text for all 8 box-model lessons to describe visual outcomes
|
||||
- [X] Task 2.2: Review box-model validation messages (already good, update only if needed)
|
||||
|
||||
## Phase 3: Box Model Translations [P]
|
||||
- [X] Task 3.1: Rewrite task text in Arabic (ar/01-box-model.json) [P]
|
||||
- [X] Task 3.2: Rewrite task text in German (de/01-box-model.json) [P]
|
||||
- [X] Task 3.3: Rewrite task text in Spanish (es/01-box-model.json) [P]
|
||||
- [X] Task 3.4: Rewrite task text in Polish (pl/01-box-model.json) [P]
|
||||
- [X] Task 3.5: Rewrite task text in Ukrainian (uk/01-box-model.json) [P]
|
||||
|
||||
## Phase 4: Validation
|
||||
- [X] Task 4.1: Run existing test suite to confirm no regressions
|
||||
- [X] Task 4.2: Run lesson format check (npm run format.lessons)
|
||||
- [X] Task 4.3: Verify all modified JSON files conform to module schema
|
||||
67
src/app.js
67
src/app.js
@@ -578,6 +578,11 @@ function updateEditorForMode(mode) {
|
||||
label: "Markdown Editor",
|
||||
cmMode: "markdown"
|
||||
},
|
||||
javascript: {
|
||||
placeholder: "// Write your JavaScript here...",
|
||||
label: "JavaScript Editor",
|
||||
cmMode: "javascript"
|
||||
},
|
||||
playground: {
|
||||
placeholder: "<style>\n /* CSS here */\n</style>\n\n<!-- HTML here -->",
|
||||
label: "HTML & CSS",
|
||||
@@ -1493,6 +1498,64 @@ This is \`inline code\`.</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
javascript: `
|
||||
<div class="section-overview">
|
||||
<p><strong>JavaScript</strong> is the programming language of the web. It adds interactivity to HTML pages—responding to clicks, updating content dynamically, validating forms, and much more. Every modern browser includes a JavaScript engine, making it the most widely deployed programming language in the world.</p>
|
||||
<p>These beginner lessons cover the fundamentals: declaring variables, selecting and modifying DOM elements, and handling user events. Each concept builds on the previous one, giving you the tools to make any web page interactive.</p>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>Variables & Data Types</h2>
|
||||
<p>JavaScript uses <code>const</code> for values that won't change and <code>let</code> for values that will. Template literals with backticks make it easy to embed expressions in strings using <code>\${...}</code> syntax.</p>
|
||||
<p>Arrays store ordered collections in square brackets. Objects store key-value pairs in curly braces. These are the building blocks of every JavaScript program.</p>
|
||||
<a href="#js-variables/0" class="topic-link">Learn JS Variables</a>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>const name = "Alice";
|
||||
let count = 0;
|
||||
count = count + 1;
|
||||
|
||||
const msg = \`Hello, \${name}!\`;
|
||||
const colors = ["red", "green"];</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>DOM Manipulation</h2>
|
||||
<p>The DOM (Document Object Model) is how JavaScript sees your HTML. Use <code>document.querySelector()</code> to find elements by CSS selector, then modify them with properties like <code>textContent</code>, <code>style</code>, and <code>classList</code>.</p>
|
||||
<a href="#js-dom/0" class="topic-link">Practice DOM Methods</a>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>const title = document.querySelector('h1');
|
||||
title.textContent = "New Title";
|
||||
title.style.color = "coral";
|
||||
title.classList.add("active");</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topic-row">
|
||||
<div class="topic-text">
|
||||
<h2>Event Handling</h2>
|
||||
<p>Events let your code respond to user actions. Use <code>addEventListener()</code> to run a function when something happens—a click, a keystroke, or an input change. The callback receives an event object with details about what happened.</p>
|
||||
<a href="#js-events/0" class="topic-link">Handle Events</a>
|
||||
</div>
|
||||
<div class="topic-code">
|
||||
<div class="code-block">
|
||||
<pre><code>const btn = document.querySelector('.btn');
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
alert('Clicked!');
|
||||
});</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
@@ -2310,7 +2373,7 @@ function showLandingPage() {
|
||||
*/
|
||||
function renderFooterLessonLinks() {
|
||||
const modules = lessonEngine.modules || [];
|
||||
const sectionGroups = { css: [], html: [] };
|
||||
const sectionGroups = { css: [], html: [], javascript: [] };
|
||||
|
||||
modules.forEach((module) => {
|
||||
if (module.excludeFromProgress) return;
|
||||
@@ -2347,7 +2410,7 @@ function renderFooterLessonLinks() {
|
||||
* Update progress indicators on landing page
|
||||
*/
|
||||
function updateLandingProgress() {
|
||||
["css", "html", "markdown"].forEach((sectionId) => { // tailwind temporarily disabled
|
||||
["css", "html", "markdown", "javascript"].forEach((sectionId) => { // tailwind temporarily disabled
|
||||
const progressEl = document.getElementById(`${sectionId}-progress`);
|
||||
if (progressEl) {
|
||||
const sectionModules = getModulesBySection(lessonEngine.modules, sectionId);
|
||||
|
||||
@@ -31,6 +31,9 @@ import filtersEN from "../../lessons/11-filters.json";
|
||||
import positioningEN from "../../lessons/12-positioning.json";
|
||||
import pseudoElementsEN from "../../lessons/13-pseudo-elements.json";
|
||||
import markdownBasicsEN from "../../lessons/40-markdown-basics.json";
|
||||
import jsVariablesEN from "../../lessons/50-js-variables.json";
|
||||
import jsDomEN from "../../lessons/51-js-dom.json";
|
||||
import jsEventsEN from "../../lessons/52-js-events.json";
|
||||
import playgroundEN from "../../lessons/98-playground.json";
|
||||
import goodbyeEN from "../../lessons/99-goodbye.json";
|
||||
|
||||
@@ -165,6 +168,10 @@ const moduleStoreEN = [
|
||||
htmlTablesEN,
|
||||
// Markdown
|
||||
markdownBasicsEN,
|
||||
// JavaScript
|
||||
jsVariablesEN,
|
||||
jsDomEN,
|
||||
jsEventsEN,
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -206,6 +213,10 @@ const moduleStoreDE = [
|
||||
htmlTablesDE,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -247,6 +258,10 @@ const moduleStorePL = [
|
||||
htmlTablesPL,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -288,6 +303,10 @@ const moduleStoreES = [
|
||||
htmlTablesES,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -329,6 +348,10 @@ const moduleStoreAR = [
|
||||
htmlTablesAR,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
@@ -370,6 +393,10 @@ const moduleStoreUK = [
|
||||
htmlTablesUK,
|
||||
// Markdown
|
||||
markdownBasicsEN, // Using EN fallback until translated
|
||||
// JavaScript
|
||||
jsVariablesEN, // Using EN fallback until translated
|
||||
jsDomEN, // Using EN fallback until translated
|
||||
jsEventsEN, // Using EN fallback until translated
|
||||
// Outro
|
||||
goodbyeEN,
|
||||
playgroundEN
|
||||
|
||||
@@ -31,6 +31,13 @@ export const sections = {
|
||||
description: "Lightweight markup language for formatting text",
|
||||
color: "#5b8dd9",
|
||||
order: 4
|
||||
},
|
||||
javascript: {
|
||||
id: "javascript",
|
||||
title: "JavaScript",
|
||||
description: "Interactive scripting for web pages",
|
||||
color: "#f0db4f",
|
||||
order: 5
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,6 +72,7 @@ export function getModuleSection(module) {
|
||||
if (mode === "html") return "html";
|
||||
if (mode === "tailwind") return "tailwind";
|
||||
if (mode === "markdown") return "markdown";
|
||||
if (mode === "javascript") return "javascript";
|
||||
return "css";
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Renderer - Handles UI updates for the CSS learning platform
|
||||
*/
|
||||
import { t } from "../i18n.js";
|
||||
import { getModuleSection, getSection, getSectionList } from "../config/sections.js";
|
||||
|
||||
/**
|
||||
* Compute lesson difficulty based on lesson structure
|
||||
@@ -72,8 +73,24 @@ export function renderModuleList(container, modules, onSelectModule, onSelectLes
|
||||
}
|
||||
}
|
||||
|
||||
// Group modules by section for headers
|
||||
let currentSectionId = null;
|
||||
|
||||
// Create list items for each module
|
||||
modules.forEach((module) => {
|
||||
// Insert section header when section changes
|
||||
const sectionId = getModuleSection(module);
|
||||
if (sectionId !== currentSectionId && !module.excludeFromProgress) {
|
||||
currentSectionId = sectionId;
|
||||
const section = getSection(sectionId);
|
||||
if (section) {
|
||||
const header = document.createElement("h3");
|
||||
header.className = "sidebar-section-header";
|
||||
header.textContent = section.title;
|
||||
header.style.borderLeftColor = section.color;
|
||||
container.appendChild(header);
|
||||
}
|
||||
}
|
||||
// Create module container
|
||||
// Use native <details>/<summary> for expand/collapse
|
||||
const moduleContainer = document.createElement("details");
|
||||
|
||||
@@ -27,7 +27,7 @@ export const RouteType = {
|
||||
/**
|
||||
* Valid section IDs
|
||||
*/
|
||||
const SECTIONS = ["css", "html", "markdown"]; // tailwind temporarily disabled
|
||||
const SECTIONS = ["css", "html", "markdown", "javascript"]; // tailwind temporarily disabled
|
||||
|
||||
/**
|
||||
* Valid language codes for URL-based switching
|
||||
|
||||
@@ -10,6 +10,8 @@ export function validateUserCode(userCode, lesson) {
|
||||
return validateHtmlCode(userCode, lesson);
|
||||
case "tailwind":
|
||||
return validateTailwindClasses(userCode, lesson);
|
||||
case "javascript":
|
||||
return validateJavaScriptCode(userCode, lesson);
|
||||
case "css":
|
||||
default:
|
||||
return validateCssCode(userCode, lesson);
|
||||
@@ -204,6 +206,80 @@ function validateHtmlCode(userHtml, lesson) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user JavaScript code against the lesson requirements
|
||||
* @param {string} userCode - User submitted JavaScript code
|
||||
* @param {Object} lesson - The current lesson object
|
||||
* @returns {Object} Validation result with isValid and message properties
|
||||
*/
|
||||
function validateJavaScriptCode(userCode, lesson) {
|
||||
if (!lesson || !lesson.validations) {
|
||||
return { isValid: true, message: "No validations specified for this lesson." };
|
||||
}
|
||||
|
||||
const validations = lesson.validations;
|
||||
|
||||
let result = {
|
||||
isValid: true,
|
||||
validCases: 0,
|
||||
totalCases: validations.length,
|
||||
message: "Your CODE looks CRISPY!"
|
||||
};
|
||||
|
||||
for (const validation of validations) {
|
||||
const { type, value, message, options } = validation;
|
||||
let validationPassed = false;
|
||||
|
||||
switch (type) {
|
||||
case "contains":
|
||||
validationPassed = containsValidation(userCode, value, options);
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `Your code should include "${value}".`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "not_contains":
|
||||
validationPassed = !containsValidation(userCode, value, options);
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || `Your code should not include "${value}".`
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "regex":
|
||||
validationPassed = regexValidation(userCode, value, options);
|
||||
if (!validationPassed) {
|
||||
result = {
|
||||
...result,
|
||||
isValid: false,
|
||||
message: message || "Your code does not match the expected pattern."
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown JavaScript validation type: ${type}`);
|
||||
validationPassed = true;
|
||||
}
|
||||
|
||||
if (validationPassed) {
|
||||
result.validCases++;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.validCases = validations.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateTailwindClasses(userClasses, lesson) {
|
||||
if (!lesson || !lesson.validations) {
|
||||
return { isValid: true, message: "No validations specified for this lesson." };
|
||||
|
||||
@@ -8,6 +8,7 @@ import { history } from "@codemirror/commands";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { autocompletion } from "@codemirror/autocomplete";
|
||||
import { abbreviationTracker, expandAbbreviation } from "@emmetio/codemirror6-plugin";
|
||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
@@ -181,7 +182,8 @@ export class CodeEditor {
|
||||
const fullDoc = prefix + initialValue + suffix;
|
||||
|
||||
// Get language extension based on mode
|
||||
const langExtension = this.mode === "html" ? html() : this.mode === "markdown" ? markdown() : css();
|
||||
const langExtension =
|
||||
this.mode === "html" ? html() : this.mode === "javascript" ? javascript() : this.mode === "markdown" ? markdown() : css();
|
||||
|
||||
// Create read-only zones decorations
|
||||
const readOnlyMark = Decoration.mark({ class: "cm-readonly-zone" });
|
||||
|
||||
@@ -256,6 +256,30 @@ export class LessonEngine {
|
||||
${htmlWithClasses}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (mode === "javascript") {
|
||||
// For JavaScript mode, user code runs as a script against previewHTML
|
||||
const { codePrefix, codeSuffix } = this.currentLesson;
|
||||
const fullScript = `${codePrefix || ""}${this.userCode || ""}${codeSuffix || ""}`;
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>html, body { min-height: 100%; margin: 0; }</style>
|
||||
<style>${previewBaseCSS || ""}</style>
|
||||
<style>${sandboxCSS || ""}</style>
|
||||
</head>
|
||||
<body>
|
||||
${previewHTML || ""}
|
||||
<script>
|
||||
try {
|
||||
${fullScript}
|
||||
} catch (e) {
|
||||
document.body.innerHTML += '<pre style="color:red">' + e.message + '</pre>';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (mode === "markdown") {
|
||||
// For Markdown mode, parse user code to HTML
|
||||
@@ -382,6 +406,30 @@ export class LessonEngine {
|
||||
${htmlWithClasses}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (mode === "javascript") {
|
||||
// For JavaScript mode, solution code runs as a script against previewHTML
|
||||
const { codePrefix, codeSuffix } = this.currentLesson;
|
||||
const fullScript = `${codePrefix || ""}${solutionCode}${codeSuffix || ""}`;
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>html, body { min-height: 100%; margin: 0; }</style>
|
||||
<style>${previewBaseCSS || ""}</style>
|
||||
<style>${sandboxCSS || ""}</style>
|
||||
</head>
|
||||
<body>
|
||||
${previewHTML || ""}
|
||||
<script>
|
||||
try {
|
||||
${fullScript}
|
||||
} catch (e) {
|
||||
document.body.innerHTML += '<pre style="color:red">' + e.message + '</pre>';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else if (mode === "markdown") {
|
||||
// For Markdown mode, parse solution to HTML
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
<a href="#html" class="nav-link" data-section="html">HTML</a>
|
||||
<!-- <a href="#tailwind" class="nav-link" data-section="tailwind">Tailwind</a> -->
|
||||
<a href="#markdown" class="nav-link" data-section="markdown">Markdown</a>
|
||||
<a href="#javascript" class="nav-link" data-section="javascript">JavaScript</a>
|
||||
<a href="#reference/css" class="nav-link nav-link-ref" data-section="reference">Reference</a>
|
||||
</nav>
|
||||
<button id="auth-trigger-header" class="btn btn-outline btn-sm" data-i18n="authLogin">Log In</button>
|
||||
@@ -178,6 +179,12 @@
|
||||
<p data-i18n="landingMarkdownDesc">Lightweight markup for formatting text</p>
|
||||
<span class="section-card-progress" id="markdown-progress"></span>
|
||||
</a>
|
||||
<a href="#javascript" class="section-card" data-section="javascript">
|
||||
<div class="section-card-icon" style="background: #f0db4f; color: #333">JS</div>
|
||||
<h3>JavaScript</h3>
|
||||
<p data-i18n="landingJsDesc">Interactive scripting for web pages</p>
|
||||
<span class="section-card-progress" id="javascript-progress"></span>
|
||||
</a>
|
||||
</div>
|
||||
<p class="device-notice" data-i18n-html="deviceNotice">
|
||||
<strong>Best on desktop or tablet (landscape).</strong> Mobile works, but larger screens make coding easier.
|
||||
@@ -194,13 +201,6 @@
|
||||
<h3 data-i18n="comingSoonAchievementsTitle">Achievements</h3>
|
||||
<p data-i18n="comingSoonAchievementsText">Earn badges as you master new skills. Track your learning milestones.</p>
|
||||
</article>
|
||||
<article class="coming-soon-card">
|
||||
<span class="coming-soon-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||||
</span>
|
||||
<h3 data-i18n="comingSoonJsTitle">JavaScript</h3>
|
||||
<p data-i18n="comingSoonJsText">Interactive JavaScript lessons with live code execution and DOM manipulation.</p>
|
||||
</article>
|
||||
<article class="coming-soon-card">
|
||||
<span class="coming-soon-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
@@ -478,6 +478,7 @@
|
||||
<a href="#css" class="sidebar-nav-link" data-section="css">CSS</a>
|
||||
<a href="#html" class="sidebar-nav-link" data-section="html">HTML</a>
|
||||
<!-- <a href="#tailwind" class="sidebar-nav-link" data-section="tailwind">Tailwind</a> -->
|
||||
<a href="#javascript" class="sidebar-nav-link" data-section="javascript">JavaScript</a>
|
||||
<button id="auth-trigger-mobile" class="sidebar-nav-link sidebar-auth-link" data-i18n="authLogin">Log In</button>
|
||||
</nav>
|
||||
|
||||
|
||||
106
src/main.css
106
src/main.css
@@ -291,6 +291,14 @@ kbd {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .logo h1 .code-text {
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .logo h1 .crispies-text {
|
||||
background: #d4a017;
|
||||
}
|
||||
|
||||
.help-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@@ -1244,6 +1252,22 @@ nav.sidebar-section:not(.sidebar-nav-mobile) {
|
||||
animation: milestone-pop 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Sidebar section grouping headers */
|
||||
.sidebar-section-header {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--light-text);
|
||||
padding: 0.75rem 0.75rem 0.25rem;
|
||||
margin: 0.5rem 0 0;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.sidebar-section-header:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Module List in Sidebar */
|
||||
.module-list {
|
||||
/* No max-height - parent nav.sidebar-section handles overflow */
|
||||
@@ -3618,6 +3642,14 @@ input:checked + .toggle-slider::before {
|
||||
--section-color-rgb: 91, 141, 217;
|
||||
}
|
||||
|
||||
/* JavaScript Section - Gold */
|
||||
[data-section="javascript"] {
|
||||
--section-color: #d4a017;
|
||||
--section-color-light: #e0b840;
|
||||
--section-color-dark: #b08610;
|
||||
--section-color-rgb: 212, 160, 23;
|
||||
}
|
||||
|
||||
/* Apply section colors to nav links */
|
||||
.nav-link[data-section="css"] {
|
||||
color: #d95a8a;
|
||||
@@ -3635,6 +3667,10 @@ input:checked + .toggle-slider::before {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
.nav-link[data-section="javascript"] {
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
.nav-link[data-section="css"]:hover,
|
||||
.nav-link[data-section="css"].active {
|
||||
background: rgba(217, 90, 138, 0.1);
|
||||
@@ -3659,6 +3695,12 @@ input:checked + .toggle-slider::before {
|
||||
color: #4070b8;
|
||||
}
|
||||
|
||||
.nav-link[data-section="javascript"]:hover,
|
||||
.nav-link[data-section="javascript"].active {
|
||||
background: rgba(212, 160, 23, 0.1);
|
||||
color: #b08610;
|
||||
}
|
||||
|
||||
/* Hint section colors */
|
||||
body[data-section="css"] .hint {
|
||||
background: rgba(217, 90, 138, 0.3);
|
||||
@@ -3696,6 +3738,15 @@ body[data-section="markdown"] .hint-progress {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .hint {
|
||||
background: rgba(212, 160, 23, 0.3);
|
||||
border-left-color: #e0b840;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .hint-progress {
|
||||
background: #d4a017;
|
||||
}
|
||||
|
||||
/* RTL hint border */
|
||||
[dir="rtl"] body[data-section="css"] .hint {
|
||||
border-right-color: #a98cd6;
|
||||
@@ -3713,6 +3764,10 @@ body[data-section="markdown"] .hint-progress {
|
||||
border-right-color: #7ba3e5;
|
||||
}
|
||||
|
||||
[dir="rtl"] body[data-section="javascript"] .hint {
|
||||
border-right-color: #e0b840;
|
||||
}
|
||||
|
||||
/* Reference nav link colors */
|
||||
.ref-nav-link[data-ref="css"],
|
||||
.ref-nav-link[data-ref="selectors"],
|
||||
@@ -3816,6 +3871,24 @@ body[data-section="markdown"] .cm-editor .cm-activeLine {
|
||||
background-color: rgba(91, 141, 217, 0.08) !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-content {
|
||||
caret-color: #d4a017 !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-cursor,
|
||||
body[data-section="javascript"] .cm-editor .cm-dropCursor {
|
||||
border-left-color: #d4a017 !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-selectionBackground,
|
||||
body[data-section="javascript"] .cm-editor .cm-content ::selection {
|
||||
background-color: rgba(212, 160, 23, 0.25) !important;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .cm-editor .cm-activeLine {
|
||||
background-color: rgba(212, 160, 23, 0.08) !important;
|
||||
}
|
||||
|
||||
/* Module pill section colors */
|
||||
body[data-section="css"] .module-pill {
|
||||
background: rgba(217, 90, 138, 0.1);
|
||||
@@ -3853,6 +3926,15 @@ body[data-section="markdown"] .module-pill .level-indicator {
|
||||
color: #4070b8;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .module-pill {
|
||||
background: rgba(212, 160, 23, 0.1);
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .module-pill .level-indicator {
|
||||
color: #b08610;
|
||||
}
|
||||
|
||||
/* Code block border section colors */
|
||||
body[data-section="css"] .code-block {
|
||||
border-color: rgba(217, 90, 138, 0.4);
|
||||
@@ -3870,6 +3952,10 @@ body[data-section="markdown"] .code-block {
|
||||
border-color: rgba(91, 141, 217, 0.4);
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .code-block {
|
||||
border-color: rgba(212, 160, 23, 0.4);
|
||||
}
|
||||
|
||||
/* Section code block CodeMirror syntax highlighting overrides */
|
||||
body[data-section="css"] .code-block .cm-editor .cm-line {
|
||||
color: #c9c0e0;
|
||||
@@ -3887,6 +3973,10 @@ body[data-section="markdown"] .code-block .cm-editor .cm-line {
|
||||
color: #c0d0e8;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .code-block .cm-editor .cm-line {
|
||||
color: #e0d8b0;
|
||||
}
|
||||
|
||||
/* Task instruction bubble section colors */
|
||||
[data-section="css"] .task-instruction {
|
||||
background: rgba(217, 90, 138, 0.92);
|
||||
@@ -3904,6 +3994,10 @@ body[data-section="markdown"] .code-block .cm-editor .cm-line {
|
||||
background: rgba(91, 141, 217, 0.92);
|
||||
}
|
||||
|
||||
[data-section="javascript"] .task-instruction {
|
||||
background: rgba(212, 160, 23, 0.92);
|
||||
}
|
||||
|
||||
/* Section page progress bar colors */
|
||||
body[data-section="css"] .section-progress-bar .progress-fill {
|
||||
background: #d95a8a;
|
||||
@@ -3921,6 +4015,10 @@ body[data-section="markdown"] .section-progress-bar .progress-fill {
|
||||
background: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] .section-progress-bar .progress-fill {
|
||||
background: #d4a017;
|
||||
}
|
||||
|
||||
/* Section page header colors */
|
||||
[data-section="css"] .section-hero h1 {
|
||||
color: #d95a8a;
|
||||
@@ -3938,6 +4036,10 @@ body[data-section="markdown"] .section-progress-bar .progress-fill {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
[data-section="javascript"] .section-hero h1 {
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
/* Lesson title h2 section colors */
|
||||
body[data-section="css"] #lesson-title {
|
||||
color: #d95a8a;
|
||||
@@ -3955,6 +4057,10 @@ body[data-section="markdown"] #lesson-title {
|
||||
color: #5b8dd9;
|
||||
}
|
||||
|
||||
body[data-section="javascript"] #lesson-title {
|
||||
color: #d4a017;
|
||||
}
|
||||
|
||||
/* Section and Reference footer - override landing-footer styles */
|
||||
.section-footer.landing-footer,
|
||||
.reference-footer.landing-footer {
|
||||
|
||||
@@ -19,6 +19,10 @@ describe("Lessons Config Module", () => {
|
||||
expect(moduleIds).toContain("css-basic-selectors");
|
||||
expect(moduleIds).toContain("box-model");
|
||||
expect(moduleIds).toContain("flexbox");
|
||||
// JavaScript modules
|
||||
expect(moduleIds).toContain("js-variables");
|
||||
expect(moduleIds).toContain("js-dom");
|
||||
expect(moduleIds).toContain("js-events");
|
||||
});
|
||||
|
||||
test("should have mode set on each lesson", async () => {
|
||||
@@ -27,7 +31,7 @@ describe("Lessons Config Module", () => {
|
||||
modules.forEach((module) => {
|
||||
module.lessons.forEach((lesson) => {
|
||||
expect(lesson.mode).toBeDefined();
|
||||
expect(["html", "css", "tailwind", "markdown", "playground"]).toContain(lesson.mode);
|
||||
expect(["html", "css", "tailwind", "markdown", "javascript", "playground"]).toContain(lesson.mode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,7 +56,8 @@ describe("Router", () => {
|
||||
test.each([
|
||||
["css", "css"],
|
||||
["html", "html"],
|
||||
["markdown", "markdown"]
|
||||
["markdown", "markdown"],
|
||||
["javascript", "javascript"]
|
||||
])("parseHash_SectionId_%s_ReturnsSectionRoute", (sectionId, expectedId) => {
|
||||
window.location.hash = `#${sectionId}`;
|
||||
const result = parseHash();
|
||||
@@ -220,6 +221,7 @@ describe("Router", () => {
|
||||
expect(ids).toContain("css");
|
||||
expect(ids).toContain("html");
|
||||
expect(ids).toContain("markdown");
|
||||
expect(ids).toContain("javascript");
|
||||
});
|
||||
|
||||
test("getSectionIds_MutatingCopy_DoesNotAffectOriginal", () => {
|
||||
|
||||
@@ -3,12 +3,13 @@ import { sections, getSection, getSectionList, getModuleSection, getModulesBySec
|
||||
|
||||
describe("Sections Config", () => {
|
||||
describe("sections constant", () => {
|
||||
test("sections_AllDefined_HasFourSections", () => {
|
||||
expect(Object.keys(sections)).toHaveLength(4);
|
||||
test("sections_AllDefined_HasFiveSections", () => {
|
||||
expect(Object.keys(sections)).toHaveLength(5);
|
||||
expect(sections).toHaveProperty("css");
|
||||
expect(sections).toHaveProperty("html");
|
||||
expect(sections).toHaveProperty("tailwind");
|
||||
expect(sections).toHaveProperty("markdown");
|
||||
expect(sections).toHaveProperty("javascript");
|
||||
});
|
||||
|
||||
test("sections_EachSection_HasRequiredFields", () => {
|
||||
@@ -27,7 +28,8 @@ describe("Sections Config", () => {
|
||||
["css", "CSS"],
|
||||
["html", "HTML"],
|
||||
["tailwind", "Tailwind CSS"],
|
||||
["markdown", "Markdown"]
|
||||
["markdown", "Markdown"],
|
||||
["javascript", "JavaScript"]
|
||||
])("getSection_%s_ReturnsCorrectSection", (id, expectedTitle) => {
|
||||
const section = getSection(id);
|
||||
expect(section).not.toBeNull();
|
||||
@@ -51,7 +53,7 @@ describe("Sections Config", () => {
|
||||
describe("getSectionList", () => {
|
||||
test("getSectionList_Default_ReturnsSortedByOrder", () => {
|
||||
const list = getSectionList();
|
||||
expect(list).toHaveLength(4);
|
||||
expect(list).toHaveLength(5);
|
||||
|
||||
// Verify sorted by order
|
||||
for (let i = 1; i < list.length; i++) {
|
||||
@@ -64,9 +66,9 @@ describe("Sections Config", () => {
|
||||
expect(list[0].id).toBe("css");
|
||||
});
|
||||
|
||||
test("getSectionList_Default_MarkdownIsLast", () => {
|
||||
test("getSectionList_Default_JavaScriptIsLast", () => {
|
||||
const list = getSectionList();
|
||||
expect(list[list.length - 1].id).toBe("markdown");
|
||||
expect(list[list.length - 1].id).toBe("javascript");
|
||||
});
|
||||
|
||||
test("getSectionList_Default_ContainsAllSections", () => {
|
||||
@@ -76,6 +78,7 @@ describe("Sections Config", () => {
|
||||
expect(ids).toContain("html");
|
||||
expect(ids).toContain("tailwind");
|
||||
expect(ids).toContain("markdown");
|
||||
expect(ids).toContain("javascript");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +92,8 @@ describe("Sections Config", () => {
|
||||
["css", "css"],
|
||||
["html", "html"],
|
||||
["tailwind", "tailwind"],
|
||||
["markdown", "markdown"]
|
||||
["markdown", "markdown"],
|
||||
["javascript", "javascript"]
|
||||
])("getModuleSection_Mode%s_InfersCorrectSection", (mode, expectedSection) => {
|
||||
const module = { mode };
|
||||
expect(getModuleSection(module)).toBe(expectedSection);
|
||||
@@ -104,7 +108,7 @@ describe("Sections Config", () => {
|
||||
});
|
||||
|
||||
test("getModuleSection_UnknownMode_DefaultsToCss", () => {
|
||||
expect(getModuleSection({ mode: "javascript" })).toBe("css");
|
||||
expect(getModuleSection({ mode: "unknown-mode" })).toBe("css");
|
||||
});
|
||||
|
||||
test("getModuleSection_ExplicitSectionOverridesMode_UsesSection", () => {
|
||||
|
||||
@@ -226,6 +226,86 @@ describe("CSS Validator", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript Validator", () => {
|
||||
describe("validateUserCode with mode: javascript", () => {
|
||||
it("should validate contains correctly for JavaScript", () => {
|
||||
const userCode = 'const name = "Alice";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "contains", value: "const", message: "Use const" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.validCases).toBe(1);
|
||||
});
|
||||
|
||||
it("should validate regex correctly for JavaScript", () => {
|
||||
const userCode = 'const name = "Alice";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "regex", value: 'const\\s+name\\s*=', message: "Declare name" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate not_contains correctly for JavaScript", () => {
|
||||
const userCode = 'const name = "Alice";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "not_contains", value: "var", message: "Do not use var" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
|
||||
const failCode = 'var name = "Alice";';
|
||||
const failResult = validateUserCode(failCode, lesson);
|
||||
expect(failResult.isValid).toBe(false);
|
||||
expect(failResult.message).toBe("Do not use var");
|
||||
});
|
||||
|
||||
it("should return invalid for missing code", () => {
|
||||
const userCode = "";
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "contains", value: "const", message: "Use const" }]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it("should pass with no validations", () => {
|
||||
const userCode = 'const x = 1;';
|
||||
const lesson = { mode: "javascript" };
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.message).toContain("No validations specified");
|
||||
});
|
||||
|
||||
it("should handle multiple validations with early return on failure", () => {
|
||||
const userCode = 'const name = "Alice";';
|
||||
const lesson = {
|
||||
mode: "javascript",
|
||||
validations: [
|
||||
{ type: "contains", value: "const", message: "Use const" },
|
||||
{ type: "contains", value: "let", message: "Use let" },
|
||||
{ type: "contains", value: "name", message: "Declare name" }
|
||||
]
|
||||
};
|
||||
|
||||
const result = validateUserCode(userCode, lesson);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Use let");
|
||||
expect(result.validCases).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML Validator", () => {
|
||||
describe("validateUserCode with mode: html", () => {
|
||||
it("should validate element_exists correctly", () => {
|
||||
|
||||
Reference in New Issue
Block a user