feat: restructure lesson files and update success indicators
This commit is contained in:
99
lessons/01-box-model.json
Normal file
99
lessons/01-box-model.json
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"id": "box-model",
|
||||||
|
"title": "CSS Box Model Essentials",
|
||||||
|
"description": "Understand how CSS calculates sizes and spacing around elements using margin, border, padding, and content areas.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "box-model-1",
|
||||||
|
"title": "Content, Padding, Border, Margin",
|
||||||
|
"description": "Learn the four layers of the CSS box model and how each affects element dimensions.",
|
||||||
|
"task": "Create a <code>div</code> with class 'box' and add 1.25rem padding, a 0.125rem solid #333 border, and 1rem margin.",
|
||||||
|
"previewHTML": "<div class=\"box\">Box Model Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background-color: #f0f0f0; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Style the box element */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "padding", "message": "Use the 'padding' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "padding", "expected": "1.25rem" }, "message": "Padding should be 1.25rem" },
|
||||||
|
{ "type": "contains", "value": "border", "message": "Use the 'border' property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "border:\\s*0.125rem\\s+solid\\s+#333",
|
||||||
|
"message": "Border should be '0.125rem solid #333'",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{ "type": "contains", "value": "margin", "message": "Use the 'margin' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "margin", "expected": "1rem" }, "message": "Margin should be 1rem" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-2",
|
||||||
|
"title": "Box-Sizing Property",
|
||||||
|
"description": "Discover how <code>box-sizing</code> changes the box model calculation for width and height.",
|
||||||
|
"task": "Set <code>box-sizing: border-box;</code> on the .box element and note how its width includes padding and border.",
|
||||||
|
"previewHTML": "<div class=\"box\">Border-box Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; } .box { width: 200px; padding: 1rem; border: 0.125rem solid #333; background: #eef; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply box-sizing */\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "box-sizing", "message": "Include the 'box-sizing' property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||||
|
"message": "Set box-sizing to 'border-box'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-3",
|
||||||
|
"title": "Margin Collapse",
|
||||||
|
"description": "Understand how vertical margins can collapse between adjacent elements.",
|
||||||
|
"task": "Create two stacked <code>div</code>s with class 'box' and 1rem top margin on each. Observe margin collapse.",
|
||||||
|
"previewHTML": "<div class=\"box\">First</div><div class=\"box\">Second</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; } .box { padding: 1rem; background: #ddd; margin-top: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "margin-top", "message": "Use 'margin-top' on .box", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "margin-top", "expected": "1rem" }, "message": "Top margin should be 1rem" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "box-model-4",
|
||||||
|
"title": "Using Shorthand Properties",
|
||||||
|
"description": "Learn to write concise padding and margin using shorthand notation.",
|
||||||
|
"task": "Refactor separate margin and padding properties into a single shorthand declaration on .box.",
|
||||||
|
"previewHTML": "<div class=\"box\">Shorthand Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; } .box { background: #fef; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Refactor to shorthand */\n.box {",
|
||||||
|
"initialCode": "margin: 1rem 0 2rem 0; padding: 0.5rem 1rem 0.5rem 1rem;",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "margin:\\s*1rem\\s+0\\s+2rem\\s+0",
|
||||||
|
"message": "Use shorthand for margin: 1rem 0 2rem 0",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "padding:\\s*0.5rem\\s+1rem\\s+0.5rem\\s+1rem",
|
||||||
|
"message": "Use shorthand for padding: 0.5rem 1rem 0.5rem 1rem",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
102
lessons/02-selectors.json
Normal file
102
lessons/02-selectors.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"id": "selectors",
|
||||||
|
"title": "CSS Selectors Deep Dive",
|
||||||
|
"description": "Master the art of targeting HTML elements using various CSS selectors, from basics to specificity rules.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "selectors-1",
|
||||||
|
"title": "Element Selectors",
|
||||||
|
"description": "Learn to target HTML elements by their tag name and apply styling.",
|
||||||
|
"task": "Use the element selector to make all <code><p></code> tags have a dark gray color.",
|
||||||
|
"previewHTML": "<p>This paragraph should be dark gray.</p><p>This one too!</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Style all paragraphs */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "p {", "message": "Use the element selector 'p'", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "color", "message": "Include the 'color' property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "color", "expected": "darkgray" },
|
||||||
|
"message": "Set color to 'darkgray'",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "selectors-2",
|
||||||
|
"title": "Class Selectors",
|
||||||
|
"description": "Use class selectors to style elements sharing the same class name.",
|
||||||
|
"task": "Apply a blueviolet text color to any element with the class 'title'.",
|
||||||
|
"previewHTML": "<h2 class=\"title\">Hello World</h2><h2>Another Heading</h2>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Style elements with class 'title' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": ".title", "message": "Use the '.title' class selector", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "color", "expected": "blueviolet" },
|
||||||
|
"message": "Set color to 'blueviolet'",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "selectors-3",
|
||||||
|
"title": "ID Selectors",
|
||||||
|
"description": "Target a unique element by its ID to apply specific styling.",
|
||||||
|
"task": "Make the element with <code>id=\"description\"</code> have orangered text.",
|
||||||
|
"previewHTML": "<div id=\"description\">This is the description text.</div><div>This is another div.</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Style the element with ID 'description' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "#description",
|
||||||
|
"message": "Use the '#description' ID selector",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "color", "expected": "orangered" },
|
||||||
|
"message": "Set color to 'orangered'",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "selectors-4",
|
||||||
|
"title": "Combined Selectors & Specificity",
|
||||||
|
"description": "Discover how combining selectors controls which rules take precedence.",
|
||||||
|
"task": "Use a class and element selector together (e.g., <code>div.note</code>) to give a yellow background to the note box.",
|
||||||
|
"previewHTML": "<div class=\"note\">Important note!</div><div>Regular div</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Style div with class 'note' */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "div.note", "message": "Use the 'div.note' combined selector", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "background-color", "expected": "yellow" },
|
||||||
|
"message": "Set background-color to 'yellow'",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
120
lessons/03-colors.json
Normal file
120
lessons/03-colors.json
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"id": "colors-backgrounds",
|
||||||
|
"title": "Colors & Backgrounds",
|
||||||
|
"description": "Learn how to apply and manipulate colors, backgrounds, and graphical fills using CSS properties.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "colors-1",
|
||||||
|
"title": "Setting Background Colors",
|
||||||
|
"description": "Use the <code>background-color</code> property to fill elements with solid colors.",
|
||||||
|
"task": "Apply a light cyan background (#e0f7fa) to the element with class 'colorbox'.",
|
||||||
|
"previewHTML": "<div class=\"colorbox\">Background Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .colorbox { padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set a background color */\n.colorbox {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": ".colorbox", "message": "Select '.colorbox'", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-color",
|
||||||
|
"message": "Use 'background-color' property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "background-color", "expected": "#e0f7fa" },
|
||||||
|
"message": "Set background-color to '#e0f7fa'",
|
||||||
|
"options": { "exact": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "colors-2",
|
||||||
|
"title": "Text Color and Contrast",
|
||||||
|
"description": "Apply the <code>color</code> property to control text readability against backgrounds.",
|
||||||
|
"task": "Set the text color of '.colorbox' to deep blue (#01579b). Ensure good contrast.",
|
||||||
|
"previewHTML": "<div class=\"colorbox\">Color & Contrast</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .colorbox { padding: 1rem; background: #e0f7fa; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set text color */\n.colorbox {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": ".colorbox", "message": "Select '.colorbox'", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "color", "message": "Use the 'color' property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "color", "expected": "#01579b" },
|
||||||
|
"message": "Set color to '#01579b'",
|
||||||
|
"options": { "exact": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "colors-3",
|
||||||
|
"title": "CSS Gradients",
|
||||||
|
"description": "Learn to create smooth transitions between colors using linear and radial gradients.",
|
||||||
|
"task": "Apply a linear gradient background from #ff9a9e to #fad0c4 on an element with class 'gradient-box'.",
|
||||||
|
"previewHTML": "<div class=\"gradient-box\">Gradient Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .gradient-box { padding: 1rem; color: white; text-align: center; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set a linear gradient background */\n.gradient-box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": ".gradient-box", "message": "Select '.gradient-box'", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-image",
|
||||||
|
"message": "Use 'background-image' property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "linear-gradient\\(.*#ff9a9e.*,.*#fad0c4.*\\)",
|
||||||
|
"message": "Use linear-gradient from #ff9a9e to #fad0c4",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "colors-4",
|
||||||
|
"title": "Background Images & Repeat",
|
||||||
|
"description": "Add images as backgrounds and control repetition and positioning.",
|
||||||
|
"task": "Set a background image on '.bg-img' using a placeholder URL, center it, and prevent tiling.",
|
||||||
|
"previewHTML": "<div class=\"bg-img\">Image Background</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .bg-img { height: 150px; display: flex; align-items: center; justify-content: center; color: white; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set background image */\n.bg-img {",
|
||||||
|
"initialCode": "background-image: url('https://via.placeholder.com/300'); background-position: center; background-repeat: no-repeat;",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-image",
|
||||||
|
"message": "Use 'background-image' property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-position: center",
|
||||||
|
"message": "Center the background image",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "background-repeat: no-repeat",
|
||||||
|
"message": "Prevent image tiling",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
90
lessons/04-typography.json
Normal file
90
lessons/04-typography.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"id": "typography-fonts",
|
||||||
|
"title": "Typography & Fonts",
|
||||||
|
"description": "Learn how to control text appearance through font selection, sizing, spacing, and decorative effects.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "typography-1",
|
||||||
|
"title": "Font Family & Fallbacks",
|
||||||
|
"description": "Specify custom fonts and reliable fallback stacks for consistent typography across devices.",
|
||||||
|
"task": "Set the <code>font-family</code> of the '.text' element to 'Georgia, serif' with serif fallback.",
|
||||||
|
"previewHTML": "<p class=\"text\">This text shows the chosen font family.</p>",
|
||||||
|
"previewBaseCSS": "body { padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set font family */\n.text {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "font-family", "message": "Use the 'font-family' property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "Georgia, serif",
|
||||||
|
"message": "Include 'Georgia, serif' in the font stack",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "typography-2",
|
||||||
|
"title": "Font Size & Line Height",
|
||||||
|
"description": "Control text scale and readability by adjusting size and line heights.",
|
||||||
|
"task": "Set the heading '.heading' to 1.5rem font-size and 1.5 line-height.",
|
||||||
|
"previewHTML": "<h2 class=\"heading\">Readable Heading</h2>",
|
||||||
|
"previewBaseCSS": "body { padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set size and line height */\n.heading {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "font-size", "message": "Use 'font-size' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "font-size", "expected": "1.5rem" }, "message": "Set font-size to '1.5rem'" },
|
||||||
|
{ "type": "contains", "value": "line-height", "message": "Use 'line-height' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "line-height", "expected": "1.5" }, "message": "Set line-height to '1.5'" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "typography-3",
|
||||||
|
"title": "Font Weight & Style",
|
||||||
|
"description": "Apply weight and style variations like bold, light, italic to emphasize text.",
|
||||||
|
"task": "Make the paragraph '.emphasis' italic and bold using <code>font-style</code> and <code>font-weight</code>.",
|
||||||
|
"previewHTML": "<p class=\"emphasis\">This text should stand out.</p>",
|
||||||
|
"previewBaseCSS": "body { padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Emphasize text */\n.emphasis {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "font-style", "message": "Use 'font-style' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "font-style", "expected": "italic" }, "message": "Set font-style to 'italic'" },
|
||||||
|
{ "type": "contains", "value": "font-weight", "message": "Use 'font-weight' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "font-weight", "expected": "bold" }, "message": "Set font-weight to 'bold'" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "typography-4",
|
||||||
|
"title": "Text Decoration & Shadow",
|
||||||
|
"description": "Add decorative underlines, overlines, line-throughs and subtle shadows to text.",
|
||||||
|
"task": "Apply an underline with <code>text-decoration</code> and a light shadow using <code>text-shadow</code> on '.fancy'.",
|
||||||
|
"previewHTML": "<p class=\"fancy\">Fancy text effect!</p>",
|
||||||
|
"previewBaseCSS": "body { padding: 1rem; } .fancy { font-size: 1.25rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Decorate text */\n.fancy {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "text-decoration",
|
||||||
|
"message": "Use 'text-decoration' property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{ "type": "contains", "value": "text-shadow", "message": "Use 'text-shadow' property", "options": { "caseSensitive": false } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
lessons/05-units-variables.json
Normal file
97
lessons/05-units-variables.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"id": "units-variables",
|
||||||
|
"title": "CSS Units & Variables",
|
||||||
|
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "units-1",
|
||||||
|
"title": "Absolute vs. Relative Units",
|
||||||
|
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.",
|
||||||
|
"task": "Set the width of '.unit-box' to 80% and max-width to 37.5rem.",
|
||||||
|
"previewHTML": "<div class=\"unit-box\">Resize me!</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .unit-box { background: #f5f5f5; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set flexible sizing */\n.unit-box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "width", "message": "Use 'width' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to '80%'" },
|
||||||
|
{ "type": "contains", "value": "max-width", "message": "Use 'max-width' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "max-width", "expected": "37.5rem" }, "message": "Set max-width to '37.5rem'" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-2",
|
||||||
|
"title": "CSS Custom Properties",
|
||||||
|
"description": "Define and reuse variables (--custom properties) to centralize your theme values.",
|
||||||
|
"task": "Create a <code>--main-color</code> variable in :root with #6200ee and apply it as the border color on '.var-box'.",
|
||||||
|
"previewHTML": "<div class=\"var-box\">Variable Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .var-box { padding: 1rem; border: 0.125rem solid #ddd; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define and use a CSS variable */\n:root {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.var-box { }",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "--main-color", "message": "Define '--main-color' in :root", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "var(--main-color)", "message": "Use var(--main-color)", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "border", "expected": "var(--main-color)" },
|
||||||
|
"message": "Apply variable to border color",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-3",
|
||||||
|
"title": "Unit Calculations (calc)",
|
||||||
|
"description": "Use the <code>calc()</code> function to combine different units in one expression.",
|
||||||
|
"task": "Set the width of '.calc-box' to calc(100% - 2rem) and min-height to calc(10vh + 1rem).",
|
||||||
|
"previewHTML": "<div class=\"calc-box\">Calc Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .calc-box { background: #e8f5e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use calc for dynamic sizing */\n.calc-box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "calc", "message": "Use 'calc()' function", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "width:\\s*calc\\(100% - 2rem\\)",
|
||||||
|
"message": "Width should be calc(100% - 2rem)",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
|
||||||
|
"message": "Min-height should be calc(10vh + 1rem)",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "units-4",
|
||||||
|
"title": "Viewport & Responsive Units",
|
||||||
|
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.",
|
||||||
|
"task": "Give '.viewport-box' a width of 50vw and height of 20vh.",
|
||||||
|
"previewHTML": "<div class=\"viewport-box\">Viewport Box</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .viewport-box { background: #ffe0b2; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Use viewport units */\n.viewport-box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "vw", "message": "Use 'vw' unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "vh", "message": "Use 'vh' unit", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to '50vw'" },
|
||||||
|
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to '20vh'" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
130
lessons/06-transitions-animations.json
Normal file
130
lessons/06-transitions-animations.json
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
{
|
||||||
|
"id": "transitions-animations",
|
||||||
|
"title": "CSS Transitions & Animations",
|
||||||
|
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.",
|
||||||
|
"difficulty": "beginner",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "transitions-1",
|
||||||
|
"title": "Simple Transitions",
|
||||||
|
"description": "Learn how to apply <code>transition</code> to properties for smooth changes on state changes.",
|
||||||
|
"task": "Add a hover transition on a button so its background-color fades over 0.3s.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Hover Me</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: #6200ee; color: white; padding: 0.5rem 1rem; border: none; } .btn:hover { background: #3700b3; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add transition */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "transition", "message": "Use the 'transition' property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||||
|
"message": "Transition background-color over 0.3s",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-2",
|
||||||
|
"title": "Transition Timing Functions",
|
||||||
|
"description": "Explore easing functions like ease, linear, ease-in, ease-out to control animation pacing.",
|
||||||
|
"task": "Modify the button to use 'ease-in-out' timing for its transition.",
|
||||||
|
"previewHTML": "<button class=\"btn\">Timing</button>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: #6200ee; color: white; padding: 0.5rem 1rem; border: none; transition: background-color 0.3s; } .btn:hover { background: #03dac6; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Set timing function */\n.btn {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "transition-timing-function",
|
||||||
|
"message": "Use 'transition-timing-function'",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||||
|
"message": "Set timing to 'ease-in-out'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-3",
|
||||||
|
"title": "Keyframe Animations Basics",
|
||||||
|
"description": "Create named animations using <code>@keyframes</code> and apply them via the <code>animation</code> shorthand.",
|
||||||
|
"task": "Define a keyframe named 'bounce' that moves an element up 20px at 50% and apply it to '.ball' over 1s infinite.",
|
||||||
|
"previewHTML": "<div class=\"ball\"></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: #ff0266; border-radius: 50%; margin: 2rem auto; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define keyframes and apply animation */\n@keyframes bounce {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.ball { }",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "@keyframes bounce", "message": "Define '@keyframes bounce'", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "50%.*transform: translateY\\(-20px\\)",
|
||||||
|
"message": "At 50%, move up 20px",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{ "type": "contains", "value": "animation", "message": "Use 'animation' property on .ball", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "animation:.*bounce.*1s.*infinite",
|
||||||
|
"message": "Apply 'bounce 1s infinite'",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transitions-4",
|
||||||
|
"title": "Animation Properties Deep Dive",
|
||||||
|
"description": "Fine-tune animations with delay, iteration-count, direction, and fill-mode.",
|
||||||
|
"task": "Animate '.box' using a 'fade' keyframe over 2s, delay 1s, run twice, and remain visible after.",
|
||||||
|
"previewHTML": "<div class=\"box\">Fade Demo</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: #4caf50; margin: 2rem auto; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define fade and set properties */\n@keyframes fade { from { opacity: 0; } to { opacity: 1; } }\n.box {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "animation-delay",
|
||||||
|
"message": "Use 'animation-delay' property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "animation-iteration-count",
|
||||||
|
"message": "Use 'animation-iteration-count' property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "animation-fill-mode",
|
||||||
|
"message": "Use 'animation-fill-mode' property",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{ "type": "property_value", "value": { "property": "animation-duration", "expected": "2s" }, "message": "Duration should be 2s" },
|
||||||
|
{ "type": "property_value", "value": { "property": "animation-delay", "expected": "1s" }, "message": "Delay should be 1s" },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||||
|
"message": "Iteration count should be 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||||
|
"message": "Fill mode should be forwards"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
99
lessons/07-layouts.json
Normal file
99
lessons/07-layouts.json
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"id": "layouts",
|
||||||
|
"title": "Advanced Layouts: Flexbox & Grid",
|
||||||
|
"description": "Master modern CSS layout techniques with Flexbox and Grid for responsive, powerful designs.",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "layouts-1",
|
||||||
|
"title": "Flexbox Fundamentals",
|
||||||
|
"description": "Learn the core properties of Flexbox to align, distribute space, and order items in a container.",
|
||||||
|
"task": "Set .flex-container to display: flex, and center its children both horizontally and vertically.",
|
||||||
|
"previewHTML": "<div class=\"flex-container\"><div>1</div><div>2</div><div>3</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .flex-container > div { background: #eceff1; margin: 0.5rem; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Enable and center Flexbox */\n.flex-container {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "display", "message": "Use 'display: flex'", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "justify-content", "message": "Use 'justify-content: center'", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "align-items", "message": "Use 'align-items: center'", "options": { "caseSensitive": false } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "layouts-2",
|
||||||
|
"title": "Flexbox Advanced Features",
|
||||||
|
"description": "Control wrapping, ordering, and flexible growth/shrink of items in a flex container.",
|
||||||
|
"task": "Allow items to wrap and set .flex-item to flex: 1 1 100px.",
|
||||||
|
"previewHTML": "<div class=\"flex-container\"><div class=\"flex-item\">A</div><div class=\"flex-item\">B</div><div class=\"flex-item\">C</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .flex-item { background: #c5cae9; margin: 0.5rem; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Enable wrap and flexible items */\n.flex-container {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}\n.flex-item { }",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "flex-wrap: wrap", "message": "Use 'flex-wrap: wrap'", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": ".flex-item.*flex:\\s*1\\s+1\\s+100px",
|
||||||
|
"message": "Set flex: 1 1 100px on items",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "layouts-3",
|
||||||
|
"title": "Grid Layout Basics",
|
||||||
|
"description": "Define grid containers, set rows and columns, and place items in a structured grid.",
|
||||||
|
"task": "Set .grid-container to display: grid with three equal columns and a 1rem gap.",
|
||||||
|
"previewHTML": "<div class=\"grid-container\"><div>1</div><div>2</div><div>3</div><div>4</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .grid-container > div { background: #ffe082; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Define Grid */\n.grid-container {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "display: grid", "message": "Use 'display: grid'", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "grid-template-columns",
|
||||||
|
"message": "Define 'grid-template-columns'",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "grid-template-columns:\\s*repeat\\(3,\\s*1fr\\)\\s*",
|
||||||
|
"message": "Create three equal columns",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{ "type": "contains", "value": "gap", "message": "Use 'gap' property", "options": { "caseSensitive": false } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "layouts-4",
|
||||||
|
"title": "Grid Item Placement",
|
||||||
|
"description": "Control the span and position of grid items with grid-column and grid-row.",
|
||||||
|
"task": "Span the first grid item across 2 columns using grid-column: 1 / span 2.",
|
||||||
|
"previewHTML": "<div class=\"grid-container\"><div class=\"item1\">Featured</div><div>2</div><div>3</div><div>4</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .grid-container > div { background: #c8e6c9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Span item */\n.item1 {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "grid-column", "message": "Use 'grid-column' property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "grid-column", "expected": "1 / span 2" },
|
||||||
|
"message": "Span across 2 columns",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
116
lessons/08-responsive.json
Normal file
116
lessons/08-responsive.json
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"id": "responsive-design",
|
||||||
|
"title": "Responsive Design & Media Queries",
|
||||||
|
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.",
|
||||||
|
"difficulty": "intermediate",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "responsive-1",
|
||||||
|
"title": "Introduction to Media Queries",
|
||||||
|
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.",
|
||||||
|
"task": "Write a media query that applies when the viewport is at most 600px wide and changes the background of '.responsive-box' to lightcoral.",
|
||||||
|
"previewHTML": "<div class=\"responsive-box\">Resize the window</div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .responsive-box { padding: 1rem; background: lightblue; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add your media query below */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||||
|
"message": "Use a media query for max-width: 600px",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": ".responsive-box",
|
||||||
|
"message": "Target '.responsive-box' inside the media query",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{ "type": "contains", "value": "background", "message": "Change the 'background' property", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "background", "expected": "lightcoral" },
|
||||||
|
"message": "Set background to 'lightcoral'",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-2",
|
||||||
|
"title": "Fluid Typography",
|
||||||
|
"description": "Use relative units like vw to make font sizes scale with the viewport width.",
|
||||||
|
"task": "Set the font-size of '.fluid-text' to 5vw so it scales as the viewport changes.",
|
||||||
|
"previewHTML": "<p class=\"fluid-text\">Fluid Typography</p>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Apply fluid font sizing */\n.fluid-text {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{ "type": "contains", "value": "font-size", "message": "Use 'font-size' property", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "contains", "value": "vw", "message": "Use 'vw' unit for fluid sizing", "options": { "caseSensitive": false } },
|
||||||
|
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set font-size to '5vw'" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-3",
|
||||||
|
"title": "Flexible Grids",
|
||||||
|
"description": "Combine CSS Grid with auto-fit or auto-fill for responsive column layouts.",
|
||||||
|
"task": "Define '.grid-responsive' with grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); and a gap of 1rem.",
|
||||||
|
"previewHTML": "<div class=\"grid-responsive\"><div>1</div><div>2</div><div>3</div><div>4</div></div>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .grid-responsive > div { background: #d1c4e9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Create a responsive grid */\n.grid-responsive {",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "}",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "contains",
|
||||||
|
"value": "grid-template-columns",
|
||||||
|
"message": "Define 'grid-template-columns'",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||||
|
"message": "Use repeat(auto-fit, minmax(200px, 1fr))",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{ "type": "contains", "value": "gap", "message": "Use 'gap' property", "options": { "caseSensitive": false } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "responsive-4",
|
||||||
|
"title": "Mobile-First Media Queries",
|
||||||
|
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.",
|
||||||
|
"task": "Write a media query for min-width 768px that sets '.sidebar' width to 250px.",
|
||||||
|
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
|
||||||
|
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
|
||||||
|
"sandboxCSS": "",
|
||||||
|
"codePrefix": "/* Add mobile-first enhancement */\n",
|
||||||
|
"initialCode": "",
|
||||||
|
"codeSuffix": "",
|
||||||
|
"previewContainer": "preview-area",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||||
|
"message": "Use a media query for min-width: 768px",
|
||||||
|
"options": { "caseSensitive": false }
|
||||||
|
},
|
||||||
|
{ "type": "contains", "value": ".sidebar", "message": "Target '.sidebar' inside media query", "options": { "caseSensitive": false } },
|
||||||
|
{
|
||||||
|
"type": "property_value",
|
||||||
|
"value": { "property": "width", "expected": "250px" },
|
||||||
|
"message": "Set width to '250px'",
|
||||||
|
"options": { "exact": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
public/bar_1680535.png
Normal file
BIN
public/bar_1680535.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -154,7 +154,9 @@ function selectModule(moduleId) {
|
|||||||
function resetSuccessIndicators() {
|
function resetSuccessIndicators() {
|
||||||
elements.codeEditor.classList.remove("success-highlight");
|
elements.codeEditor.classList.remove("success-highlight");
|
||||||
elements.lessonTitle.classList.remove("success-text");
|
elements.lessonTitle.classList.remove("success-text");
|
||||||
elements.runBtn.classList.remove("success");
|
elements.runBtn.classList.remove("hidden");
|
||||||
|
elements.nextBtn.classList.remove("success");
|
||||||
|
elements.taskInstruction.classList.remove("success-instruction");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the current lesson
|
// Load the current lesson
|
||||||
@@ -264,7 +266,9 @@ function runCode() {
|
|||||||
// Add success visual indicators
|
// Add success visual indicators
|
||||||
elements.codeEditor.classList.add("success-highlight");
|
elements.codeEditor.classList.add("success-highlight");
|
||||||
elements.lessonTitle.classList.add("success-text");
|
elements.lessonTitle.classList.add("success-text");
|
||||||
elements.runBtn.classList.add("success");
|
elements.runBtn.classList.add("hidden");
|
||||||
|
elements.nextBtn.classList.add("success");
|
||||||
|
elements.taskInstruction.classList.add("success-instruction");
|
||||||
|
|
||||||
// Apply the code to see the result
|
// Apply the code to see the result
|
||||||
lessonEngine.applyUserCode(userCode);
|
lessonEngine.applyUserCode(userCode);
|
||||||
|
|||||||
@@ -3,13 +3,28 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Import lesson configs
|
// Import lesson configs
|
||||||
import flexboxConfig from "../../lessons/flexbox.json";
|
import basicsConfig from "../../lessons/00-basics.json";
|
||||||
import gridConfig from "../../lessons/grid.json";
|
import boxModelConfig from "../../lessons/01-box-model.json";
|
||||||
import basicsConfig from "../../lessons/basics.json";
|
import selectorsConfig from "../../lessons/02-selectors.json";
|
||||||
import tailwindConfig from "../../lessons/tailwindcss.json";
|
import colorsConfig from "../../lessons/03-colors.json";
|
||||||
|
import typographyConfig from "../../lessons/04-typography.json";
|
||||||
|
import unitVariablesConfig from "../../lessons/05-units-variables.json";
|
||||||
|
import transitionsAnimationsConfig from "../../lessons/06-transitions-animations.json";
|
||||||
|
import layoutConfig from "../../lessons/07-layouts.json";
|
||||||
|
import responsiveConfig from "../../lessons/08-responsive.json";
|
||||||
|
|
||||||
// Module store
|
// Module store
|
||||||
const moduleStore = [basicsConfig, flexboxConfig, gridConfig, tailwindConfig];
|
const moduleStore = [
|
||||||
|
basicsConfig,
|
||||||
|
boxModelConfig,
|
||||||
|
selectorsConfig,
|
||||||
|
colorsConfig,
|
||||||
|
typographyConfig,
|
||||||
|
unitVariablesConfig,
|
||||||
|
transitionsAnimationsConfig,
|
||||||
|
layoutConfig,
|
||||||
|
responsiveConfig
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all available modules
|
* Load all available modules
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ let feedbackElement = null;
|
|||||||
*/
|
*/
|
||||||
export function renderModuleList(container, modules, onSelectModule) {
|
export function renderModuleList(container, modules, onSelectModule) {
|
||||||
// Clear the container
|
// Clear the container
|
||||||
container.innerHTML = "<h3>Modules</h3>";
|
container.innerHTML = "<h3>CSS Lessons</h3>";
|
||||||
|
|
||||||
// Create list items for each module
|
// Create list items for each module
|
||||||
modules.forEach((module) => {
|
modules.forEach((module) => {
|
||||||
@@ -86,16 +86,18 @@ export function showFeedback(isSuccess, message) {
|
|||||||
feedbackElement.textContent = message;
|
feedbackElement.textContent = message;
|
||||||
|
|
||||||
// Find where to insert the feedback
|
// Find where to insert the feedback
|
||||||
const insertAfter = document.querySelector(".code-editor");
|
const insertAfter = document.querySelector(".editor-content");
|
||||||
if (insertAfter && insertAfter.parentNode) {
|
if (insertAfter && insertAfter.parentNode) {
|
||||||
insertAfter.parentNode.insertBefore(feedbackElement, insertAfter.nextSibling);
|
insertAfter.parentNode.insertBefore(feedbackElement, insertAfter.nextSibling);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-remove feedback after some time if successful
|
if (!isSuccess) {
|
||||||
if (isSuccess) {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearFeedback();
|
if (feedbackElement && feedbackElement.parentNode) {
|
||||||
}, 5000);
|
feedbackElement.parentNode.removeChild(feedbackElement);
|
||||||
|
}
|
||||||
|
feedbackElement = null;
|
||||||
|
}, 3_000); // Remove feedback after 3 seconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<h1>🏵️ CODE CRISPIES</h1>
|
<img src="./bar_1680535.png" width="32" alt="CODE CRISPIES Logo" />
|
||||||
|
<h1>CODE<br /><span>CRISPIES</span></h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
128
src/main.css
128
src/main.css
@@ -1,14 +1,18 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary-color: #4a6bfd;
|
--primary-color: #dd3b59;
|
||||||
--primary-light: #7a93fe;
|
--primary-light: #ff5f7f;
|
||||||
--primary-dark: #244ae8;
|
--primary-dark: #af2740;
|
||||||
--secondary-color: #ff7e5f;
|
--secondary-color: #ff7e5f;
|
||||||
--text-color: #2c3e50;
|
--text-color: #13181c;
|
||||||
--light-text: #777;
|
--light-text: #777;
|
||||||
--bg-color: #f9f9f9;
|
--bg-color: #f9f9f9;
|
||||||
--panel-bg: #ffffff;
|
--panel-bg: #ffffff;
|
||||||
--border-color: #e0e0e0;
|
--border-color: #e0e0e0;
|
||||||
--success-color: #2ecc71;
|
--info-color: #7a93fe;
|
||||||
|
--info-color-dark: #4a6bfd;
|
||||||
|
--success-color: #24b664;
|
||||||
|
--success-color-dark: #1e8f4d;
|
||||||
|
--success-color-light: #a3e6b4;
|
||||||
--error-color: #e74c3c;
|
--error-color: #e74c3c;
|
||||||
--font-main: "Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
--font-main: "Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
--shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
--shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
@@ -46,10 +50,21 @@ body {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.logo h1 {
|
.logo h1 {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 1.7rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 700;
|
line-height: 0.8;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 span {
|
||||||
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav ul {
|
.main-nav ul {
|
||||||
@@ -87,6 +102,19 @@ body {
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-list h3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
/*color: var(--light-text);*/
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
/*background-color: rgba(0, 0, 0, 0.05);*/
|
||||||
|
}
|
||||||
|
|
||||||
.module-list-item {
|
.module-list-item {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
@@ -96,13 +124,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.module-list-item:hover {
|
.module-list-item:hover {
|
||||||
background-color: rgba(74, 107, 253, 0.05);
|
background-color: rgba(253, 74, 149, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-list-item.active {
|
.module-list-item.active {
|
||||||
background-color: rgba(74, 107, 253, 0.1);
|
background-color: rgba(253, 74, 149, 0.1);
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lesson Container */
|
/* Lesson Container */
|
||||||
@@ -170,13 +197,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-instruction {
|
.task-instruction {
|
||||||
background-color: rgba(74, 107, 253, 0.05);
|
background-color: rgba(253, 74, 122, 0.05);
|
||||||
border-left: 4px solid var(--primary-color);
|
border-left: 4px solid var(--primary-color);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-editor {
|
.code-editor {
|
||||||
|
position: relative;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -192,11 +220,11 @@ body {
|
|||||||
color: var(--light-text);
|
color: var(--light-text);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
/*.target{color:blue}*/
|
||||||
.editor-content {
|
.editor-content {
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
padding: 1rem;
|
padding: 1rem 1rem 4rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -271,7 +299,10 @@ code {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-main);
|
font-family: var(--font-main);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
transition: all 0.2s;
|
transition:
|
||||||
|
background,
|
||||||
|
color 0.2s;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
@@ -324,6 +355,26 @@ code {
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content ol,
|
||||||
|
.modal-content ul {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h3 {
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h4 {
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -338,30 +389,34 @@ code {
|
|||||||
|
|
||||||
/* Feedback */
|
/* Feedback */
|
||||||
.feedback-success {
|
.feedback-success {
|
||||||
color: var(--success-color);
|
position: absolute;
|
||||||
font-weight: 500;
|
bottom: 0.8rem;
|
||||||
margin-top: 1rem;
|
right: 0.8rem;
|
||||||
|
color: var(--success-color-light);
|
||||||
|
font-weight: 800;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: rgba(46, 204, 113, 0.1);
|
background-color: rgba(46, 204, 113, 0.4);
|
||||||
border-left: 3px solid var(--success-color);
|
border-left: 3px solid var(--success-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feedback-error {
|
/*.feedback-error {*/
|
||||||
color: var(--error-color);
|
/* color: var(--error-color);*/
|
||||||
font-weight: 500;
|
/* font-weight: 500;*/
|
||||||
margin-top: 1rem;
|
/* margin-top: 1rem;*/
|
||||||
padding: 0.5rem;
|
/* padding: 0.5rem;*/
|
||||||
border-radius: 4px;
|
/* border-radius: 4px;*/
|
||||||
background-color: rgba(231, 76, 60, 0.1);
|
/* background-color: rgba(231, 76, 60, 0.1);*/
|
||||||
border-left: 3px solid var(--error-color);
|
/* border-left: 3px solid var(--error-color);*/
|
||||||
}
|
/*}*/
|
||||||
|
|
||||||
/* Add these styles to your main.css file */
|
/* Add these styles to your main.css file */
|
||||||
|
|
||||||
/* Success highlight for lesson container */
|
/* Success highlight for lesson container */
|
||||||
.success-highlight {
|
.success-highlight {
|
||||||
box-shadow: 0 0 0 3px var(--success-color);
|
/*box-shadow: 0 0 0 3px var(--success-color);*/
|
||||||
|
border-left: 4px solid var(--success-color);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +426,13 @@ code {
|
|||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.success-instruction {
|
||||||
|
background-color: rgba(46, 204, 113, 0.1);
|
||||||
|
border-left: 4px solid var(--success-color);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.success,
|
.success,
|
||||||
.success:hover,
|
.success:hover,
|
||||||
.success:focus {
|
.success:focus {
|
||||||
@@ -380,13 +442,15 @@ code {
|
|||||||
|
|
||||||
/* Friendlier error feedback */
|
/* Friendlier error feedback */
|
||||||
.feedback-error {
|
.feedback-error {
|
||||||
color: #996633;
|
position: absolute;
|
||||||
|
bottom: 0.8rem;
|
||||||
|
right: 0.8rem;
|
||||||
|
color: var(--text-color);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: rgba(255, 248, 230, 0.5);
|
background-color: rgba(244, 244, 244, 0.8);
|
||||||
border-left: 3px solid #cc9944;
|
border-left: 3px solid white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Module selector button with progress */
|
/* Module selector button with progress */
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { loadModules, getModuleById, loadModuleFromUrl, addCustomModule } from "
|
|||||||
// Mock the module store for testing
|
// Mock the module store for testing
|
||||||
vi.mock("../../lessons/flexbox.json", () => ({ default: { id: "flexbox", title: "Flexbox", lessons: [] } }));
|
vi.mock("../../lessons/flexbox.json", () => ({ default: { id: "flexbox", title: "Flexbox", lessons: [] } }));
|
||||||
vi.mock("../../lessons/grid.json", () => ({ default: { id: "grid", title: "CSS Grid", lessons: [] } }));
|
vi.mock("../../lessons/grid.json", () => ({ default: { id: "grid", title: "CSS Grid", lessons: [] } }));
|
||||||
vi.mock("../../lessons/basics.json", () => ({ default: { id: "basics", title: "CSS Basics", lessons: [] } }));
|
vi.mock("../../lessons/00-basics.json", () => ({ default: { id: "basics", title: "CSS Basics", lessons: [] } }));
|
||||||
vi.mock("../../lessons/tailwindcss.json", () => ({ default: { id: "tailwind", title: "Tailwind CSS", lessons: [] } }));
|
vi.mock("../../lessons/tailwindcss.json", () => ({ default: { id: "tailwind", title: "Tailwind CSS", lessons: [] } }));
|
||||||
|
|
||||||
describe("Lessons Config Module", () => {
|
describe("Lessons Config Module", () => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig((env) => ({
|
||||||
base: "/code-crispies/",
|
base: env.command.build ? "/code-crispies/" : "/",
|
||||||
root: "./src",
|
root: "./src",
|
||||||
publicDir: "./public",
|
publicDir: "../public",
|
||||||
build: {
|
build: {
|
||||||
outDir: "../dist",
|
outDir: "../dist",
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
@@ -13,4 +13,4 @@ export default defineConfig({
|
|||||||
port: 1312,
|
port: 1312,
|
||||||
open: false
|
open: false
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user