Compare commits
9 Commits
004-pedago
...
009-colors
| Author | SHA1 | Date | |
|---|---|---|---|
| 1baff9075c | |||
| 3d6ff645fe | |||
| dc048eba4e | |||
| 05a683388b | |||
| ae8f9fef45 | |||
| 8d567390e5 | |||
| 372320b807 | |||
| 61acd692f4 | |||
| 672a2d28cb |
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Which property controls text color?"
|
||||
"message": "Add <kbd>color: coral;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Check the <kbd>background</kbd> property"
|
||||
"message": "Add <kbd>background: lavender;</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "The card needs space inside its edges"
|
||||
"message": "Add <kbd>padding: 1rem;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Which property changes text color?"
|
||||
"message": "Set <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "What value gives a warm, reddish-orange color?"
|
||||
"message": "Set <kbd>color: coral</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "The badge needs a bright red background"
|
||||
"message": "Set <kbd>background: tomato</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Which property sets the button's fill color?"
|
||||
"message": "Set <kbd>background: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Which property controls the underline on links?"
|
||||
"message": "Set <kbd>text-decoration: none</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Check the <kbd>color</kbd> property"
|
||||
"message": "Set <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "The links need to stand out against the blue background"
|
||||
"message": "Set <kbd>color: white</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Check the <kbd>font-size</kbd> property — the text should be slightly smaller"
|
||||
"message": "Set <kbd>font-size: 0.9rem</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"property": "padding",
|
||||
"expected": "20px"
|
||||
},
|
||||
"message": "How much breathing room does the content need? Re-read the task for the exact size",
|
||||
"message": "Set the padding value to <kbd>20px</kbd>",
|
||||
"options": {
|
||||
"exact": true
|
||||
}
|
||||
@@ -181,7 +181,7 @@
|
||||
"property": "margin-bottom",
|
||||
"expected": "30px"
|
||||
},
|
||||
"message": "How much space should separate the title from the content below? Check the task for the amount",
|
||||
"message": "Set the margin-bottom value to <kbd>30px</kbd>",
|
||||
"options": {
|
||||
"exact": true
|
||||
}
|
||||
@@ -212,7 +212,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border:\\s*2px\\s+solid\\s+blue",
|
||||
"message": "The <kbd>border</kbd> shorthand takes three parts: width, style, and color",
|
||||
"message": "Set the border to <kbd>2px solid blue</kbd>",
|
||||
"options": {
|
||||
"caseSensitive": false
|
||||
}
|
||||
@@ -246,7 +246,7 @@
|
||||
"property": "justify-content",
|
||||
"expected": "center"
|
||||
},
|
||||
"message": "How do you center items along the main axis?",
|
||||
"message": "Set <kbd>justify-content</kbd> to <kbd>center</kbd>",
|
||||
"options": {
|
||||
"exact": true
|
||||
}
|
||||
@@ -265,7 +265,7 @@
|
||||
"property": "align-items",
|
||||
"expected": "center"
|
||||
},
|
||||
"message": "Which property centers items along the cross axis?",
|
||||
"message": "Set <kbd>align-items</kbd> to <kbd>center</kbd>",
|
||||
"options": {
|
||||
"exact": true
|
||||
}
|
||||
@@ -327,7 +327,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "font-family:\\s*Courier,\\s*monospace",
|
||||
"message": "A font stack lists preferred fonts first, followed by a generic fallback, separated by commas",
|
||||
"message": "Set the font-family to <kbd>Courier, monospace</kbd>",
|
||||
"options": {
|
||||
"caseSensitive": false
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^input\\[type=\"text\"\\]\\s*{",
|
||||
"message": "Which attribute selector syntax targets inputs with a specific type? Check the square-bracket notation from the description.",
|
||||
"message": "Use <kbd>input[type=\"text\"] { … }</kbd> as your attribute selector",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^a\\[href\\^=\"https\"\\]\\s*{",
|
||||
"message": "Which partial-match attribute selector targets values that <em>start with</em> a given string? Combine the element name with that selector.",
|
||||
"message": "Use <kbd>a[href^=\"https\"] { … }</kbd> as your attribute selector to target HTTPS links",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -145,7 +145,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^\\.main-nav\\s*>\\s*li\\s*{",
|
||||
"message": "Which combinator selects only <em>direct</em> children, skipping deeper descendants? Place it between the parent and child selectors.",
|
||||
"message": "Use <kbd>.main-nav > li { … }</kbd> with the child combinator to target only direct children",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -203,7 +203,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^nav\\s+a\\s*{",
|
||||
"message": "The descendant combinator is the simplest one — what character separates a parent selector from a descendant selector?",
|
||||
"message": "Use <kbd>nav a</kbd> with a space between nav and a",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -261,7 +261,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^h2\\s*\\+\\s*p\\s*{",
|
||||
"message": "Which combinator targets the element <em>immediately</em> following a sibling? Place it between the two element selectors.",
|
||||
"message": "Use <kbd>h2 + p</kbd> with the adjacent sibling combinator (+)",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -319,7 +319,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^h3\\s*~\\s*p\\s*{",
|
||||
"message": "Which combinator selects <em>all</em> later siblings, not just the one right next to it? Place it between the two element selectors.",
|
||||
"message": "Use <kbd>h3 ~ p</kbd> with the general sibling combinator (~)",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -377,7 +377,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^button:hover\\s*{",
|
||||
"message": "Which pseudo-class activates when the cursor is over an element? Attach it to the button selector with a colon.",
|
||||
"message": "Use <kbd>button:hover</kbd> to target buttons on hover",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -435,7 +435,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^li:first-child\\s*{",
|
||||
"message": "Which pseudo-class selects an element only when it is the <em>first</em> child of its parent? Attach it to the <kbd>li</kbd> selector.",
|
||||
"message": "Use <kbd>li:first-child</kbd> to target first list items",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Which property adds space between an element's content and its border?"
|
||||
"message": "Which property adds space between content and the element's edge?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Use the <kbd>border-left</kbd> shorthand with width, style, and color values",
|
||||
"message": "Use the shorthand that sets a border on just one side",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Which property creates space below an element, pushing neighbors away?"
|
||||
"message": "Which property pushes neighboring elements away from the bottom?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Which <kbd>box-sizing</kbd> value includes padding and border in the element's total width?"
|
||||
"message": "Which sizing mode includes padding and border in the element's width?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Use the <kbd>padding</kbd> shorthand with two values: vertical then horizontal",
|
||||
"message": "Use the two-value shorthand: vertical first, then horizontal",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Use <kbd>margin</kbd> with a keyword that auto-calculates equal left and right spacing",
|
||||
"message": "Use the shorthand that auto-calculates equal horizontal margins",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Which <kbd>border-radius</kbd> percentage creates a perfect circle from a square element?"
|
||||
"message": "Which property rounds corners? Think about what percentage makes a circle"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Add inner spacing to the notification card"
|
||||
"message": "Add inner spacing to the notification"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Add a left border accent using the <kbd>border-left</kbd> shorthand",
|
||||
"message": "Add a colored accent on the left edge",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Round the corners slightly with <kbd>border-radius</kbd>"
|
||||
"message": "Soften the corners of the notification"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 sets the fill color behind an element's content area?"
|
||||
"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 CSS property changes the color of text content?"
|
||||
"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 color of an existing 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": "Set the <kbd>background-color</kbd> using a hex code format"
|
||||
"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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "2px 2px",
|
||||
"message": "How far should the shadow move horizontally and vertically?"
|
||||
"message": "Set offset to <kbd>2px 2px</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Which property caps an element's width? Try a <kbd>rem</kbd> value for readable line length."
|
||||
"message": "Set <kbd>max-width: 40rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Use <kbd>calc()</kbd> to subtract the sidebar's fixed width from the full container width.",
|
||||
"message": "Set <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Which property ensures a minimum height? Use a viewport unit for full-screen coverage."
|
||||
"message": "Set <kbd>min-height: 100vh</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Specify which property to transition and how long it should take.",
|
||||
"message": "Set <kbd>transition: background-color 0.3s</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Which easing keyword starts slow, speeds up, then slows down again?"
|
||||
"message": "Set timing to <kbd>ease-in-out</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Use the <kbd>animation</kbd> shorthand: name, duration, and repeat count.",
|
||||
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Which property links an element to a named <kbd>@keyframes</kbd> rule?"
|
||||
"message": "Set <kbd>animation-name: pulse</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "How long should one full cycle of the animation take?"
|
||||
"message": "Set <kbd>animation-duration: 2s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Which property makes the animation wait before starting?"
|
||||
"message": "Set <kbd>animation-delay: 1s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Which property controls how many times the animation repeats?"
|
||||
"message": "Set <kbd>animation-iteration-count: 2</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Which property keeps the element styled in its final keyframe state after the animation ends?"
|
||||
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,24 +18,14 @@
|
||||
"codeSuffix": "}",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "display",
|
||||
"message": "Which display mode arranges children in a row or column?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{ "type": "contains", "value": "display", "message": "Use <kbd>display: flex</kbd>", "options": { "caseSensitive": false } },
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "justify-content",
|
||||
"message": "How do you center items along the main axis?",
|
||||
"message": "Use <kbd>justify-content: center</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "align-items",
|
||||
"message": "Which property centers items along the cross axis?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
{ "type": "contains", "value": "align-items", "message": "Use <kbd>align-items: center</kbd>", "options": { "caseSensitive": false } }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -54,13 +44,13 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "flex-wrap: wrap",
|
||||
"message": "Which property allows flex items to flow onto multiple lines?",
|
||||
"message": "Use <kbd>flex-wrap: wrap</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": ".item.*flex:\\s*1\\s+1\\s+100px",
|
||||
"message": "The <kbd>flex</kbd> shorthand takes grow, shrink, and basis values — what basis size should each item start from?",
|
||||
"message": "Set <kbd>flex: 1 1 100px</kbd> on items",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -78,22 +68,17 @@
|
||||
"codeSuffix": "}",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "display: grid",
|
||||
"message": "Which display mode lets you define rows and columns?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{ "type": "contains", "value": "display: grid", "message": "Use <kbd>display: grid</kbd>", "options": { "caseSensitive": false } },
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "grid-template-columns",
|
||||
"message": "Which property defines the column structure of a grid?",
|
||||
"message": "Define <kbd>grid-template-columns</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "grid-template-columns:\\s*repeat\\(3,\\s*1fr\\)\\s*",
|
||||
"message": "The <kbd>repeat()</kbd> function can create equal-width columns — how many do you need, and what unit makes them equal?",
|
||||
"message": "Create three equal columns with <kbd>repeat(3, 1fr)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{ "type": "contains", "value": "gap", "message": "Use <kbd>gap</kbd> property", "options": { "caseSensitive": false } }
|
||||
@@ -121,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "grid-column", "expected": "1 / span 2" },
|
||||
"message": "Use <kbd>grid-column</kbd> with a start line and a span count \u2014 how many columns should this item stretch across?",
|
||||
"message": "Span across 2 columns with <kbd>grid-column: 1 / span 2</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||
"message": "Start with an <kbd>@media</kbd> rule \u2014 which condition targets screens 600px wide or smaller?",
|
||||
"message": "Use <kbd>@media (max-width: 600px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Which property changes the element's background color?",
|
||||
"message": "Set <kbd>background: lightcoral</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -53,11 +53,7 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Which CSS unit scales relative to the viewport width?"
|
||||
}
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -77,18 +73,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Which display mode lets you define rows and columns?"
|
||||
"message": "Set <kbd>display: grid</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||
"message": "Try <kbd>repeat()</kbd> with <kbd>auto-fit</kbd> and <kbd>minmax()</kbd> — what minimum and maximum sizes create flexible columns?",
|
||||
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Which property adds space between grid items?"
|
||||
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -109,7 +105,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||
"message": "Which <kbd>@media</kbd> condition applies styles when the viewport is at least 768px wide?",
|
||||
"message": "Use <kbd>@media (min-width: 768px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -121,7 +117,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Which property controls how wide the sidebar should be on larger screens?",
|
||||
"message": "Set <kbd>width: 250px</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "linear-gradient",
|
||||
"message": "Which CSS function creates a smooth transition between colors along a straight line?"
|
||||
"message": "Use <kbd>linear-gradient()</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -53,7 +53,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "to right",
|
||||
"message": "Which direction keyword makes a gradient flow horizontally from the left side?"
|
||||
"message": "Add <kbd>to right</kbd> to set the direction"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "radial-gradient",
|
||||
"message": "Which CSS function creates a gradient that radiates outward from a center point?"
|
||||
"message": "Use <kbd>radial-gradient()</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "bg-blue-500",
|
||||
"message": "Which Tailwind utility sets a blue background color? Think about the <kbd>bg-{color}-{shade}</kbd> pattern."
|
||||
"message": "Add the <kbd>bg-blue-500</kbd> class for a blue background."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -38,22 +38,22 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "bg-white",
|
||||
"message": "Which Tailwind utility sets a white background? The pattern is <kbd>bg-{color}</kbd>."
|
||||
"message": "Add <kbd>bg-white</kbd> to set the background color to white."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "p-4",
|
||||
"message": "Which Tailwind utility adds 1rem padding on all sides? Remember: each spacing unit is 0.25rem."
|
||||
"message": "Add <kbd>p-4</kbd> to apply 1rem padding on all sides."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "rounded",
|
||||
"message": "Which Tailwind utility adds rounded corners? It is one of the simplest utility names."
|
||||
"message": "Add <kbd>rounded</kbd> to apply border-radius of 0.25rem."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "shadow-sm",
|
||||
"message": "Which Tailwind utility adds a small drop-shadow? Look for a <kbd>shadow-</kbd> variant."
|
||||
"message": "Add <kbd>shadow-sm</kbd> to apply small drop-shadow."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,17 +71,17 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-blue-600",
|
||||
"message": "Which Tailwind utility controls text color? Use the <kbd>text-{color}-{shade}</kbd> pattern with a blue shade."
|
||||
"message": "Add <kbd>text-blue-600</kbd> to make the text blue"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-2xl",
|
||||
"message": "Which Tailwind utility sets the font size to 1.5rem? Check the <kbd>text-{size}</kbd> scale."
|
||||
"message": "Add <kbd>text-2xl</kbd> to increase the font size to 1.5rem"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "font-bold",
|
||||
"message": "Which Tailwind utility makes text bold? The <kbd>font-{weight}</kbd> pattern controls font weight."
|
||||
"message": "Add <kbd>font-bold</kbd> to make the text bold (font-weight: 700)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -99,17 +99,17 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "px-6",
|
||||
"message": "Which Tailwind utility adds horizontal padding of 1.5rem? The <kbd>px-</kbd> prefix targets left and right."
|
||||
"message": "Add <kbd>px-6</kbd> for horizontal padding (1.5rem left and right)"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "py-3",
|
||||
"message": "Which Tailwind utility adds vertical padding of 0.75rem? The <kbd>py-</kbd> prefix targets top and bottom."
|
||||
"message": "Add <kbd>py-3</kbd> for vertical padding (0.75rem top and bottom)"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "mx-auto",
|
||||
"message": "Which Tailwind utility centers an element horizontally using auto margins?"
|
||||
"message": "Add <kbd>mx-auto</kbd> to center the button horizontally"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,32 +127,32 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "w-full",
|
||||
"message": "Which Tailwind utility makes an element take up 100% width? This is the base (mobile) style."
|
||||
"message": "Add <kbd>w-full</kbd> for 100% width on mobile"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "md:w-1/2",
|
||||
"message": "How do you set 50% width at the <kbd>md:</kbd> breakpoint? Tailwind uses fraction notation for widths."
|
||||
"message": "Add <kbd>md:w-1/2</kbd> for 50% width on tablet and up"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "lg:w-1/3",
|
||||
"message": "How do you set one-third width at the <kbd>lg:</kbd> breakpoint? Use the same fraction pattern."
|
||||
"message": "Add <kbd>lg:w-1/3</kbd> for 33.33% width on desktop and up"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-lg",
|
||||
"message": "Which Tailwind text size utility is one step above the base size? Think about the <kbd>text-{size}</kbd> scale."
|
||||
"message": "Add <kbd>text-lg</kbd> for the base text size"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "md:text-xl",
|
||||
"message": "How do you increase the text size at the <kbd>md:</kbd> breakpoint? Go one step larger."
|
||||
"message": "Add <kbd>md:text-xl</kbd> for larger text on tablets"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "lg:text-2xl",
|
||||
"message": "How do you set an even larger text size at the <kbd>lg:</kbd> breakpoint? Continue stepping up the scale."
|
||||
"message": "Add <kbd>lg:text-2xl</kbd> for even larger text on desktop"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "filter", "expected": "blur(4px)" },
|
||||
"message": "Which CSS property applies visual effects like blur? Use the <kbd>blur()</kbd> function with a pixel value."
|
||||
"message": "Set <kbd>filter: blur(4px)</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -48,7 +48,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "100%",
|
||||
"message": "What percentage value removes all color completely?"
|
||||
"message": "Set to <kbd>100%</kbd> for full grayscale"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "120%",
|
||||
"message": "What percentage makes the element slightly brighter than normal? Normal is 100%."
|
||||
"message": "Set to <kbd>120%</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "4px 4px 8px",
|
||||
"message": "Set the x-offset, y-offset, and blur radius. The task describes the exact values needed."
|
||||
"message": "Set shadow offset and blur"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "position", "expected": "relative" },
|
||||
"message": "Which position value keeps an element in normal flow but allows offset adjustments?"
|
||||
"message": "Set <kbd>position: relative</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "top", "expected": "-8px" },
|
||||
"message": "Which offset property moves an element upward from its current position?"
|
||||
"message": "Set <kbd>top: -8px</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "position", "expected": "absolute" },
|
||||
"message": "Which position value removes an element from normal flow for precise placement?"
|
||||
"message": "Set <kbd>position: absolute</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,12 +85,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "top", "expected": "8px" },
|
||||
"message": "Which offset property controls the distance from the top of the positioned ancestor?"
|
||||
"message": "Set <kbd>top: 8px</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "right", "expected": "8px" },
|
||||
"message": "Which offset property controls the distance from the right edge?"
|
||||
"message": "Set <kbd>right: 8px</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Which CSS property changes the text color of the bullet? Try a warm, pinkish-orange named color."
|
||||
"message": "Set <kbd>color: coral</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,17 +95,17 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "40px" },
|
||||
"message": "How wide should the decorative line be? Check the task for the pixel value."
|
||||
"message": "Set <kbd>width: 40px</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "height", "expected": "3px" },
|
||||
"message": "Which CSS property controls the thickness of the line? A thin line looks best here."
|
||||
"message": "Set <kbd>height: 3px</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Which CSS property fills the line with color? Use a steel-toned blue named color."
|
||||
"message": "Set <kbd>background: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "ما الخاصية التي تتحكم في لون النص؟"
|
||||
"message": "أضف <kbd>color: coral;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "تحقق من خاصية <kbd>background</kbd>"
|
||||
"message": "أضف <kbd>background: lavender;</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "البطاقة تحتاج إلى مساحة داخل حوافها"
|
||||
"message": "أضف <kbd>padding: 1rem;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "ما الخاصية التي تغيّر لون النص؟"
|
||||
"message": "اضبط <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "ما القيمة التي تعطي لوناً دافئاً برتقالياً محمراً؟"
|
||||
"message": "اضبط <kbd>color: coral</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "الشارة تحتاج إلى خلفية حمراء زاهية"
|
||||
"message": "اضبط <kbd>background: tomato</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "ما الخاصية التي تضبط لون تعبئة الزر؟"
|
||||
"message": "اضبط <kbd>background: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "ما الخاصية التي تتحكم في الخط أسفل الروابط؟"
|
||||
"message": "اضبط <kbd>text-decoration: none</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "تحقق من خاصية <kbd>color</kbd>"
|
||||
"message": "اضبط <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "الروابط تحتاج إلى أن تبرز على الخلفية الزرقاء"
|
||||
"message": "اضبط <kbd>color: white</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "تحقق من خاصية <kbd>font-size</kbd> — النص يجب أن يكون أصغر قليلاً"
|
||||
"message": "اضبط <kbd>font-size: 0.9rem</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": "",
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "ما الخاصية التي تضيف مساحة بين محتوى العنصر وحدوده؟"
|
||||
"message": "أي خاصية تضيف مساحة بين المحتوى وحافة العنصر؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "استخدم اختصار <kbd>border-left</kbd> مع قيم العرض والنمط واللون",
|
||||
"message": "استخدم الاختصار الذي يحدد حداً على جانب واحد فقط",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "ما الخاصية التي تُنشئ مساحة أسفل العنصر وتدفع الجيران بعيداً؟"
|
||||
"message": "أي خاصية تدفع العناصر المجاورة بعيداً من الأسفل؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "ما قيمة <kbd>box-sizing</kbd> التي تشمل الحشو والحدود في العرض الإجمالي للعنصر؟"
|
||||
"message": "أي وضع تحجيم يشمل padding والحدود في عرض العنصر؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "استخدم اختصار <kbd>padding</kbd> بقيمتين: عمودي ثم أفقي",
|
||||
"message": "استخدم اختصار القيمتين: العمودي أولاً، ثم الأفقي",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "استخدم <kbd>margin</kbd> مع كلمة مفتاحية تحسب تلقائياً مسافات متساوية يميناً ويساراً",
|
||||
"message": "استخدم الاختصار الذي يحسب هوامش أفقية متساوية تلقائياً",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "ما نسبة <kbd>border-radius</kbd> التي تُنشئ دائرة كاملة من عنصر مربع؟"
|
||||
"message": "أي خاصية تدوّر الزوايا؟ فكر في النسبة المئوية التي تصنع دائرة"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "أضف مساحة داخلية لبطاقة الإشعار"
|
||||
"message": "أضف مساحة داخلية للإشعار"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "أضف لمسة حدود يسارية باستخدام اختصار <kbd>border-left</kbd>",
|
||||
"message": "أضف لمسة ملونة على الحافة اليسرى",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "دوّر الزوايا قليلاً باستخدام <kbd>border-radius</kbd>"
|
||||
"message": "نعّم زوايا الإشعار"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "ما الخاصية التي تحدّ من عرض العنصر؟ جرّب قيمة بوحدة <kbd>rem</kbd> لطول سطر مقروء."
|
||||
"message": "اضبط <kbd>max-width: 40rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,13 +43,13 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "--brand",
|
||||
"message": "عرّف متغير <kbd>--brand</kbd>",
|
||||
"message": "عرّف المتغير <kbd>--brand</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "steelblue",
|
||||
"message": "اضبط القيمة إلى <kbd>steelblue</kbd>",
|
||||
"message": "اضبط القيمة على <kbd>steelblue</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "استخدم <kbd>calc()</kbd> لطرح عرض الشريط الجانبي الثابت من عرض الحاوية الكامل.",
|
||||
"message": "اضبط <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "ما الخاصية التي تضمن حداً أدنى للارتفاع؟ استخدم وحدة viewport لتغطية الشاشة بالكامل."
|
||||
"message": "اضبط <kbd>min-height: 100vh</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "حدد أي خاصية تريد تحريكها وكم من الوقت يجب أن تستغرق.",
|
||||
"message": "اضبط <kbd>transition: background-color 0.3s</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "ما كلمة التسهيل التي تبدأ بطيئة، تتسارع، ثم تبطئ مرة أخرى؟"
|
||||
"message": "اضبط التوقيت على <kbd>ease-in-out</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "استخدم اختصار <kbd>animation</kbd>: الاسم، المدة، وعدد التكرار.",
|
||||
"message": "طبّق <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "ما الخاصية التي تربط العنصر بقاعدة <kbd>@keyframes</kbd> مسماة؟"
|
||||
"message": "اضبط <kbd>animation-name: pulse</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "كم يجب أن تستغرق دورة كاملة من الحركة؟"
|
||||
"message": "اضبط <kbd>animation-duration: 2s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "ما الخاصية التي تجعل الحركة تنتظر قبل أن تبدأ؟"
|
||||
"message": "اضبط <kbd>animation-delay: 1s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "ما الخاصية التي تتحكم في عدد مرات تكرار الحركة؟"
|
||||
"message": "اضبط <kbd>animation-iteration-count: 2</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "ما الخاصية التي تُبقي العنصر بتنسيق حالته النهائية بعد انتهاء الحركة؟"
|
||||
"message": "اضبط <kbd>animation-fill-mode: forwards</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||
"message": "ابدأ بقاعدة <kbd>@media</kbd> — ما الشرط الذي يستهدف الشاشات بعرض 600px أو أقل؟",
|
||||
"message": "استخدم <kbd>@media (max-width: 600px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "ما الخاصية التي تغيّر لون خلفية العنصر؟",
|
||||
"message": "اضبط <kbd>background: lightcoral</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -53,11 +53,7 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "ما وحدة CSS التي تتناسب مع عرض viewport؟"
|
||||
}
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "اضبط <kbd>font-size: 5vw</kbd>" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -77,18 +73,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "ما وضع العرض الذي يتيح لك تعريف صفوف وأعمدة؟"
|
||||
"message": "اضبط <kbd>display: grid</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||
"message": "جرّب <kbd>repeat()</kbd> مع <kbd>auto-fit</kbd> و <kbd>minmax()</kbd> — ما الحد الأدنى والأقصى للحجم لإنشاء أعمدة مرنة؟",
|
||||
"message": "استخدم <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "ما الخاصية التي تضيف مساحة بين عناصر الشبكة؟"
|
||||
"message": "اضبط <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -109,7 +105,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||
"message": "ما شرط <kbd>@media</kbd> الذي يُطبّق الأنماط عندما يكون عرض viewport على الأقل 768px؟",
|
||||
"message": "استخدم <kbd>@media (min-width: 768px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -121,7 +117,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "ما الخاصية التي تتحكم في عرض الشريط الجانبي على الشاشات الكبيرة؟",
|
||||
"message": "اضبط <kbd>width: 250px</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "ما قيمة display التي تحوّل العنصر إلى حاوية صندوق مرن؟"
|
||||
"message": "اضبط <kbd>display: flex</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "ما الخاصية التي تُنشئ تباعداً بين عناصر flex بدون استخدام الهوامش؟"
|
||||
"message": "اضبط <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "ما قيمة <kbd>justify-content</kbd> التي تدفع العنصر الأول والأخير إلى الحواف المتقابلة؟"
|
||||
"message": "اضبط <kbd>justify-content: space-between</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "ما الخاصية التي تُحاذي عناصر flex على طول المحور المتقاطع؟"
|
||||
"message": "اضبط <kbd>align-items: center</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "ما الخاصية التي تسمح لعناصر flex بالتدفق إلى أسطر متعددة؟"
|
||||
"message": "اضبط <kbd>flex-wrap: wrap</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "ما الخاصية التي تجعل عنصر flex ينمو لملء المساحة المتبقية؟"
|
||||
"message": "اضبط <kbd>flex: 1</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Welche Eigenschaft ändert die Textfarbe?"
|
||||
"message": "Füge <kbd>color: coral;</kbd> hinzu"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Welche Eigenschaft steuert die Hintergrundfarbe?"
|
||||
"message": "Füge <kbd>background: lavender;</kbd> hinzu"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Das Element benötigt auch Innenabstand -- überprüfe die <kbd>padding</kbd>-Eigenschaft"
|
||||
"message": "Füge <kbd>padding: 1rem;</kbd> hinzu"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welcher Farbwert wurde in der Beschreibung genannt?"
|
||||
"message": "Setze <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welche Farbe sollen die Links haben?"
|
||||
"message": "Setze <kbd>color: coral</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "Überprüfe die <kbd>background</kbd>-Eigenschaft -- welche Farbe soll das Badge haben?"
|
||||
"message": "Setze <kbd>background: tomato</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Überprüfe die <kbd>background</kbd>-Eigenschaft -- welche Farbe soll der primäre Button haben?"
|
||||
"message": "Setze <kbd>background: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Welche Eigenschaft entfernt die Unterstreichung von Links?"
|
||||
"message": "Setze <kbd>text-decoration: none</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Welche Eigenschaft ändert die Textfarbe der Überschriften?"
|
||||
"message": "Setze <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welche Farbe passt zu einem dunklen Hintergrund?"
|
||||
"message": "Setze <kbd>color: white</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Welche Eigenschaft steuert die Schriftgröße?"
|
||||
"message": "Setze <kbd>font-size: 0.9rem</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"property": "background-color",
|
||||
"expected": "lightblue"
|
||||
},
|
||||
"message": "Überprüfe die <kbd>background-color</kbd>-Eigenschaft -- welche Farbe sollen die Text-Eingabefelder haben?"
|
||||
"message": "Setze die Hintergrundfarbe auf <kbd>lightblue</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -56,7 +56,7 @@
|
||||
"property": "border",
|
||||
"expected": "2px solid blue"
|
||||
},
|
||||
"message": "Das Element benötigt einen Rahmen -- überprüfe die <kbd>border</kbd>-Eigenschaft"
|
||||
"message": "Setze den Rahmen auf <kbd>2px solid blue</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -101,7 +101,7 @@
|
||||
"property": "color",
|
||||
"expected": "green"
|
||||
},
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welche Farbe kennzeichnet sichere Links?"
|
||||
"message": "Setze die Textfarbe auf <kbd>green</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -114,7 +114,7 @@
|
||||
"property": "text-decoration",
|
||||
"expected": "underline"
|
||||
},
|
||||
"message": "Welcher <kbd>text-decoration</kbd>-Wert macht Links visuell hervorgehoben?"
|
||||
"message": "Setze text-decoration auf <kbd>underline</kbd>, um HTTPS-Links zu unterstreichen"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -159,7 +159,7 @@
|
||||
"property": "background-color",
|
||||
"expected": "cornflowerblue"
|
||||
},
|
||||
"message": "Überprüfe die <kbd>background-color</kbd>-Eigenschaft für die Hauptmenüpunkte"
|
||||
"message": "Setze background-color auf <kbd>cornflowerblue</kbd> für das Hauptmenü-Styling"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -172,7 +172,7 @@
|
||||
"property": "color",
|
||||
"expected": "white"
|
||||
},
|
||||
"message": "Welche Textfarbe sorgt für guten Kontrast auf einem blauen Hintergrund?"
|
||||
"message": "Setze die Textfarbe auf <kbd>white</kbd> für Kontrast gegen den blauen Hintergrund"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -217,7 +217,7 @@
|
||||
"property": "text-decoration",
|
||||
"expected": "none"
|
||||
},
|
||||
"message": "Welcher <kbd>text-decoration</kbd>-Wert entfernt die Unterstreichung?"
|
||||
"message": "Setze text-decoration auf <kbd>none</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -230,7 +230,7 @@
|
||||
"property": "color",
|
||||
"expected": "blue"
|
||||
},
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft für die Links"
|
||||
"message": "Setze color auf <kbd>blue</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -275,7 +275,7 @@
|
||||
"property": "margin-top",
|
||||
"expected": "0"
|
||||
},
|
||||
"message": "Welcher Wert bei <kbd>margin-top</kbd> entfernt den oberen Abstand?"
|
||||
"message": "Setze margin-top auf <kbd>0</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -288,7 +288,7 @@
|
||||
"property": "font-style",
|
||||
"expected": "italic"
|
||||
},
|
||||
"message": "Welcher <kbd>font-style</kbd>-Wert macht den Text kursiv?"
|
||||
"message": "Setze font-style auf <kbd>italic</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -333,7 +333,7 @@
|
||||
"property": "color",
|
||||
"expected": "gray"
|
||||
},
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welche Farbe sollen die Absätze haben?"
|
||||
"message": "Setze color auf <kbd>gray</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -346,7 +346,7 @@
|
||||
"property": "padding-left",
|
||||
"expected": "20px"
|
||||
},
|
||||
"message": "Das Element benötigt eine Einrückung -- überprüfe die <kbd>padding-left</kbd>-Eigenschaft"
|
||||
"message": "Setze padding-left auf <kbd>20px</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -391,7 +391,7 @@
|
||||
"property": "background-color",
|
||||
"expected": "darkblue"
|
||||
},
|
||||
"message": "Welche Hintergrundfarbe soll der Button beim Hover haben?"
|
||||
"message": "Setze background-color auf <kbd>darkblue</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -404,7 +404,7 @@
|
||||
"property": "color",
|
||||
"expected": "white"
|
||||
},
|
||||
"message": "Welche Textfarbe sorgt für Kontrast auf dunklem Hintergrund?"
|
||||
"message": "Setze color auf <kbd>white</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -449,7 +449,7 @@
|
||||
"property": "font-weight",
|
||||
"expected": "bold"
|
||||
},
|
||||
"message": "Welcher <kbd>font-weight</kbd>-Wert macht Text fett?"
|
||||
"message": "Setze font-weight auf <kbd>bold</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -462,7 +462,7 @@
|
||||
"property": "margin-top",
|
||||
"expected": "0"
|
||||
},
|
||||
"message": "Welcher Wert entfernt den oberen Abstand vollständig?"
|
||||
"message": "Setze margin-top auf <kbd>0</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
|
||||
@@ -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": "",
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Welche Eigenschaft steuert den Innenabstand zwischen Inhalt und Rahmen?"
|
||||
"message": "Welche Eigenschaft fügt Abstand zwischen Inhalt und Elementrand hinzu?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Überprüfe die <kbd>border-left</kbd>-Eigenschaft -- welche drei Werte braucht sie?",
|
||||
"message": "Verwende die Kurzschreibweise, die einen Rahmen auf nur einer Seite setzt",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Welche Eigenschaft steuert den Außenabstand nach unten?"
|
||||
"message": "Welche Eigenschaft schiebt benachbarte Elemente nach unten weg?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Welcher <kbd>box-sizing</kbd>-Wert bezieht Padding und Rahmen in die Breite ein?"
|
||||
"message": "Welcher Größenmodus bezieht Padding und Rahmen in die Breite des Elements ein?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Überprüfe die <kbd>padding</kbd>-Kurzschreibweise -- zwei Werte setzen vertikal und horizontal",
|
||||
"message": "Verwende die Zwei-Werte-Kurzschreibweise: vertikal zuerst, dann horizontal",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Welche <kbd>margin</kbd>-Kurzschreibweise zentriert ein Block-Element horizontal?",
|
||||
"message": "Verwende die Kurzschreibweise, die gleiche horizontale Abstände automatisch berechnet",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Welcher <kbd>border-radius</kbd>-Wert macht ein quadratisches Element rund?"
|
||||
"message": "Welche Eigenschaft rundet Ecken? Denke daran, welcher Prozentwert einen Kreis ergibt"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Das Element benötigt Innenabstand -- überprüfe die <kbd>padding</kbd>-Eigenschaft"
|
||||
"message": "Füge inneren Abstand zur Benachrichtigung hinzu"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Überprüfe die <kbd>border-left</kbd>-Eigenschaft -- sie braucht Breite, Stil und Farbe",
|
||||
"message": "Füge einen farbigen Akzent an der linken Kante hinzu",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Das Element benötigt abgerundete Ecken -- überprüfe die <kbd>border-radius</kbd>-Eigenschaft"
|
||||
"message": "Runde die Ecken der Benachrichtigung ab"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Welche Eigenschaft begrenzt die maximale Breite eines Elements?"
|
||||
"message": "Setze <kbd>max-width: 40rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "steelblue",
|
||||
"message": "Welche Farbe soll die Variable haben?",
|
||||
"message": "Setze den Wert auf <kbd>steelblue</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Überprüfe die <kbd>width</kbd>-Eigenschaft -- wie berechnest du den verbleibenden Platz nach der Sidebar?",
|
||||
"message": "Setze <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Welche Eigenschaft setzt die Mindesthöhe? Welche Viewport-Einheit entspricht 100% der Fensterhöhe?"
|
||||
"message": "Setze <kbd>min-height: 100vh</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Überprüfe die <kbd>transition</kbd>-Eigenschaft -- welche CSS-Eigenschaft soll sanft übergehen und wie lange?",
|
||||
"message": "Setze <kbd>transition: background-color 0.3s</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Welche Timing-Funktion startet und endet langsam?"
|
||||
"message": "Setze timing auf <kbd>ease-in-out</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Welche Animation soll angewendet werden? Überprüfe den <kbd>@keyframes</kbd>-Namen."
|
||||
"message": "Setze <kbd>animation-name: pulse</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Welche Eigenschaft steuert die Dauer der Animation?"
|
||||
"message": "Setze <kbd>animation-duration: 2s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Welche Eigenschaft verzögert den Start der Animation?"
|
||||
"message": "Setze <kbd>animation-delay: 1s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Welche Eigenschaft steuert, wie oft die Animation wiederholt wird?"
|
||||
"message": "Setze <kbd>animation-iteration-count: 2</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Welcher <kbd>animation-fill-mode</kbd>-Wert behält den Endzustand bei?"
|
||||
"message": "Setze <kbd>animation-fill-mode: forwards</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Überprüfe die <kbd>background</kbd>-Eigenschaft innerhalb der Media Query",
|
||||
"message": "Setze <kbd>background: lightcoral</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -53,11 +53,7 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Welche Eigenschaft steuert die Schriftgröße? Welche Viewport-Einheit skaliert mit der Breite?"
|
||||
}
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Setze <kbd>font-size: 5vw</kbd>" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -77,7 +73,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Welcher Display-Wert aktiviert das CSS-Grid-Layout?"
|
||||
"message": "Setze <kbd>display: grid</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -88,7 +84,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Welche Eigenschaft steuert den Abstand zwischen Grid-Zellen?"
|
||||
"message": "Setze <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -121,7 +117,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Überprüfe die <kbd>width</kbd>-Eigenschaft für die Sidebar",
|
||||
"message": "Setze <kbd>width: 250px</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "bg-blue-500",
|
||||
"message": "Welche Tailwind-Klasse setzt eine blaue Hintergrundfarbe? Denke an das <kbd>bg-{farbe}-{abstufung}</kbd>-Muster."
|
||||
"message": "Füge die <kbd>bg-blue-500</kbd>-Klasse für einen blauen Hintergrund hinzu."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -38,22 +38,22 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "bg-white",
|
||||
"message": "Das Element benötigt einen weißen Hintergrund -- welches <kbd>bg-</kbd>-Utility passt?"
|
||||
"message": "Füge <kbd>bg-white</kbd> hinzu, um die Hintergrundfarbe auf weiß zu setzen."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "p-4",
|
||||
"message": "Welches <kbd>p-</kbd>-Utility erzeugt 1rem Padding auf allen Seiten?"
|
||||
"message": "Füge <kbd>p-4</kbd> hinzu, um 1rem Padding auf allen Seiten anzuwenden."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "rounded",
|
||||
"message": "Welche Klasse fügt abgerundete Ecken hinzu?"
|
||||
"message": "Füge <kbd>rounded</kbd> hinzu, um einen border-radius von 0.25rem anzuwenden."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "shadow-sm",
|
||||
"message": "Das Element benötigt einen kleinen Schatten -- welches <kbd>shadow-</kbd>-Utility passt?"
|
||||
"message": "Füge <kbd>shadow-sm</kbd> hinzu, um einen kleinen Schlagschatten anzuwenden."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,17 +71,17 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-blue-600",
|
||||
"message": "Welches <kbd>text-</kbd>-Utility setzt eine blaue Textfarbe? Denke an das <kbd>text-{farbe}-{abstufung}</kbd>-Muster."
|
||||
"message": "Füge <kbd>text-blue-600</kbd> hinzu, um den Text blau zu machen"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-2xl",
|
||||
"message": "Welches <kbd>text-</kbd>-Utility setzt die Schriftgröße auf 1.5rem?"
|
||||
"message": "Füge <kbd>text-2xl</kbd> hinzu, um die Schriftgröße auf 1.5rem zu erhöhen"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "font-bold",
|
||||
"message": "Welches <kbd>font-</kbd>-Utility macht den Text fett?"
|
||||
"message": "Füge <kbd>font-bold</kbd> hinzu, um den Text fett zu machen (font-weight: 700)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -99,17 +99,17 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "px-6",
|
||||
"message": "Welches <kbd>px-</kbd>-Utility erzeugt 1.5rem horizontales Padding?"
|
||||
"message": "Füge <kbd>px-6</kbd> für horizontales Padding hinzu (1.5rem links und rechts)"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "py-3",
|
||||
"message": "Welches <kbd>py-</kbd>-Utility erzeugt 0.75rem vertikales Padding?"
|
||||
"message": "Füge <kbd>py-3</kbd> für vertikales Padding hinzu (0.75rem oben und unten)"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "mx-auto",
|
||||
"message": "Welches <kbd>mx-</kbd>-Utility zentriert ein Element horizontal?"
|
||||
"message": "Füge <kbd>mx-auto</kbd> hinzu, um den Button horizontal zu zentrieren"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,32 +127,32 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "w-full",
|
||||
"message": "Welches Breiten-Utility macht das Element auf Mobil 100% breit?"
|
||||
"message": "Füge <kbd>w-full</kbd> für 100% Breite auf Mobil hinzu"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "md:w-1/2",
|
||||
"message": "Welches responsive Breiten-Utility setzt 50% ab dem <kbd>md:</kbd>-Breakpoint?"
|
||||
"message": "Füge <kbd>md:w-1/2</kbd> für 50% Breite auf Tablet und größer hinzu"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "lg:w-1/3",
|
||||
"message": "Welches responsive Breiten-Utility setzt 33.33% ab dem <kbd>lg:</kbd>-Breakpoint?"
|
||||
"message": "Füge <kbd>lg:w-1/3</kbd> für 33.33% Breite auf Desktop und größer hinzu"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-lg",
|
||||
"message": "Welches <kbd>text-</kbd>-Utility setzt die Basis-Textgröße auf 1.125rem?"
|
||||
"message": "Füge <kbd>text-lg</kbd> für die Basis-Textgröße hinzu"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "md:text-xl",
|
||||
"message": "Welches responsive Text-Utility setzt eine größere Schrift ab dem <kbd>md:</kbd>-Breakpoint?"
|
||||
"message": "Füge <kbd>md:text-xl</kbd> für größeren Text auf Tablets hinzu"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "lg:text-2xl",
|
||||
"message": "Welches responsive Text-Utility setzt die größte Schrift ab dem <kbd>lg:</kbd>-Breakpoint?"
|
||||
"message": "Füge <kbd>lg:text-2xl</kbd> für noch größeren Text auf Desktop hinzu"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Welcher Display-Wert macht ein Element zu einem flexiblen Box-Container?"
|
||||
"message": "Setze <kbd>display: flex</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Welche Eigenschaft erzeugt Abstände zwischen Flex-Items ohne Margins?"
|
||||
"message": "Setze <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Welcher <kbd>justify-content</kbd>-Wert schiebt das erste und letzte Element an gegenüberliegende Ränder?"
|
||||
"message": "Setze <kbd>justify-content: space-between</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Welche Eigenschaft richtet Flex-Items entlang der Querachse aus?"
|
||||
"message": "Setze <kbd>align-items: center</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Welche Eigenschaft erlaubt Flex-Items, auf mehrere Zeilen umzubrechen?"
|
||||
"message": "Setze <kbd>flex-wrap: wrap</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Welche Eigenschaft lässt ein Flex-Item wachsen, um den verbleibenden Platz zu füllen?"
|
||||
"message": "Setze <kbd>flex: 1</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "¿Qué propiedad controla el color del texto?"
|
||||
"message": "Añade <kbd>color: coral;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Revisa la propiedad <kbd>background</kbd>"
|
||||
"message": "Añade <kbd>background: lavender;</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "La tarjeta necesita espacio dentro de sus bordes"
|
||||
"message": "Añade <kbd>padding: 1rem;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "¿Qué propiedad cambia el color del texto?"
|
||||
"message": "Establece <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "¿Qué valor da un color cálido, rojo-anaranjado?"
|
||||
"message": "Establece <kbd>color: coral</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "El badge necesita un fondo rojo brillante"
|
||||
"message": "Establece <kbd>background: tomato</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "¿Qué propiedad establece el color de relleno del botón?"
|
||||
"message": "Establece <kbd>background: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "¿Qué propiedad controla el subrayado de los enlaces?"
|
||||
"message": "Establece <kbd>text-decoration: none</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Revisa la propiedad <kbd>color</kbd>"
|
||||
"message": "Establece <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Los enlaces necesitan destacar sobre el fondo azul"
|
||||
"message": "Establece <kbd>color: white</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Revisa la propiedad <kbd>font-size</kbd> — el texto debería ser ligeramente más pequeño"
|
||||
"message": "Establece <kbd>font-size: 0.9rem</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "¿Qué propiedad añade espacio entre el contenido de un elemento y su borde?"
|
||||
"message": "¿Qué propiedad añade espacio entre el contenido y el borde del elemento?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Usa el atajo <kbd>border-left</kbd> con valores de ancho, estilo y color",
|
||||
"message": "Usa el atajo que define un borde en un solo lado",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "¿Qué propiedad crea espacio debajo de un elemento, separándolo de sus vecinos?"
|
||||
"message": "¿Qué propiedad empuja los elementos vecinos hacia abajo?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "¿Qué valor de <kbd>box-sizing</kbd> incluye padding y borde en el ancho total del elemento?"
|
||||
"message": "¿Qué modo de tamaño incluye padding y borde en el ancho del elemento?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Usa el atajo <kbd>padding</kbd> con dos valores: vertical y luego horizontal",
|
||||
"message": "Usa el atajo de dos valores: vertical primero, luego horizontal",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Usa <kbd>margin</kbd> con una palabra clave que calcula automáticamente espaciado igual a izquierda y derecha",
|
||||
"message": "Usa el atajo que calcula márgenes horizontales iguales automáticamente",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "¿Qué porcentaje de <kbd>border-radius</kbd> crea un círculo perfecto a partir de un elemento cuadrado?"
|
||||
"message": "¿Qué propiedad redondea las esquinas? Piensa en qué porcentaje crea un círculo"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "El elemento necesita espacio interior"
|
||||
"message": "Añade espacio interior a la notificación"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Añade un acento de borde izquierdo usando el atajo <kbd>border-left</kbd>",
|
||||
"message": "Añade un acento de color en el borde izquierdo",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Redondea las esquinas ligeramente con <kbd>border-radius</kbd>"
|
||||
"message": "Suaviza las esquinas de la notificación"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "¿Qué propiedad limita el ancho de un elemento? Prueba un valor en <kbd>rem</kbd> para una longitud de línea legible."
|
||||
"message": "Establece <kbd>max-width: 40rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "steelblue",
|
||||
"message": "Asigna el valor <kbd>steelblue</kbd> a la variable",
|
||||
"message": "Establece el valor a <kbd>steelblue</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Usa <kbd>calc()</kbd> para restar el ancho fijo de la barra lateral del ancho total del contenedor.",
|
||||
"message": "Establece <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "¿Qué propiedad asegura una altura mínima? Usa una unidad de viewport para cobertura de pantalla completa."
|
||||
"message": "Establece <kbd>min-height: 100vh</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Especifica qué propiedad transicionar y cuánto debe durar.",
|
||||
"message": "Establece <kbd>transition: background-color 0.3s</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "¿Qué palabra clave de easing empieza lento, acelera, y luego desacelera de nuevo?"
|
||||
"message": "Establece timing a <kbd>ease-in-out</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Usa el atajo <kbd>animation</kbd>: nombre, duración y número de repeticiones.",
|
||||
"message": "Aplica <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "¿Qué propiedad vincula un elemento a una regla <kbd>@keyframes</kbd> nombrada?"
|
||||
"message": "Establece <kbd>animation-name: pulse</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "¿Cuánto debe durar un ciclo completo de la animación?"
|
||||
"message": "Establece <kbd>animation-duration: 2s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "¿Qué propiedad hace que la animación espere antes de comenzar?"
|
||||
"message": "Establece <kbd>animation-delay: 1s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "¿Qué propiedad controla cuántas veces se repite la animación?"
|
||||
"message": "Establece <kbd>animation-iteration-count: 2</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "¿Qué propiedad mantiene el elemento con los estilos de su último keyframe después de que termina la animación?"
|
||||
"message": "Establece <kbd>animation-fill-mode: forwards</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||
"message": "Empieza con una regla <kbd>@media</kbd> — ¿qué condición apunta a pantallas de 600px de ancho o menos?",
|
||||
"message": "Usa <kbd>@media (max-width: 600px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "¿Qué propiedad cambia el color de fondo del elemento?",
|
||||
"message": "Establece <kbd>background: lightcoral</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "¿Qué unidad CSS escala en relación al ancho del viewport?"
|
||||
"message": "Establece <kbd>font-size: 5vw</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -77,18 +77,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "¿Qué modo de display permite definir filas y columnas?"
|
||||
"message": "Establece <kbd>display: grid</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||
"message": "Prueba <kbd>repeat()</kbd> con <kbd>auto-fit</kbd> y <kbd>minmax()</kbd> — ¿qué tamaños mínimo y máximo crean columnas flexibles?",
|
||||
"message": "Usa <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "¿Qué propiedad añade espacio entre los elementos del grid?"
|
||||
"message": "Establece <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -109,7 +109,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||
"message": "¿Qué condición <kbd>@media</kbd> aplica estilos cuando el viewport tiene al menos 768px de ancho?",
|
||||
"message": "Usa <kbd>@media (min-width: 768px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -121,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "¿Qué propiedad controla el ancho de la barra lateral en pantallas más grandes?",
|
||||
"message": "Establece <kbd>width: 250px</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "¿Qué valor de display convierte un elemento en un contenedor de caja flexible?"
|
||||
"message": "Establece <kbd>display: flex</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "¿Qué propiedad crea espaciado entre elementos flex sin usar márgenes?"
|
||||
"message": "Establece <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "¿Qué valor de <kbd>justify-content</kbd> empuja el primer y último elemento a los extremos opuestos?"
|
||||
"message": "Establece <kbd>justify-content: space-between</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "¿Qué propiedad alinea los elementos flex a lo largo del eje transversal?"
|
||||
"message": "Establece <kbd>align-items: center</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "¿Qué propiedad permite que los elementos flex fluyan a múltiples líneas?"
|
||||
"message": "Establece <kbd>flex-wrap: wrap</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "¿Qué propiedad hace que un elemento flex crezca para llenar el espacio restante?"
|
||||
"message": "Establece <kbd>flex: 1</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"id": "flexbox-1",
|
||||
"title": "Container",
|
||||
"description": "Before flexbox, creating even simple layouts required floats, positioning hacks, or table-based layouts. Flexbox (Flexible Box Layout) revolutionized CSS by providing a one-dimensional layout system designed specifically for distributing space and aligning content.<br><br><strong>How it works:</strong> When you set <kbd>display: flex</kbd> on an element, it becomes a <em>flex container</em>. Its direct children automatically become <em>flex items</em> that flow along a main axis (horizontal by default). This single property transforms stacked block elements into a horizontal row.<br><br><strong>The two axes:</strong><br>• <em>Main axis</em> – The primary direction items flow (row = left→right)<br>• <em>Cross axis</em> – Perpendicular to main (row = top→bottom)<br><br><pre>.nav {\n display: flex;\n}</pre>",
|
||||
"task": "This navigation menu stacks vertically. Add <kbd>display: flex</kbd> to <kbd>.nav</kbd> to arrange the links horizontally.",
|
||||
"task": "The navigation links are stacking vertically. Make them display side by side in a horizontal row.",
|
||||
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); }",
|
||||
"sandboxCSS": "",
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Which display value turns an element into a flexible box container?"
|
||||
"message": "Try changing the display mode to create a flex container"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": "flexbox-2",
|
||||
"title": "Gap",
|
||||
"description": "The <kbd>gap</kbd> property adds consistent spacing between flex items without needing margins. It only creates space between items, not around the edges.",
|
||||
"task": "Add <kbd>gap: 1rem</kbd> to space out the navigation links evenly.",
|
||||
"task": "The navigation links are crammed together with no breathing room. Add 1rem of spacing between them.",
|
||||
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; background: rgba(255,255,255,0.1); }",
|
||||
"sandboxCSS": "",
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Which property creates spacing between flex items without using margins?"
|
||||
"message": "Use the property that adds spacing between flex items"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -51,7 +51,7 @@
|
||||
"id": "flexbox-3",
|
||||
"title": "Justify Content",
|
||||
"description": "<kbd>justify-content</kbd> distributes items along the main axis. Common values:<br>• <kbd>flex-start</kbd> – pack items at the start<br>• <kbd>flex-end</kbd> – pack at the end<br>• <kbd>center</kbd> – center items<br>• <kbd>space-between</kbd> – equal space between items<br>• <kbd>space-around</kbd> – equal space around items",
|
||||
"task": "Push the \"Login\" button to the right by setting <kbd>justify-content: space-between</kbd> on the nav.",
|
||||
"task": "The Login button should sit on the far right, with the other links staying on the left. Distribute the space between them.",
|
||||
"previewHTML": "<nav class=\"nav\"><div class=\"links\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a></div><a href=\"#\" class=\"login\">Login</a></nav>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .links { display: flex; gap: 8px; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); } .login { background: steelblue; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Which <kbd>justify-content</kbd> value pushes the first and last items to opposite edges?"
|
||||
"message": "Use the property that distributes items along the main axis"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -72,7 +72,7 @@
|
||||
"id": "flexbox-4",
|
||||
"title": "Align Items",
|
||||
"description": "<kbd>align-items</kbd> controls alignment on the cross axis (vertical when flex-direction is row). Values include:<br>• <kbd>stretch</kbd> – stretch to fill (default)<br>• <kbd>flex-start</kbd> – align to top<br>• <kbd>flex-end</kbd> – align to bottom<br>• <kbd>center</kbd> – center vertically",
|
||||
"task": "The logo and nav links have different heights. Center them vertically with <kbd>align-items: center</kbd>.",
|
||||
"task": "The logo and nav links sit at different heights. Center them vertically so they line up.",
|
||||
"previewHTML": "<header class=\"header\"><div class=\"logo\">ACME</div><nav><a href=\"#\">Products</a><a href=\"#\">Pricing</a><a href=\"#\">Docs</a></nav></header>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .header { background: white; padding: 1rem 2rem; display: flex; justify-content: space-between; border-bottom: 1px solid #eee; } .logo { font-size: 1.5rem; font-weight: bold; color: steelblue; } nav { display: flex; gap: 1rem; } nav a { color: #333; text-decoration: none; font-size: 0.9rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Which property aligns flex items along the cross axis?"
|
||||
"message": "Use the property that controls cross-axis alignment"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -93,7 +93,7 @@
|
||||
"id": "flexbox-5",
|
||||
"title": "Flex Wrap",
|
||||
"description": "By default, flex items squeeze onto one line. <kbd>flex-wrap: wrap</kbd> allows items to flow onto multiple lines when they run out of space.",
|
||||
"task": "These cards overflow the container. Add <kbd>flex-wrap: wrap</kbd> to allow them to wrap to new rows.",
|
||||
"task": "The cards overflow the container instead of fitting within it. Allow the items to flow onto new rows when they run out of space.",
|
||||
"previewHTML": "<div class=\"cards\"><article class=\"card\">Card 1</article><article class=\"card\">Card 2</article><article class=\"card\">Card 3</article><article class=\"card\">Card 4</article><article class=\"card\">Card 5</article><article class=\"card\">Card 6</article></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .cards { display: flex; gap: 1rem; } .card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); min-width: 120px; text-align: center; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Which property allows flex items to flow onto multiple lines?"
|
||||
"message": "Use the property that allows flex items to wrap onto new lines"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -114,7 +114,7 @@
|
||||
"id": "flexbox-6",
|
||||
"title": "Flex Grow",
|
||||
"description": "The <kbd>flex</kbd> property on items controls how they grow and shrink. <kbd>flex: 1</kbd> makes an item grow to fill available space. Multiple items with <kbd>flex: 1</kbd> share space equally.",
|
||||
"task": "Make the search input expand to fill available space by setting <kbd>flex: 1</kbd> on <kbd>.search</kbd>.",
|
||||
"task": "The search input is too narrow. Make it stretch to fill all the remaining space in the toolbar.",
|
||||
"previewHTML": "<div class=\"toolbar\"><input class=\"search\" type=\"text\" placeholder=\"Search...\"><button class=\"btn\">Search</button><button class=\"btn\">Filters</button></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .toolbar { display: flex; gap: 8px; padding: 1rem; background: #f5f5f5; border-radius: 8px; } .search { padding: 8px 1rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; } .btn { padding: 8px 1rem; background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -125,9 +125,9 @@
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Which property makes a flex item grow to fill the remaining space?"
|
||||
"type": "regex",
|
||||
"value": "(flex\\s*:\\s*1|flex-grow\\s*:\\s*1)",
|
||||
"message": "Use the property that makes a flex item grow to fill available space"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Which <kbd>display</kbd> value activates the CSS Grid layout system?"
|
||||
"message": "Set <kbd>display: grid</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "grid-template-columns:\\s*repeat\\(\\s*3\\s*,\\s*1fr\\s*\\)",
|
||||
"message": "Which CSS property defines column sizes in a grid? Use <kbd>repeat()</kbd> with the <kbd>fr</kbd> unit for equal columns.",
|
||||
"message": "Set <kbd>grid-template-columns: repeat(3, 1fr)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Which CSS property adds spacing between grid cells without affecting the outer edges?"
|
||||
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "grid-column:\\s*span\\s+2",
|
||||
"message": "Which CSS property makes a grid item stretch across multiple columns? Use the <kbd>span</kbd> keyword.",
|
||||
"message": "Set <kbd>grid-column: span 2</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -108,7 +108,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "grid-template-columns:\\s*repeat\\(\\s*auto-fit\\s*,\\s*minmax\\(\\s*150px\\s*,\\s*1fr\\s*\\)\\s*\\)",
|
||||
"message": "Which CSS property creates responsive columns? Combine <kbd>auto-fit</kbd> with <kbd>minmax()</kbd> for flexible sizing.",
|
||||
"message": "Set <kbd>grid-template-columns: repeat(auto-fit, minmax(150px, 1fr))</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Która właściwość kontroluje kolor tekstu?"
|
||||
"message": "Dodaj <kbd>color: coral;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Sprawdź właściwość <kbd>background</kbd> — jaki kolor potrzebuje karta?"
|
||||
"message": "Dodaj <kbd>background: lavender;</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Element potrzebuje wewnętrznej przestrzeni — sprawdź właściwość <kbd>padding</kbd>"
|
||||
"message": "Dodaj <kbd>padding: 1rem;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Która właściwość kontroluje kolor tekstu?"
|
||||
"message": "Ustaw <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Sprawdź właściwość <kbd>color</kbd> — jaki kolor potrzebują linki?"
|
||||
"message": "Ustaw <kbd>color: coral</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "Sprawdź właściwość <kbd>background</kbd> — jaki kolor potrzebuje badge?"
|
||||
"message": "Ustaw <kbd>background: tomato</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Sprawdź właściwość <kbd>background</kbd> — jaki kolor potrzebuje przycisk?"
|
||||
"message": "Ustaw <kbd>background: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Która właściwość kontroluje podkreślenie linków?"
|
||||
"message": "Ustaw <kbd>text-decoration: none</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Która właściwość kontroluje kolor tekstu nagłówków?"
|
||||
"message": "Ustaw <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Sprawdź właściwość <kbd>color</kbd> — jaki kolor potrzebują linki nawigacji?"
|
||||
"message": "Ustaw <kbd>color: white</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Która właściwość kontroluje rozmiar tekstu?"
|
||||
"message": "Ustaw <kbd>font-size: 0.9rem</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Element potrzebuje wewnętrznej przestrzeni — sprawdź właściwość <kbd>padding</kbd>"
|
||||
"message": "Która właściwość dodaje przestrzeń między treścią a krawędzią elementu?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Sprawdź właściwość <kbd>border-left</kbd> — jakiej szerokości, stylu i koloru potrzebujesz?",
|
||||
"message": "Użyj skrótu, który ustawia ramkę tylko po jednej stronie",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Która właściwość kontroluje przestrzeń pod elementem?"
|
||||
"message": "Która właściwość odpycha sąsiednie elementy w dół?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Która wartość <kbd>box-sizing</kbd> włącza padding i ramkę do szerokości?"
|
||||
"message": "Który tryb rozmiaru uwzględnia padding i ramkę w szerokości elementu?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Sprawdź skrót <kbd>padding</kbd> — dwie wartości oznaczają pion i poziom",
|
||||
"message": "Użyj skrótu dwuwartościowego: najpierw pionowo, potem poziomo",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Sprawdź skrót <kbd>margin</kbd> — jak automatycznie wycentrować element poziomo?",
|
||||
"message": "Użyj skrótu, który automatycznie oblicza równe marginesy poziome",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Która wartość <kbd>border-radius</kbd> tworzy pełne koło?"
|
||||
"message": "Która właściwość zaokrągla rogi? Pomyśl, jaki procent tworzy koło"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Element potrzebuje wewnętrznej przestrzeni — sprawdź właściwość <kbd>padding</kbd>"
|
||||
"message": "Dodaj wewnętrzny odstęp do powiadomienia"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Sprawdź właściwość <kbd>border-left</kbd> — jaki styl akcentu potrzebuje powiadomienie?",
|
||||
"message": "Dodaj kolorowy akcent na lewej krawędzi",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Element potrzebuje zaokrąglonych rogów — sprawdź właściwość <kbd>border-radius</kbd>"
|
||||
"message": "Wygładź rogi powiadomienia"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Która właściwość ogranicza maksymalną szerokość elementu?"
|
||||
"message": "Ustaw <kbd>max-width: 40rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "steelblue",
|
||||
"message": "Jaki kolor powinna mieć zmienna brand?",
|
||||
"message": "Ustaw wartość na <kbd>steelblue</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Sprawdź funkcję <kbd>calc()</kbd> — jak obliczyć szerokość minus sidebar?",
|
||||
"message": "Ustaw <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Która właściwość zapewnia minimalną wysokość na cały viewport?"
|
||||
"message": "Ustaw <kbd>min-height: 100vh</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Sprawdź właściwość <kbd>transition</kbd> — jaką właściwość i czas trwania podać?",
|
||||
"message": "Ustaw <kbd>transition: background-color 0.3s</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Która wartość tworzy płynne przyspieszenie i spowolnienie?"
|
||||
"message": "Ustaw timing na <kbd>ease-in-out</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -83,7 +83,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "50%.*transform: translateY\\(-20px\\)",
|
||||
"message": "W połowie animacji piłka powinna podskoczyć w górę — sprawdź <kbd>transform</kbd>",
|
||||
"message": "Przy <kbd>50%</kbd>, użyj <kbd>transform: translateY(-20px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Sprawdź skrót <kbd>animation</kbd> — podaj nazwę, czas trwania i powtarzanie",
|
||||
"message": "Zastosuj <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Która właściwość wskazuje nazwę animacji do zastosowania?"
|
||||
"message": "Ustaw <kbd>animation-name: pulse</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Sprawdź właściwość <kbd>animation-duration</kbd> — jak długo trwa jeden cykl?"
|
||||
"message": "Ustaw <kbd>animation-duration: 2s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Sprawdź właściwość <kbd>animation-delay</kbd> — ile czeka przed startem?"
|
||||
"message": "Ustaw <kbd>animation-delay: 1s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Sprawdź właściwość <kbd>animation-iteration-count</kbd> — ile razy ma się powtórzyć?"
|
||||
"message": "Ustaw <kbd>animation-iteration-count: 2</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Która wartość <kbd>animation-fill-mode</kbd> zachowuje końcowy stan animacji?"
|
||||
"message": "Ustaw <kbd>animation-fill-mode: forwards</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Sprawdź właściwość <kbd>background</kbd> — jaki kolor potrzebuje panel na małych ekranach?",
|
||||
"message": "Ustaw <kbd>background: lightcoral</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -53,11 +53,7 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Sprawdź właściwość <kbd>font-size</kbd> — która jednostka skaluje się z viewport?"
|
||||
}
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Ustaw <kbd>font-size: 5vw</kbd>" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -77,7 +73,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Która wartość <kbd>display</kbd> włącza układ siatkowy?"
|
||||
"message": "Ustaw <kbd>display: grid</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -88,7 +84,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Sprawdź właściwość <kbd>gap</kbd> — jaki odstęp potrzebują elementy siatki?"
|
||||
"message": "Ustaw <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -121,7 +117,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Sprawdź właściwość <kbd>width</kbd> — jaką stałą szerokość potrzebuje sidebar?",
|
||||
"message": "Ustaw <kbd>width: 250px</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Która właściwość <kbd>display</kbd> tworzy kontener flex?"
|
||||
"message": "Ustaw <kbd>display: flex</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Sprawdź właściwość <kbd>gap</kbd> — jaki odstęp potrzebują elementy?"
|
||||
"message": "Ustaw <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Która wartość <kbd>justify-content</kbd> rozdziela elementy na końce?"
|
||||
"message": "Ustaw <kbd>justify-content: space-between</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Która właściwość kontroluje wyrównanie na osi poprzecznej?"
|
||||
"message": "Ustaw <kbd>align-items: center</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Sprawdź właściwość <kbd>flex-wrap</kbd> — jak pozwolić elementom przenosić się na nowe linie?"
|
||||
"message": "Ustaw <kbd>flex-wrap: wrap</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Sprawdź właściwość <kbd>flex</kbd> — jak sprawić, by element wypełnił dostępną przestrzeń?"
|
||||
"message": "Ustaw <kbd>flex: 1</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Яка властивість керує кольором тексту?"
|
||||
"message": "Додайте <kbd>color: coral;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Перевірте властивість <kbd>background</kbd>"
|
||||
"message": "Додайте <kbd>background: lavender;</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Картка потребує простору всередині її меж"
|
||||
"message": "Додайте <kbd>padding: 1rem;</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Яка властивість змінює колір тексту?"
|
||||
"message": "Встановіть <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Яке значення дає теплий червонувато-оранжевий колір?"
|
||||
"message": "Встановіть <kbd>color: coral</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "Значку потрібен яскравий червоний фон"
|
||||
"message": "Встановіть <kbd>background: tomato</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Яка властивість встановлює колір заливки кнопки?"
|
||||
"message": "Встановіть <kbd>background: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Яка властивість керує підкресленням посилань?"
|
||||
"message": "Встановіть <kbd>text-decoration: none</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Перевірте властивість <kbd>color</kbd>"
|
||||
"message": "Встановіть <kbd>color: steelblue</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Посилання мають виділятися на синьому фоні"
|
||||
"message": "Встановіть <kbd>color: white</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Перевірте властивість <kbd>font-size</kbd> — текст має бути трохи меншим"
|
||||
"message": "Встановіть <kbd>font-size: 0.9rem</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": "",
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Яка властивість додає простір між вмістом елемента та його межею?"
|
||||
"message": "Яка властивість додає простір між контентом і краєм елемента?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Використайте скорочення <kbd>border-left</kbd> зі значеннями ширини, стилю та кольору",
|
||||
"message": "Використайте скорочення, яке встановлює межу лише з одного боку",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Яка властивість створює простір знизу елемента, відштовхуючи сусідів?"
|
||||
"message": "Яка властивість відштовхує сусідні елементи знизу?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Яке значення <kbd>box-sizing</kbd> включає padding та межу в загальну ширину елемента?"
|
||||
"message": "Який режим розміру включає padding і межу в ширину елемента?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Використайте скорочення <kbd>padding</kbd> з двома значеннями: вертикальне та горизонтальне",
|
||||
"message": "Використайте скорочення з двома значеннями: спочатку вертикальне, потім горизонтальне",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Використайте <kbd>margin</kbd> з ключовим словом, яке автоматично обчислює рівні ліві та праві відступи",
|
||||
"message": "Використайте скорочення, яке автоматично розраховує рівні горизонтальні поля",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -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": "",
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Який відсоток <kbd>border-radius</kbd> створює ідеальне коло з квадратного елемента?"
|
||||
"message": "Яка властивість заокруглює кути? Подумайте, який відсоток створює коло"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -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": "",
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Додайте внутрішній відступ до картки сповіщення"
|
||||
"message": "Додайте внутрішній відступ до сповіщення"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Додайте лівий акцент за допомогою скорочення <kbd>border-left</kbd>",
|
||||
"message": "Додайте кольоровий акцент на лівому краю",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Злегка заокругліть кути за допомогою <kbd>border-radius</kbd>"
|
||||
"message": "Згладьте кути сповіщення"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Яка властивість обмежує ширину елемента? Спробуйте значення в <kbd>rem</kbd> для комфортної довжини рядка."
|
||||
"message": "Встановіть <kbd>max-width: 40rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "steelblue",
|
||||
"message": "Встановіть значення на <kbd>steelblue</kbd>",
|
||||
"message": "Встановіть значення <kbd>steelblue</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Використайте <kbd>calc()</kbd>, щоб відняти фіксовану ширину сайдбару від повної ширини контейнера.",
|
||||
"message": "Встановіть <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Яка властивість забезпечує мінімальну висоту? Використайте одиницю viewport для повноекранного покриття."
|
||||
"message": "Встановіть <kbd>min-height: 100vh</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Вкажіть, яку властивість анімувати та скільки це має тривати.",
|
||||
"message": "Встановіть <kbd>transition: background-color 0.3s</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Яке ключове слово пом'якшення починається повільно, прискорюється, а потім знову сповільнюється?"
|
||||
"message": "Встановіть timing на <kbd>ease-in-out</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Використайте скорочення <kbd>animation</kbd>: назва, тривалість та кількість повторень.",
|
||||
"message": "Застосуйте <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Яка властивість пов'язує елемент з іменованим правилом <kbd>@keyframes</kbd>?"
|
||||
"message": "Встановіть <kbd>animation-name: pulse</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Скільки має тривати один повний цикл анімації?"
|
||||
"message": "Встановіть <kbd>animation-duration: 2s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Яка властивість змушує анімацію зачекати перед початком?"
|
||||
"message": "Встановіть <kbd>animation-delay: 1s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Яка властивість контролює кількість повторень анімації?"
|
||||
"message": "Встановіть <kbd>animation-iteration-count: 2</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Яка властивість зберігає стиль елемента в його фінальному стані keyframe після завершення анімації?"
|
||||
"message": "Встановіть <kbd>animation-fill-mode: forwards</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||
"message": "Почніть з правила <kbd>@media</kbd> — яка умова націлюється на екрани шириною 600px або менше?",
|
||||
"message": "Використайте <kbd>@media (max-width: 600px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Яка властивість змінює колір фону елемента?",
|
||||
"message": "Встановіть <kbd>background: lightcoral</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Яка одиниця CSS масштабується відносно ширини viewport?"
|
||||
"message": "Встановіть <kbd>font-size: 5vw</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -77,18 +77,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Який режим display дозволяє визначати рядки та колонки?"
|
||||
"message": "Встановіть <kbd>display: grid</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||
"message": "Спробуйте <kbd>repeat()</kbd> з <kbd>auto-fit</kbd> та <kbd>minmax()</kbd> — які мінімальний та максимальний розміри створять гнучкі колонки?",
|
||||
"message": "Використайте <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Яка властивість додає простір між елементами grid?"
|
||||
"message": "Встановіть <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -109,7 +109,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||
"message": "Яка умова <kbd>@media</kbd> застосовує стилі, коли viewport має ширину щонайменше 768px?",
|
||||
"message": "Використайте <kbd>@media (min-width: 768px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -121,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Яка властивість контролює ширину сайдбару на великих екранах?",
|
||||
"message": "Встановіть <kbd>width: 250px</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Яке значення display перетворює елемент на гнучкий контейнер?"
|
||||
"message": "Встановіть <kbd>display: flex</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Яка властивість створює відстань між flex-елементами без використання margin?"
|
||||
"message": "Встановіть <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Яке значення <kbd>justify-content</kbd> розміщує перший та останній елементи на протилежних краях?"
|
||||
"message": "Встановіть <kbd>justify-content: space-between</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Яка властивість вирівнює flex-елементи вздовж поперечної осі?"
|
||||
"message": "Встановіть <kbd>align-items: center</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Яка властивість дозволяє flex-елементам переходити на кілька рядків?"
|
||||
"message": "Встановіть <kbd>flex-wrap: wrap</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Яка властивість змушує flex-елемент зростати, щоб заповнити залишковий простір?"
|
||||
"message": "Встановіть <kbd>flex: 1</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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": {
|
||||
|
||||
76
specs/003-flexbox-task-wording/plan.md
Normal file
76
specs/003-flexbox-task-wording/plan.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Implementation Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Rewrite all 6 flexbox lesson task descriptions to describe the desired visual outcome instead of giving the exact CSS declaration. Update validation messages to hint without revealing answers, and accept alternative valid solutions where applicable.
|
||||
|
||||
## Approach
|
||||
|
||||
This is a content-only change to a single JSON file (`lessons/flexbox.json`). Each lesson needs three edits:
|
||||
|
||||
1. **Task text**: Replace copy-pasteable CSS declarations with outcome-oriented descriptions
|
||||
2. **Validation messages**: Replace answer-revealing messages with pedagogical hints
|
||||
3. **Validations array**: Add alternative accepted solutions where multiple CSS approaches achieve the same visual result
|
||||
|
||||
The lesson `description` fields (which teach concepts with code examples) remain unchanged — they are the learning material, not the exercise prompt.
|
||||
|
||||
## File Mapping
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `lessons/flexbox.json` | modify | Rewrite `task` and validation `message` fields for all 6 lessons; add alternative validations for flexbox-6 |
|
||||
|
||||
No new files need to be created. No validator code changes needed — the existing `property_value` and `regex` validation types already support everything required.
|
||||
|
||||
## Detailed Changes Per Lesson
|
||||
|
||||
### flexbox-1 (Container)
|
||||
- **Task**: Describe that nav links stack vertically and should display side by side
|
||||
- **Validation msg**: Hint at display property for flex layout
|
||||
- **Alt solutions**: None — `display: flex` is the only correct answer (inline-flex changes block behavior)
|
||||
|
||||
### flexbox-2 (Gap)
|
||||
- **Task**: Describe that links are crammed together and need 1rem of spacing between them
|
||||
- **Validation msg**: Hint at the gap property
|
||||
- **Alt solutions**: None — `gap: 1rem` is the specific expected value
|
||||
|
||||
### flexbox-3 (Justify Content)
|
||||
- **Task**: Describe that Login button should be pushed to the far right, with nav links on the left
|
||||
- **Validation msg**: Hint at main-axis distribution property
|
||||
- **Alt solutions**: None — `justify-content: space-between` is the only property that works when targeting `.nav`
|
||||
|
||||
### flexbox-4 (Align Items)
|
||||
- **Task**: Describe the visual misalignment and ask for vertical centering
|
||||
- **Validation msg**: Hint at cross-axis alignment property
|
||||
- **Alt solutions**: None — `align-items: center` is the correct answer
|
||||
|
||||
### flexbox-5 (Flex Wrap)
|
||||
- **Task**: Describe cards overflowing and needing to flow onto new rows
|
||||
- **Validation msg**: Hint at wrapping property
|
||||
- **Alt solutions**: None — `flex-wrap: wrap` is the only answer
|
||||
|
||||
### flexbox-6 (Flex Grow)
|
||||
- **Task**: Describe that the search input should stretch to fill remaining space
|
||||
- **Validation msg**: Hint at flex growth property
|
||||
- **Alt solutions**: Accept both `flex: 1` and `flex-grow: 1` via regex validation
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
1. **No validator code changes**: The existing `regex` validation type can handle alternative solutions for flexbox-6. No need to add a new validation type.
|
||||
2. **Keep values in tasks where needed**: Some tasks mention target values like "1rem" since the validator checks exact values and students need to know the amount. The key change is removing the *property name* from the task.
|
||||
3. **Solution field unchanged**: The `solution` field is used for the "show solution" feature and should remain as the canonical answer.
|
||||
4. **codePrefix unchanged**: The existing codePrefix already shows the selector context (e.g., `.nav {`), which is enough guidance for students.
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|-----------|------------|
|
||||
| Tasks become too vague for beginners | Low | Descriptions still teach the property; tasks describe specific visual outcomes |
|
||||
| Alternative regex validation too permissive | Low | Regex will be specific to `flex:\s*1` and `flex-grow:\s*1` patterns |
|
||||
| Validation messages too cryptic | Low | Messages will hint at the property category without giving the exact declaration |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Run existing test suite**: `npm run test` — all tests should pass since no code or module structure changes
|
||||
2. **Manual verification**: Validate that each rewritten task accurately describes the visual outcome shown in the preview
|
||||
3. **JSON schema validation**: Ensure `lessons/flexbox.json` still conforms to the module schema
|
||||
35
specs/003-flexbox-task-wording/spec.md
Normal file
35
specs/003-flexbox-task-wording/spec.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# fix: remove answers from flexbox task descriptions (copy-paste score 95%)
|
||||
|
||||
**Issue**: [libretech/code-crispies#3](https://git.librete.ch/libretech/code-crispies/issues/3)
|
||||
**State**: open
|
||||
**Author**: libretech
|
||||
**Labels**: none
|
||||
**Complexity**: simple
|
||||
|
||||
## Issue Body
|
||||
|
||||
Pedagogy audit: All 6 flexbox exercises give the exact CSS declaration in the task text. Students type without understanding. Rewrite tasks to describe the DESIRED OUTCOME instead of the exact code. Example: 'Add display: flex' → 'The navigation links stack vertically. Make them display side by side.' Accept multiple valid solutions in validations.
|
||||
|
||||
## Current State
|
||||
|
||||
All 6 lessons in `lessons/flexbox.json` have task descriptions that include the exact CSS declaration students need to type:
|
||||
|
||||
| Lesson | Current Task (gives away answer) |
|
||||
|--------|----------------------------------|
|
||||
| flexbox-1 | "Add `display: flex` to `.nav`" |
|
||||
| flexbox-2 | "Add `gap: 1rem` to space out..." |
|
||||
| flexbox-3 | "setting `justify-content: space-between` on the nav" |
|
||||
| flexbox-4 | "Center them vertically with `align-items: center`" |
|
||||
| flexbox-5 | "Add `flex-wrap: wrap` to allow them to wrap" |
|
||||
| flexbox-6 | "setting `flex: 1` on `.search`" |
|
||||
|
||||
Validation error messages also give away answers (e.g., "Set `display: flex`").
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. All 6 flexbox task descriptions rewritten to describe the desired visual outcome, not the exact CSS code
|
||||
2. Students cannot copy-paste from the task into the editor to pass
|
||||
3. Validation error messages updated to provide hints without revealing the exact declaration
|
||||
4. Where applicable, validations accept multiple valid CSS solutions (e.g., `flex: 1` and `flex-grow: 1`)
|
||||
5. Existing tests continue to pass
|
||||
6. Lesson descriptions (which teach the concepts) remain unchanged
|
||||
13
specs/003-flexbox-task-wording/tasks.md
Normal file
13
specs/003-flexbox-task-wording/tasks.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Tasks
|
||||
|
||||
## Phase 1: Core Content Changes
|
||||
- [X] Task 1.1: Rewrite task text for all 6 flexbox lessons to describe visual outcomes [P]
|
||||
- [X] Task 1.2: Rewrite validation error messages to hint without revealing answers [P]
|
||||
|
||||
## Phase 2: Alternative Validations
|
||||
- [X] Task 2.1: Add regex validation for flexbox-6 to accept both `flex: 1` and `flex-grow: 1`
|
||||
|
||||
## Phase 3: Validation
|
||||
- [X] Task 3.1: Run existing test suite to confirm no regressions
|
||||
- [X] Task 3.2: Verify flexbox.json still conforms to module schema
|
||||
- [X] Task 3.3: Run lesson format check (`npm run format.lessons`)
|
||||
@@ -1,87 +0,0 @@
|
||||
# Implementation Plan
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Rewrite all answer-revealing validation error messages across lesson JSON files to use pedagogical hints (concept questions, property-name nudges, directional guidance) instead of literal CSS solutions. This eliminates the fail-then-copy anti-pattern and promotes genuine learning.
|
||||
|
||||
## 2. Approach
|
||||
|
||||
**Phase-based, content-first strategy:**
|
||||
|
||||
1. Define a message style guide with 3 hint categories:
|
||||
- **Concept question:** "Which property adds space inside an element?" (for property discovery)
|
||||
- **Property hint:** "Check the `padding` property" (when the property is known but value is wrong)
|
||||
- **Directional nudge:** "The items need to wrap to the next line" (for layout concepts)
|
||||
|
||||
2. Rewrite English priority modules first (flexbox, box-model, colors, positioning) — these are 100% answer-revealing and form the template for all other rewrites.
|
||||
|
||||
3. Rewrite remaining English modules, reusing the same hint patterns established in step 2.
|
||||
|
||||
4. Update localized variants with equivalent pedagogical messages in each target language (ar, de, es, pl, uk), translating the English hints while preserving natural phrasing in each language.
|
||||
|
||||
5. Run `npm run format.lessons` to ensure consistent formatting, then run tests.
|
||||
|
||||
## 3. File Mapping
|
||||
|
||||
### Files to modify (message field only, no validation logic changes):
|
||||
|
||||
**English priority (create → N/A, modify → 4, delete → N/A):**
|
||||
- `lessons/flexbox.json` — modify 6 messages
|
||||
- `lessons/01-box-model.json` — modify 10 messages
|
||||
- `lessons/03-colors.json` — modify 4 messages
|
||||
- `lessons/12-positioning.json` — modify 5 messages
|
||||
|
||||
**English remaining (modify → 13):**
|
||||
- `lessons/00-basics.json` — modify 4 messages
|
||||
- `lessons/00-basic-selectors.json` — modify 15 messages
|
||||
- `lessons/01-advanced-selectors.json` — modify 8 messages
|
||||
- `lessons/04-typography.json` — modify 1 message
|
||||
- `lessons/05-units-variables.json` — modify 3 messages
|
||||
- `lessons/06-transitions-animations.json` — modify 8 messages
|
||||
- `lessons/07-layouts.json` — modify 8 messages
|
||||
- `lessons/08-responsive.json` — modify 8 messages
|
||||
- `lessons/09-gradients.json` — modify 3 messages
|
||||
- `lessons/10-tailwind-basics.json` — modify 16 messages
|
||||
- `lessons/11-filters.json` — modify 4 messages
|
||||
- `lessons/13-pseudo-elements.json` — modify 4 messages
|
||||
- `lessons/grid.json` — modify 5 messages
|
||||
|
||||
**Localized variants (modify):**
|
||||
- `lessons/ar/flexbox.json`, `lessons/ar/01-box-model.json`, + other ar/ modules with answer-revealing messages
|
||||
- `lessons/de/flexbox.json`, `lessons/de/01-box-model.json`, + other de/ modules
|
||||
- `lessons/es/flexbox.json`, `lessons/es/01-box-model.json`, + other es/ modules
|
||||
- `lessons/pl/flexbox.json`, `lessons/pl/01-box-model.json`, + other pl/ modules
|
||||
- `lessons/uk/flexbox.json`, `lessons/uk/01-box-model.json`, + other uk/ modules
|
||||
|
||||
**No new files or deleted files.**
|
||||
|
||||
## 4. Architecture Decisions
|
||||
|
||||
1. **Message-only changes:** Only the `"message"` string within validation objects is modified. The `type`, `value`, and `options` fields remain untouched. This preserves all validation logic.
|
||||
|
||||
2. **No code changes to validator.js:** The validator reads the `message` field as a passthrough string for display. No runtime changes needed.
|
||||
|
||||
3. **Hint style per validation type:**
|
||||
- `property_value` validations → concept question or property hint (since the property and value are tested programmatically, the message should teach the concept, not repeat the answer)
|
||||
- `regex` validations → directional nudge describing the expected pattern conceptually
|
||||
- `contains` / `contains_class` validations → concept question about what to include
|
||||
|
||||
4. **Localization approach:** Each localized message should be a natural translation of the English pedagogical hint, not a word-for-word translation. The hint category (question, nudge, property hint) should match the English version.
|
||||
|
||||
5. **Preserve `<kbd>` tags selectively:** `<kbd>` tags may still be used for property names (e.g., "Check the `<kbd>padding</kbd>` property") but never for complete property-value pairs that reveal the answer.
|
||||
|
||||
## 5. Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|-----------|------------|
|
||||
| Pedagogical hints are too vague, frustrating learners | Medium | Each hint should name the relevant CSS property or concept — just not the exact value. The task description already provides context. |
|
||||
| Localized translations lose pedagogical intent | Medium | Use consistent hint categories across languages. Review each language for natural phrasing. |
|
||||
| Existing tests assert on specific message text | Low | Check test files for hardcoded message assertions before changing. Adjust tests if needed. |
|
||||
| Formatting inconsistency after bulk edits | Low | Run `npm run format.lessons` after all changes. |
|
||||
|
||||
## 6. Testing Strategy
|
||||
|
||||
1. **Existing test suite:** Run `npm run test` to verify no regressions. The validator tests should pass since validation logic is unchanged.
|
||||
2. **Grep audit:** After changes, grep all lesson files for remaining "Set <kbd>" patterns to confirm none were missed.
|
||||
3. **JSON validity:** Ensure all modified JSON files parse correctly (the format.lessons command will catch syntax errors).
|
||||
4. **Manual spot-check:** Verify a few lessons in the dev server to confirm messages display correctly in the UI.
|
||||
@@ -1,50 +0,0 @@
|
||||
# fix: validation error messages reveal the solution instead of guiding learning
|
||||
|
||||
**Issue:** [#4](https://git.librete.ch/libretech/code-crispies/issues/4)
|
||||
**Repository:** libretech/code-crispies
|
||||
**Author:** libretech
|
||||
**State:** open
|
||||
**Labels:** none
|
||||
|
||||
## Issue Body
|
||||
|
||||
Pedagogy audit: 88% of exercises reveal the answer in error messages, creating a fail-then-copy loop. Change validation messages from 'Set padding: 1rem' to 'Which property adds space between content and the element edge?' This applies across all modules — start with flexbox, box-model, and colors (the 3 worst offenders).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Validation error messages in **flexbox**, **box-model**, and **colors** modules must no longer reveal the exact CSS property-value answer
|
||||
2. Replacement messages should use pedagogical hints: concept questions, property-name hints, or directional guidance — never the literal solution
|
||||
3. All remaining English lesson modules with answer-revealing messages must also be rewritten
|
||||
4. Localized variants (ar/, de/, es/, pl/, uk/) of affected modules must be updated with equivalent pedagogical messages in each language
|
||||
5. Existing validations (type, value, options) must remain unchanged — only the `"message"` field is modified
|
||||
6. All existing tests must continue to pass
|
||||
|
||||
## Scope
|
||||
|
||||
### English priority modules (100% answer-revealing):
|
||||
- `lessons/flexbox.json` — 6 messages
|
||||
- `lessons/01-box-model.json` — 10 messages
|
||||
- `lessons/03-colors.json` — 4 messages
|
||||
- `lessons/12-positioning.json` — 5 messages
|
||||
|
||||
### English remaining modules (partial answer-revealing):
|
||||
- `lessons/00-basics.json` — 4 of 26
|
||||
- `lessons/00-basic-selectors.json` — 15 of 18
|
||||
- `lessons/01-advanced-selectors.json` — 8 of 49
|
||||
- `lessons/04-typography.json` — 1 of 9
|
||||
- `lessons/05-units-variables.json` — 3 of 5
|
||||
- `lessons/06-transitions-animations.json` — 8 of 13
|
||||
- `lessons/07-layouts.json` — 8 of 11
|
||||
- `lessons/08-responsive.json` — 8 of 10
|
||||
- `lessons/09-gradients.json` — 3 of 7
|
||||
- `lessons/10-tailwind-basics.json` — 16 of 17
|
||||
- `lessons/11-filters.json` — 4 of 7
|
||||
- `lessons/13-pseudo-elements.json` — 4 of 8
|
||||
- `lessons/grid.json` — 5 of 9
|
||||
|
||||
### Localized variants (each language directory):
|
||||
- `lessons/ar/` — Arabic
|
||||
- `lessons/de/` — German
|
||||
- `lessons/es/` — Spanish
|
||||
- `lessons/pl/` — Polish
|
||||
- `lessons/uk/` — Ukrainian
|
||||
@@ -1,39 +0,0 @@
|
||||
# Tasks
|
||||
|
||||
## Phase 1: Preparation
|
||||
- [X] Task 1.1: Audit existing tests for hardcoded validation message assertions; note any that need updating
|
||||
- [X] Task 1.2: Read each priority English module and draft replacement messages using the hint style guide (concept question / property hint / directional nudge)
|
||||
|
||||
## Phase 2: English Priority Modules (100% answer-revealing)
|
||||
- [X] Task 2.1: Rewrite validation messages in `lessons/flexbox.json` (6 messages) [P]
|
||||
- [X] Task 2.2: Rewrite validation messages in `lessons/01-box-model.json` (10 messages) [P]
|
||||
- [X] Task 2.3: Rewrite validation messages in `lessons/03-colors.json` (4 messages) [P]
|
||||
- [X] Task 2.4: Rewrite validation messages in `lessons/12-positioning.json` (5 messages) [P]
|
||||
|
||||
## Phase 3: English Remaining Modules
|
||||
- [X] Task 3.1: Rewrite messages in `lessons/00-basic-selectors.json` (15 messages) [P]
|
||||
- [X] Task 3.2: Rewrite messages in `lessons/00-basics.json` (4 messages) [P]
|
||||
- [X] Task 3.3: Rewrite messages in `lessons/01-advanced-selectors.json` (8 messages) [P]
|
||||
- [X] Task 3.4: Rewrite messages in `lessons/04-typography.json` (1 message) [P]
|
||||
- [X] Task 3.5: Rewrite messages in `lessons/05-units-variables.json` (3 messages) [P]
|
||||
- [X] Task 3.6: Rewrite messages in `lessons/06-transitions-animations.json` (8 messages) [P]
|
||||
- [X] Task 3.7: Rewrite messages in `lessons/07-layouts.json` (8 messages) [P]
|
||||
- [X] Task 3.8: Rewrite messages in `lessons/08-responsive.json` (8 messages) [P]
|
||||
- [X] Task 3.9: Rewrite messages in `lessons/09-gradients.json` (3 messages) [P]
|
||||
- [X] Task 3.10: Rewrite messages in `lessons/10-tailwind-basics.json` (16 messages) [P]
|
||||
- [X] Task 3.11: Rewrite messages in `lessons/11-filters.json` (4 messages) [P]
|
||||
- [X] Task 3.12: Rewrite messages in `lessons/13-pseudo-elements.json` (4 messages) [P]
|
||||
- [X] Task 3.13: Rewrite messages in `lessons/grid.json` (5 messages) [P]
|
||||
|
||||
## Phase 4: Localized Variants
|
||||
- [X] Task 4.1: Update Arabic (ar/) localized modules with pedagogical messages [P]
|
||||
- [X] Task 4.2: Update German (de/) localized modules with pedagogical messages [P]
|
||||
- [X] Task 4.3: Update Spanish (es/) localized modules with pedagogical messages [P]
|
||||
- [X] Task 4.4: Update Polish (pl/) localized modules with pedagogical messages [P]
|
||||
- [X] Task 4.5: Update Ukrainian (uk/) localized modules with pedagogical messages [P]
|
||||
|
||||
## Phase 5: Validation & Polish
|
||||
- [X] Task 5.1: Run `npm run format.lessons` to ensure JSON formatting consistency
|
||||
- [X] Task 5.2: Run `npm run test` and fix any test failures related to message text assertions
|
||||
- [X] Task 5.3: Grep audit — verify no "Set <kbd>" answer-revealing patterns remain in any lesson file
|
||||
- [X] Task 5.4: Spot-check a few lessons via `npm start` to confirm messages render correctly in the UI
|
||||
77
specs/004-validation-messages/plan.md
Normal file
77
specs/004-validation-messages/plan.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Implementation Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Rewrite validation error messages in the box-model and colors lesson modules (and their localizations) so they guide learners toward the answer instead of revealing it. This breaks the "fail-then-copy" loop identified in the pedagogy audit.
|
||||
|
||||
## Approach
|
||||
|
||||
1. Rewrite each validation `message` field in the English box-model and colors JSON files using question/hint phrasing that describes the *concept* without stating the exact property-value pair
|
||||
2. Use the flexbox module's existing messages as the style guide
|
||||
3. Apply equivalent translations to all 5 localized box-model files (ar, de, es, pl, uk)
|
||||
4. Run the format-lessons script and tests to verify nothing breaks
|
||||
5. Commit as a docs/content fix (`fix:` conventional commit)
|
||||
|
||||
## File Mapping
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Action | Changes |
|
||||
|------|--------|---------|
|
||||
| `lessons/01-box-model.json` | modify | Rewrite 11 validation messages |
|
||||
| `lessons/03-colors.json` | modify | Rewrite 4 validation messages |
|
||||
| `lessons/ar/01-box-model.json` | modify | Translate 11 new guiding messages to Arabic |
|
||||
| `lessons/de/01-box-model.json` | modify | Translate 11 new guiding messages to German |
|
||||
| `lessons/es/01-box-model.json` | modify | Translate 11 new guiding messages to Spanish |
|
||||
| `lessons/pl/01-box-model.json` | modify | Translate 11 new guiding messages to Polish |
|
||||
| `lessons/uk/01-box-model.json` | modify | Translate 11 new guiding messages to Ukrainian |
|
||||
|
||||
### Files NOT Changed
|
||||
|
||||
- `lessons/flexbox.json` — already uses guiding messages
|
||||
- All localized flexbox files — already correct
|
||||
- No colors localizations exist
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
1. **Message style**: Use the same imperative hint style as flexbox ("Use the property that...", "Try the property that...") rather than pure questions. This is consistent with the existing codebase and gives just enough direction without revealing the answer.
|
||||
|
||||
2. **No `<kbd>` tags in new messages**: The current answer-revealing messages use `<kbd>` to format exact code. The new guiding messages should avoid `<kbd>` since they won't contain code literals — they describe concepts.
|
||||
|
||||
3. **Preserve validation logic**: Only the `message` field changes. The `type`, `value`, `options`, and all other fields remain untouched.
|
||||
|
||||
4. **Localization approach**: Translate the English guiding messages into each target language, maintaining the same hint/question style. Keep CSS property names untranslated (they are code).
|
||||
|
||||
## Message Mapping (English)
|
||||
|
||||
| Lesson | Current Message | New Message |
|
||||
|--------|----------------|-------------|
|
||||
| box-model-1 | Set `padding: 1rem` | Which property adds space between content and the element's edge? |
|
||||
| box-model-2 | Set `border-left: 4px solid steelblue` | Use the shorthand that sets a border on just one side |
|
||||
| box-model-3 | Set `margin-bottom: 1rem` | Which property pushes neighboring elements away from the bottom? |
|
||||
| box-model-4 | Set `box-sizing: border-box` | Which sizing mode includes padding and border in the element's width? |
|
||||
| box-model-5 | Set `padding: 8px 1rem` | Use the two-value shorthand: vertical first, then horizontal |
|
||||
| box-model-6 | Set `margin: 0 auto` | Use the shorthand that auto-calculates equal horizontal margins |
|
||||
| box-model-7 | Set `border-radius: 50%` | Which property rounds corners? Think about what percentage makes a circle |
|
||||
| box-model-8 v1 | Set `padding: 1rem` | Add inner spacing to the notification |
|
||||
| box-model-8 v2 | Set `border-left: 4px solid coral` | Add a colored accent on the left edge |
|
||||
| box-model-8 v3 | Set `border-radius: 4px` | Soften the corners of the notification |
|
||||
| colors-1 | Set `background-color: seashell` | Which property fills the area behind the content? |
|
||||
| colors-2 | Set `color: coral` | Which property changes the text color? |
|
||||
| colors-3 | Set `border-color: coral` | Which property changes just the border's color without redefining the whole border? |
|
||||
| colors-4 | Set `background-color: #ffd700` | Use the same background property, but with a hex code this time |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|-----------|------------|
|
||||
| Translation quality for 5 languages | Medium | Use consistent patterns; CSS property names stay in English; keep messages short |
|
||||
| Messages too vague, frustrating learners | Low | Each message still hints at the concept/direction; task descriptions already contain the answer for early lessons |
|
||||
| Schema validation failure | Very Low | Only `message` string changes; no structural changes |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Automated**: Run `npm run test` — existing unit tests validate the validator logic, not message content, so they should pass unchanged
|
||||
2. **Automated**: Run `npm run format.lessons` — ensures JSON formatting is correct
|
||||
3. **Manual verification**: Spot-check that each new message conceptually matches its lesson without revealing the answer
|
||||
4. **Schema validation**: JSON files reference the schema; any structural errors would be caught by the editor/tooling
|
||||
57
specs/004-validation-messages/spec.md
Normal file
57
specs/004-validation-messages/spec.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# fix: validation error messages reveal the solution instead of guiding learning
|
||||
|
||||
**Issue:** [#4](https://git.librete.ch/libretech/code-crispies/issues/4)
|
||||
**Repository:** libretech/code-crispies
|
||||
**Author:** libretech
|
||||
**State:** open
|
||||
**Labels:** none
|
||||
|
||||
## Issue Body
|
||||
|
||||
Pedagogy audit: 88% of exercises reveal the answer in error messages, creating a fail-then-copy loop. Change validation messages from 'Set padding: 1rem' to 'Which property adds space between content and the element edge?' This applies across all modules — start with flexbox, box-model, and colors (the 3 worst offenders).
|
||||
|
||||
## Scope
|
||||
|
||||
The three priority modules:
|
||||
|
||||
1. **Flexbox** (`lessons/flexbox.json`) — already uses guiding messages (0 messages need changes)
|
||||
2. **Box Model** (`lessons/01-box-model.json`) — 11 validation messages reveal exact answers
|
||||
3. **Colors** (`lessons/03-colors.json`) — 4 validation messages reveal exact answers
|
||||
|
||||
Localized versions that need corresponding updates:
|
||||
- `lessons/ar/01-box-model.json`
|
||||
- `lessons/de/01-box-model.json`
|
||||
- `lessons/es/01-box-model.json`
|
||||
- `lessons/pl/01-box-model.json`
|
||||
- `lessons/uk/01-box-model.json`
|
||||
|
||||
No localized versions exist for colors.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All validation messages in box-model module guide the learner instead of revealing the answer
|
||||
- [ ] All validation messages in colors module guide the learner instead of revealing the answer
|
||||
- [ ] Messages use question or hint phrasing (e.g., "Which property..." or "Try the property that...")
|
||||
- [ ] Messages never include the exact property-value pair that solves the exercise
|
||||
- [ ] All 5 localized box-model files receive equivalent translated guiding messages
|
||||
- [ ] Existing tests continue to pass (message content is not tested, only validation logic)
|
||||
- [ ] Lesson JSON files remain valid against the module schema
|
||||
|
||||
## Current vs Desired Pattern
|
||||
|
||||
**Current (answer-revealing):**
|
||||
```
|
||||
"message": "Set <kbd>padding: 1rem</kbd>"
|
||||
```
|
||||
|
||||
**Desired (guiding):**
|
||||
```
|
||||
"message": "Which property adds space between the content and the element's edge?"
|
||||
```
|
||||
|
||||
## Prior Art
|
||||
|
||||
The flexbox module already follows the desired pattern. Its messages serve as the style reference:
|
||||
- "Try changing the display mode to create a flex container"
|
||||
- "Use the property that adds spacing between flex items"
|
||||
- "Use the property that distributes items along the main axis"
|
||||
20
specs/004-validation-messages/tasks.md
Normal file
20
specs/004-validation-messages/tasks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Tasks
|
||||
|
||||
## Phase 1: English Lesson Files
|
||||
- [X] Task 1.1: Rewrite 11 validation messages in `lessons/01-box-model.json`
|
||||
- [X] Task 1.2: Rewrite 4 validation messages in `lessons/03-colors.json`
|
||||
|
||||
## Phase 2: Localized Box-Model Files
|
||||
- [X] Task 2.1: Update validation messages in `lessons/ar/01-box-model.json` (Arabic) [P]
|
||||
- [X] Task 2.2: Update validation messages in `lessons/de/01-box-model.json` (German) [P]
|
||||
- [X] Task 2.3: Update validation messages in `lessons/es/01-box-model.json` (Spanish) [P]
|
||||
- [X] Task 2.4: Update validation messages in `lessons/pl/01-box-model.json` (Polish) [P]
|
||||
- [X] Task 2.5: Update validation messages in `lessons/uk/01-box-model.json` (Ukrainian) [P]
|
||||
|
||||
## Phase 3: Validation
|
||||
- [X] Task 3.1: Run `npm run format.lessons` to normalize JSON formatting
|
||||
- [X] Task 3.2: Run `npm run test` to verify no regressions
|
||||
- [X] Task 3.3: Spot-check that no message reveals the exact answer
|
||||
|
||||
## Phase 4: Commit
|
||||
- [X] Task 4.1: Commit all changes with conventional commit message
|
||||
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