Compare commits
8 Commits
feat/impl-
...
004-pedago
| Author | SHA1 | Date | |
|---|---|---|---|
| c560676544 | |||
| 782e87705c | |||
| 433379155b | |||
| 756841f8c2 | |||
| c97fce1f29 | |||
| 8b6a88ad59 | |||
| 4476d26140 | |||
| f28531fb4c |
@@ -1,15 +1,6 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(npm run format.lessons:*)",
|
||||
"Bash(xargs:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(prettier --write:*)"
|
||||
],
|
||||
"deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"]
|
||||
},
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -8,3 +8,14 @@ coverage
|
||||
|
||||
# Claude Code local settings (user-specific)
|
||||
.claude/settings.local.json
|
||||
.claude_settings.json
|
||||
|
||||
# Auto-Claude
|
||||
.auto-claude
|
||||
.worktrees
|
||||
|
||||
# Wave ephemeral data
|
||||
.wave/workspaces
|
||||
.wave/traces
|
||||
.wave/artifacts
|
||||
.wave/output
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Add <kbd>color: coral;</kbd>"
|
||||
"message": "Which property controls text color?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Add <kbd>background: lavender;</kbd>"
|
||||
"message": "Check the <kbd>background</kbd> property"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Add <kbd>padding: 1rem;</kbd>"
|
||||
"message": "The card needs space inside its edges"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Set <kbd>color: steelblue</kbd>"
|
||||
"message": "Which property changes text color?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Set <kbd>color: coral</kbd>"
|
||||
"message": "What value gives a warm, reddish-orange color?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "Set <kbd>background: tomato</kbd>"
|
||||
"message": "The badge needs a bright red background"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Set <kbd>background: steelblue</kbd>"
|
||||
"message": "Which property sets the button's fill color?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Set <kbd>text-decoration: none</kbd>"
|
||||
"message": "Which property controls the underline on links?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Set <kbd>color: steelblue</kbd>"
|
||||
"message": "Check the <kbd>color</kbd> property"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Set <kbd>color: white</kbd>"
|
||||
"message": "The links need to stand out against the blue background"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Set <kbd>font-size: 0.9rem</kbd>"
|
||||
"message": "Check the <kbd>font-size</kbd> property — the text should be slightly smaller"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"property": "padding",
|
||||
"expected": "20px"
|
||||
},
|
||||
"message": "Set the padding value to <kbd>20px</kbd>",
|
||||
"message": "How much breathing room does the content need? Re-read the task for the exact size",
|
||||
"options": {
|
||||
"exact": true
|
||||
}
|
||||
@@ -181,7 +181,7 @@
|
||||
"property": "margin-bottom",
|
||||
"expected": "30px"
|
||||
},
|
||||
"message": "Set the margin-bottom value to <kbd>30px</kbd>",
|
||||
"message": "How much space should separate the title from the content below? Check the task for the amount",
|
||||
"options": {
|
||||
"exact": true
|
||||
}
|
||||
@@ -212,7 +212,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border:\\s*2px\\s+solid\\s+blue",
|
||||
"message": "Set the border to <kbd>2px solid blue</kbd>",
|
||||
"message": "The <kbd>border</kbd> shorthand takes three parts: width, style, and color",
|
||||
"options": {
|
||||
"caseSensitive": false
|
||||
}
|
||||
@@ -246,7 +246,7 @@
|
||||
"property": "justify-content",
|
||||
"expected": "center"
|
||||
},
|
||||
"message": "Set <kbd>justify-content</kbd> to <kbd>center</kbd>",
|
||||
"message": "How do you center items along the main axis?",
|
||||
"options": {
|
||||
"exact": true
|
||||
}
|
||||
@@ -265,7 +265,7 @@
|
||||
"property": "align-items",
|
||||
"expected": "center"
|
||||
},
|
||||
"message": "Set <kbd>align-items</kbd> to <kbd>center</kbd>",
|
||||
"message": "Which property centers items along the cross axis?",
|
||||
"options": {
|
||||
"exact": true
|
||||
}
|
||||
@@ -327,7 +327,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "font-family:\\s*Courier,\\s*monospace",
|
||||
"message": "Set the font-family to <kbd>Courier, monospace</kbd>",
|
||||
"message": "A font stack lists preferred fonts first, followed by a generic fallback, separated by commas",
|
||||
"options": {
|
||||
"caseSensitive": false
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^input\\[type=\"text\"\\]\\s*{",
|
||||
"message": "Use <kbd>input[type=\"text\"] { … }</kbd> as your attribute selector",
|
||||
"message": "Which attribute selector syntax targets inputs with a specific type? Check the square-bracket notation from the description.",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^a\\[href\\^=\"https\"\\]\\s*{",
|
||||
"message": "Use <kbd>a[href^=\"https\"] { … }</kbd> as your attribute selector to target HTTPS links",
|
||||
"message": "Which partial-match attribute selector targets values that <em>start with</em> a given string? Combine the element name with that selector.",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -145,7 +145,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^\\.main-nav\\s*>\\s*li\\s*{",
|
||||
"message": "Use <kbd>.main-nav > li { … }</kbd> with the child combinator to target only direct children",
|
||||
"message": "Which combinator selects only <em>direct</em> children, skipping deeper descendants? Place it between the parent and child selectors.",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -203,7 +203,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^nav\\s+a\\s*{",
|
||||
"message": "Use <kbd>nav a</kbd> with a space between nav and a",
|
||||
"message": "The descendant combinator is the simplest one — what character separates a parent selector from a descendant selector?",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -261,7 +261,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^h2\\s*\\+\\s*p\\s*{",
|
||||
"message": "Use <kbd>h2 + p</kbd> with the adjacent sibling combinator (+)",
|
||||
"message": "Which combinator targets the element <em>immediately</em> following a sibling? Place it between the two element selectors.",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -319,7 +319,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^h3\\s*~\\s*p\\s*{",
|
||||
"message": "Use <kbd>h3 ~ p</kbd> with the general sibling combinator (~)",
|
||||
"message": "Which combinator selects <em>all</em> later siblings, not just the one right next to it? Place it between the two element selectors.",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -377,7 +377,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^button:hover\\s*{",
|
||||
"message": "Use <kbd>button:hover</kbd> to target buttons on hover",
|
||||
"message": "Which pseudo-class activates when the cursor is over an element? Attach it to the button selector with a colon.",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
@@ -435,7 +435,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "^li:first-child\\s*{",
|
||||
"message": "Use <kbd>li:first-child</kbd> to target first list items",
|
||||
"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.",
|
||||
"options": {
|
||||
"caseSensitive": true
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Set <kbd>padding: 1rem</kbd>"
|
||||
"message": "Which property adds space between an element's content and its border?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Set <kbd>border-left: 4px solid steelblue</kbd>",
|
||||
"message": "Use the <kbd>border-left</kbd> shorthand with width, style, and color values",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Set <kbd>margin-bottom: 1rem</kbd>"
|
||||
"message": "Which property creates space below an element, pushing neighbors away?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Set <kbd>box-sizing: border-box</kbd>"
|
||||
"message": "Which <kbd>box-sizing</kbd> value includes padding and border in the element's total width?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Set <kbd>padding: 8px 1rem</kbd>",
|
||||
"message": "Use the <kbd>padding</kbd> shorthand with two values: vertical then horizontal",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Set <kbd>margin: 0 auto</kbd>",
|
||||
"message": "Use <kbd>margin</kbd> with a keyword that auto-calculates equal left and right spacing",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Set <kbd>border-radius: 50%</kbd>"
|
||||
"message": "Which <kbd>border-radius</kbd> percentage creates a perfect circle from a square element?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Set <kbd>padding: 1rem</kbd>"
|
||||
"message": "Add inner spacing to the notification card"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Set <kbd>border-left: 4px solid coral</kbd>",
|
||||
"message": "Add a left border accent using the <kbd>border-left</kbd> shorthand",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Set <kbd>border-radius: 4px</kbd>"
|
||||
"message": "Round the corners slightly with <kbd>border-radius</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background-color", "expected": "seashell" },
|
||||
"message": "Set <kbd>background-color: seashell</kbd>"
|
||||
"message": "Which property sets the fill color behind an element's content area?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Set <kbd>color: coral</kbd>"
|
||||
"message": "Which CSS property changes the color of text content?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-color", "expected": "coral" },
|
||||
"message": "Set <kbd>border-color: coral</kbd>"
|
||||
"message": "Which property changes just the color of an existing border?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background-color", "expected": "#ffd700" },
|
||||
"message": "Set <kbd>background-color: #ffd700</kbd>"
|
||||
"message": "Set the <kbd>background-color</kbd> using a hex code format"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "2px 2px",
|
||||
"message": "Set offset to <kbd>2px 2px</kbd>"
|
||||
"message": "How far should the shadow move horizontally and vertically?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Set <kbd>max-width: 40rem</kbd>"
|
||||
"message": "Which property caps an element's width? Try a <kbd>rem</kbd> value for readable line length."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Set <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"message": "Use <kbd>calc()</kbd> to subtract the sidebar's fixed width from the full container width.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Set <kbd>min-height: 100vh</kbd>"
|
||||
"message": "Which property ensures a minimum height? Use a viewport unit for full-screen coverage."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Set <kbd>transition: background-color 0.3s</kbd>",
|
||||
"message": "Specify which property to transition and how long it should take.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Set timing to <kbd>ease-in-out</kbd>"
|
||||
"message": "Which easing keyword starts slow, speeds up, then slows down again?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"message": "Use the <kbd>animation</kbd> shorthand: name, duration, and repeat count.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Set <kbd>animation-name: pulse</kbd>"
|
||||
"message": "Which property links an element to a named <kbd>@keyframes</kbd> rule?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Set <kbd>animation-duration: 2s</kbd>"
|
||||
"message": "How long should one full cycle of the animation take?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Set <kbd>animation-delay: 1s</kbd>"
|
||||
"message": "Which property makes the animation wait before starting?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Set <kbd>animation-iteration-count: 2</kbd>"
|
||||
"message": "Which property controls how many times the animation repeats?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
|
||||
"message": "Which property keeps the element styled in its final keyframe state after the animation ends?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,14 +18,24 @@
|
||||
"codeSuffix": "}",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{ "type": "contains", "value": "display", "message": "Use <kbd>display: flex</kbd>", "options": { "caseSensitive": false } },
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "display",
|
||||
"message": "Which display mode arranges children in a row or column?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "justify-content",
|
||||
"message": "Use <kbd>justify-content: center</kbd>",
|
||||
"message": "How do you center items along the main axis?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{ "type": "contains", "value": "align-items", "message": "Use <kbd>align-items: center</kbd>", "options": { "caseSensitive": false } }
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "align-items",
|
||||
"message": "Which property centers items along the cross axis?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -44,13 +54,13 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "flex-wrap: wrap",
|
||||
"message": "Use <kbd>flex-wrap: wrap</kbd>",
|
||||
"message": "Which property allows flex items to flow onto multiple lines?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": ".item.*flex:\\s*1\\s+1\\s+100px",
|
||||
"message": "Set <kbd>flex: 1 1 100px</kbd> on items",
|
||||
"message": "The <kbd>flex</kbd> shorthand takes grow, shrink, and basis values — what basis size should each item start from?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -68,17 +78,22 @@
|
||||
"codeSuffix": "}",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{ "type": "contains", "value": "display: grid", "message": "Use <kbd>display: grid</kbd>", "options": { "caseSensitive": false } },
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "display: grid",
|
||||
"message": "Which display mode lets you define rows and columns?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "grid-template-columns",
|
||||
"message": "Define <kbd>grid-template-columns</kbd>",
|
||||
"message": "Which property defines the column structure of a grid?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "grid-template-columns:\\s*repeat\\(3,\\s*1fr\\)\\s*",
|
||||
"message": "Create three equal columns with <kbd>repeat(3, 1fr)</kbd>",
|
||||
"message": "The <kbd>repeat()</kbd> function can create equal-width columns — how many do you need, and what unit makes them equal?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{ "type": "contains", "value": "gap", "message": "Use <kbd>gap</kbd> property", "options": { "caseSensitive": false } }
|
||||
@@ -106,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "grid-column", "expected": "1 / span 2" },
|
||||
"message": "Span across 2 columns with <kbd>grid-column: 1 / span 2</kbd>",
|
||||
"message": "Use <kbd>grid-column</kbd> with a start line and a span count \u2014 how many columns should this item stretch across?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||
"message": "Use <kbd>@media (max-width: 600px)</kbd>",
|
||||
"message": "Start with an <kbd>@media</kbd> rule \u2014 which condition targets screens 600px wide or smaller?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Set <kbd>background: lightcoral</kbd>",
|
||||
"message": "Which property changes the element's background color?",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -53,7 +53,11 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Which CSS unit scales relative to the viewport width?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -73,18 +77,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Set <kbd>display: grid</kbd>"
|
||||
"message": "Which display mode lets you define rows and columns?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||
"message": "Try <kbd>repeat()</kbd> with <kbd>auto-fit</kbd> and <kbd>minmax()</kbd> — what minimum and maximum sizes create flexible columns?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||
"message": "Which property adds space between grid items?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -105,7 +109,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||
"message": "Use <kbd>@media (min-width: 768px)</kbd>",
|
||||
"message": "Which <kbd>@media</kbd> condition applies styles when the viewport is at least 768px wide?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -117,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Set <kbd>width: 250px</kbd>",
|
||||
"message": "Which property controls how wide the sidebar should be on larger screens?",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "linear-gradient",
|
||||
"message": "Use <kbd>linear-gradient()</kbd>"
|
||||
"message": "Which CSS function creates a smooth transition between colors along a straight line?"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -53,7 +53,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "to right",
|
||||
"message": "Add <kbd>to right</kbd> to set the direction"
|
||||
"message": "Which direction keyword makes a gradient flow horizontally from the left side?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "radial-gradient",
|
||||
"message": "Use <kbd>radial-gradient()</kbd>"
|
||||
"message": "Which CSS function creates a gradient that radiates outward from a center point?"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "bg-blue-500",
|
||||
"message": "Add the <kbd>bg-blue-500</kbd> class for a blue background."
|
||||
"message": "Which Tailwind utility sets a blue background color? Think about the <kbd>bg-{color}-{shade}</kbd> pattern."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -38,22 +38,22 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "bg-white",
|
||||
"message": "Add <kbd>bg-white</kbd> to set the background color to white."
|
||||
"message": "Which Tailwind utility sets a white background? The pattern is <kbd>bg-{color}</kbd>."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "p-4",
|
||||
"message": "Add <kbd>p-4</kbd> to apply 1rem padding on all sides."
|
||||
"message": "Which Tailwind utility adds 1rem padding on all sides? Remember: each spacing unit is 0.25rem."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "rounded",
|
||||
"message": "Add <kbd>rounded</kbd> to apply border-radius of 0.25rem."
|
||||
"message": "Which Tailwind utility adds rounded corners? It is one of the simplest utility names."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "shadow-sm",
|
||||
"message": "Add <kbd>shadow-sm</kbd> to apply small drop-shadow."
|
||||
"message": "Which Tailwind utility adds a small drop-shadow? Look for a <kbd>shadow-</kbd> variant."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,17 +71,17 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-blue-600",
|
||||
"message": "Add <kbd>text-blue-600</kbd> to make the text blue"
|
||||
"message": "Which Tailwind utility controls text color? Use the <kbd>text-{color}-{shade}</kbd> pattern with a blue shade."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-2xl",
|
||||
"message": "Add <kbd>text-2xl</kbd> to increase the font size to 1.5rem"
|
||||
"message": "Which Tailwind utility sets the font size to 1.5rem? Check the <kbd>text-{size}</kbd> scale."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "font-bold",
|
||||
"message": "Add <kbd>font-bold</kbd> to make the text bold (font-weight: 700)"
|
||||
"message": "Which Tailwind utility makes text bold? The <kbd>font-{weight}</kbd> pattern controls font weight."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -99,17 +99,17 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "px-6",
|
||||
"message": "Add <kbd>px-6</kbd> for horizontal padding (1.5rem left and right)"
|
||||
"message": "Which Tailwind utility adds horizontal padding of 1.5rem? The <kbd>px-</kbd> prefix targets left and right."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "py-3",
|
||||
"message": "Add <kbd>py-3</kbd> for vertical padding (0.75rem top and bottom)"
|
||||
"message": "Which Tailwind utility adds vertical padding of 0.75rem? The <kbd>py-</kbd> prefix targets top and bottom."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "mx-auto",
|
||||
"message": "Add <kbd>mx-auto</kbd> to center the button horizontally"
|
||||
"message": "Which Tailwind utility centers an element horizontally using auto margins?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,32 +127,32 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "w-full",
|
||||
"message": "Add <kbd>w-full</kbd> for 100% width on mobile"
|
||||
"message": "Which Tailwind utility makes an element take up 100% width? This is the base (mobile) style."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "md:w-1/2",
|
||||
"message": "Add <kbd>md:w-1/2</kbd> for 50% width on tablet and up"
|
||||
"message": "How do you set 50% width at the <kbd>md:</kbd> breakpoint? Tailwind uses fraction notation for widths."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "lg:w-1/3",
|
||||
"message": "Add <kbd>lg:w-1/3</kbd> for 33.33% width on desktop and up"
|
||||
"message": "How do you set one-third width at the <kbd>lg:</kbd> breakpoint? Use the same fraction pattern."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-lg",
|
||||
"message": "Add <kbd>text-lg</kbd> for the base text size"
|
||||
"message": "Which Tailwind text size utility is one step above the base size? Think about the <kbd>text-{size}</kbd> scale."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "md:text-xl",
|
||||
"message": "Add <kbd>md:text-xl</kbd> for larger text on tablets"
|
||||
"message": "How do you increase the text size at the <kbd>md:</kbd> breakpoint? Go one step larger."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "lg:text-2xl",
|
||||
"message": "Add <kbd>lg:text-2xl</kbd> for even larger text on desktop"
|
||||
"message": "How do you set an even larger text size at the <kbd>lg:</kbd> breakpoint? Continue stepping up the scale."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "filter", "expected": "blur(4px)" },
|
||||
"message": "Set <kbd>filter: blur(4px)</kbd>"
|
||||
"message": "Which CSS property applies visual effects like blur? Use the <kbd>blur()</kbd> function with a pixel value."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -48,7 +48,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "100%",
|
||||
"message": "Set to <kbd>100%</kbd> for full grayscale"
|
||||
"message": "What percentage value removes all color completely?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "120%",
|
||||
"message": "Set to <kbd>120%</kbd>"
|
||||
"message": "What percentage makes the element slightly brighter than normal? Normal is 100%."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "4px 4px 8px",
|
||||
"message": "Set shadow offset and blur"
|
||||
"message": "Set the x-offset, y-offset, and blur radius. The task describes the exact values needed."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "position", "expected": "relative" },
|
||||
"message": "Set <kbd>position: relative</kbd>"
|
||||
"message": "Which position value keeps an element in normal flow but allows offset adjustments?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "top", "expected": "-8px" },
|
||||
"message": "Set <kbd>top: -8px</kbd>"
|
||||
"message": "Which offset property moves an element upward from its current position?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "position", "expected": "absolute" },
|
||||
"message": "Set <kbd>position: absolute</kbd>"
|
||||
"message": "Which position value removes an element from normal flow for precise placement?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,12 +85,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "top", "expected": "8px" },
|
||||
"message": "Set <kbd>top: 8px</kbd>"
|
||||
"message": "Which offset property controls the distance from the top of the positioned ancestor?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "right", "expected": "8px" },
|
||||
"message": "Set <kbd>right: 8px</kbd>"
|
||||
"message": "Which offset property controls the distance from the right edge?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Set <kbd>color: coral</kbd>"
|
||||
"message": "Which CSS property changes the text color of the bullet? Try a warm, pinkish-orange named color."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,17 +95,17 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "40px" },
|
||||
"message": "Set <kbd>width: 40px</kbd>"
|
||||
"message": "How wide should the decorative line be? Check the task for the pixel value."
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "height", "expected": "3px" },
|
||||
"message": "Set <kbd>height: 3px</kbd>"
|
||||
"message": "Which CSS property controls the thickness of the line? A thin line looks best here."
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Set <kbd>background: steelblue</kbd>"
|
||||
"message": "Which CSS property fills the line with color? Use a steel-toned blue named color."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "أضف <kbd>color: coral;</kbd>"
|
||||
"message": "ما الخاصية التي تتحكم في لون النص؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "أضف <kbd>background: lavender;</kbd>"
|
||||
"message": "تحقق من خاصية <kbd>background</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "أضف <kbd>padding: 1rem;</kbd>"
|
||||
"message": "البطاقة تحتاج إلى مساحة داخل حوافها"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "اضبط <kbd>color: steelblue</kbd>"
|
||||
"message": "ما الخاصية التي تغيّر لون النص؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "اضبط <kbd>color: coral</kbd>"
|
||||
"message": "ما القيمة التي تعطي لوناً دافئاً برتقالياً محمراً؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "اضبط <kbd>background: tomato</kbd>"
|
||||
"message": "الشارة تحتاج إلى خلفية حمراء زاهية"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "اضبط <kbd>background: steelblue</kbd>"
|
||||
"message": "ما الخاصية التي تضبط لون تعبئة الزر؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "اضبط <kbd>text-decoration: none</kbd>"
|
||||
"message": "ما الخاصية التي تتحكم في الخط أسفل الروابط؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "اضبط <kbd>color: steelblue</kbd>"
|
||||
"message": "تحقق من خاصية <kbd>color</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "اضبط <kbd>color: white</kbd>"
|
||||
"message": "الروابط تحتاج إلى أن تبرز على الخلفية الزرقاء"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "اضبط <kbd>font-size: 0.9rem</kbd>"
|
||||
"message": "تحقق من خاصية <kbd>font-size</kbd> — النص يجب أن يكون أصغر قليلاً"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "اضبط <kbd>padding: 1rem</kbd>"
|
||||
"message": "ما الخاصية التي تضيف مساحة بين محتوى العنصر وحدوده؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "اضبط <kbd>border-left: 4px solid steelblue</kbd>",
|
||||
"message": "استخدم اختصار <kbd>border-left</kbd> مع قيم العرض والنمط واللون",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "اضبط <kbd>margin-bottom: 1rem</kbd>"
|
||||
"message": "ما الخاصية التي تُنشئ مساحة أسفل العنصر وتدفع الجيران بعيداً؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "اضبط <kbd>box-sizing: border-box</kbd>"
|
||||
"message": "ما قيمة <kbd>box-sizing</kbd> التي تشمل الحشو والحدود في العرض الإجمالي للعنصر؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "اضبط <kbd>padding: 8px 1rem</kbd>",
|
||||
"message": "استخدم اختصار <kbd>padding</kbd> بقيمتين: عمودي ثم أفقي",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "اضبط <kbd>margin: 0 auto</kbd>",
|
||||
"message": "استخدم <kbd>margin</kbd> مع كلمة مفتاحية تحسب تلقائياً مسافات متساوية يميناً ويساراً",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "اضبط <kbd>border-radius: 50%</kbd>"
|
||||
"message": "ما نسبة <kbd>border-radius</kbd> التي تُنشئ دائرة كاملة من عنصر مربع؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "اضبط <kbd>padding: 1rem</kbd>"
|
||||
"message": "أضف مساحة داخلية لبطاقة الإشعار"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "اضبط <kbd>border-left: 4px solid coral</kbd>",
|
||||
"message": "أضف لمسة حدود يسارية باستخدام اختصار <kbd>border-left</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "اضبط <kbd>border-radius: 4px</kbd>"
|
||||
"message": "دوّر الزوايا قليلاً باستخدام <kbd>border-radius</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "اضبط <kbd>max-width: 40rem</kbd>"
|
||||
"message": "ما الخاصية التي تحدّ من عرض العنصر؟ جرّب قيمة بوحدة <kbd>rem</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>width: calc(100% - 200px)</kbd>",
|
||||
"message": "استخدم <kbd>calc()</kbd> لطرح عرض الشريط الجانبي الثابت من عرض الحاوية الكامل.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "اضبط <kbd>min-height: 100vh</kbd>"
|
||||
"message": "ما الخاصية التي تضمن حداً أدنى للارتفاع؟ استخدم وحدة viewport لتغطية الشاشة بالكامل."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "اضبط <kbd>transition: background-color 0.3s</kbd>",
|
||||
"message": "حدد أي خاصية تريد تحريكها وكم من الوقت يجب أن تستغرق.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "اضبط التوقيت على <kbd>ease-in-out</kbd>"
|
||||
"message": "ما كلمة التسهيل التي تبدأ بطيئة، تتسارع، ثم تبطئ مرة أخرى؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "طبّق <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"message": "استخدم اختصار <kbd>animation</kbd>: الاسم، المدة، وعدد التكرار.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "اضبط <kbd>animation-name: pulse</kbd>"
|
||||
"message": "ما الخاصية التي تربط العنصر بقاعدة <kbd>@keyframes</kbd> مسماة؟"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "اضبط <kbd>animation-duration: 2s</kbd>"
|
||||
"message": "كم يجب أن تستغرق دورة كاملة من الحركة؟"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "اضبط <kbd>animation-delay: 1s</kbd>"
|
||||
"message": "ما الخاصية التي تجعل الحركة تنتظر قبل أن تبدأ؟"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "اضبط <kbd>animation-iteration-count: 2</kbd>"
|
||||
"message": "ما الخاصية التي تتحكم في عدد مرات تكرار الحركة؟"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "اضبط <kbd>animation-fill-mode: forwards</kbd>"
|
||||
"message": "ما الخاصية التي تُبقي العنصر بتنسيق حالته النهائية بعد انتهاء الحركة؟"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||
"message": "استخدم <kbd>@media (max-width: 600px)</kbd>",
|
||||
"message": "ابدأ بقاعدة <kbd>@media</kbd> — ما الشرط الذي يستهدف الشاشات بعرض 600px أو أقل؟",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "اضبط <kbd>background: lightcoral</kbd>",
|
||||
"message": "ما الخاصية التي تغيّر لون خلفية العنصر؟",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -53,7 +53,11 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "اضبط <kbd>font-size: 5vw</kbd>" }
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "ما وحدة CSS التي تتناسب مع عرض viewport؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -73,18 +77,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "اضبط <kbd>display: grid</kbd>"
|
||||
"message": "ما وضع العرض الذي يتيح لك تعريف صفوف وأعمدة؟"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||
"message": "استخدم <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||
"message": "جرّب <kbd>repeat()</kbd> مع <kbd>auto-fit</kbd> و <kbd>minmax()</kbd> — ما الحد الأدنى والأقصى للحجم لإنشاء أعمدة مرنة؟",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "اضبط <kbd>gap: 1rem</kbd>"
|
||||
"message": "ما الخاصية التي تضيف مساحة بين عناصر الشبكة؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -105,7 +109,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||
"message": "استخدم <kbd>@media (min-width: 768px)</kbd>",
|
||||
"message": "ما شرط <kbd>@media</kbd> الذي يُطبّق الأنماط عندما يكون عرض viewport على الأقل 768px؟",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -117,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "اضبط <kbd>width: 250px</kbd>",
|
||||
"message": "ما الخاصية التي تتحكم في عرض الشريط الجانبي على الشاشات الكبيرة؟",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "اضبط <kbd>display: flex</kbd>"
|
||||
"message": "ما قيمة display التي تحوّل العنصر إلى حاوية صندوق مرن؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "اضبط <kbd>gap: 1rem</kbd>"
|
||||
"message": "ما الخاصية التي تُنشئ تباعداً بين عناصر flex بدون استخدام الهوامش؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "اضبط <kbd>justify-content: space-between</kbd>"
|
||||
"message": "ما قيمة <kbd>justify-content</kbd> التي تدفع العنصر الأول والأخير إلى الحواف المتقابلة؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "اضبط <kbd>align-items: center</kbd>"
|
||||
"message": "ما الخاصية التي تُحاذي عناصر flex على طول المحور المتقاطع؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "اضبط <kbd>flex-wrap: wrap</kbd>"
|
||||
"message": "ما الخاصية التي تسمح لعناصر flex بالتدفق إلى أسطر متعددة؟"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "اضبط <kbd>flex: 1</kbd>"
|
||||
"message": "ما الخاصية التي تجعل عنصر flex ينمو لملء المساحة المتبقية؟"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Füge <kbd>color: coral;</kbd> hinzu"
|
||||
"message": "Welche Eigenschaft ändert die Textfarbe?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Füge <kbd>background: lavender;</kbd> hinzu"
|
||||
"message": "Welche Eigenschaft steuert die Hintergrundfarbe?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Füge <kbd>padding: 1rem;</kbd> hinzu"
|
||||
"message": "Das Element benötigt auch Innenabstand -- überprüfe die <kbd>padding</kbd>-Eigenschaft"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Setze <kbd>color: steelblue</kbd>"
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welcher Farbwert wurde in der Beschreibung genannt?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Setze <kbd>color: coral</kbd>"
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welche Farbe sollen die Links haben?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "Setze <kbd>background: tomato</kbd>"
|
||||
"message": "Überprüfe die <kbd>background</kbd>-Eigenschaft -- welche Farbe soll das Badge haben?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Setze <kbd>background: steelblue</kbd>"
|
||||
"message": "Überprüfe die <kbd>background</kbd>-Eigenschaft -- welche Farbe soll der primäre Button haben?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Setze <kbd>text-decoration: none</kbd>"
|
||||
"message": "Welche Eigenschaft entfernt die Unterstreichung von Links?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Setze <kbd>color: steelblue</kbd>"
|
||||
"message": "Welche Eigenschaft ändert die Textfarbe der Überschriften?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Setze <kbd>color: white</kbd>"
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welche Farbe passt zu einem dunklen Hintergrund?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Setze <kbd>font-size: 0.9rem</kbd>"
|
||||
"message": "Welche Eigenschaft steuert die Schriftgröße?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"property": "background-color",
|
||||
"expected": "lightblue"
|
||||
},
|
||||
"message": "Setze die Hintergrundfarbe auf <kbd>lightblue</kbd>"
|
||||
"message": "Überprüfe die <kbd>background-color</kbd>-Eigenschaft -- welche Farbe sollen die Text-Eingabefelder haben?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -56,7 +56,7 @@
|
||||
"property": "border",
|
||||
"expected": "2px solid blue"
|
||||
},
|
||||
"message": "Setze den Rahmen auf <kbd>2px solid blue</kbd>"
|
||||
"message": "Das Element benötigt einen Rahmen -- überprüfe die <kbd>border</kbd>-Eigenschaft"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -101,7 +101,7 @@
|
||||
"property": "color",
|
||||
"expected": "green"
|
||||
},
|
||||
"message": "Setze die Textfarbe auf <kbd>green</kbd>"
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welche Farbe kennzeichnet sichere Links?"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -114,7 +114,7 @@
|
||||
"property": "text-decoration",
|
||||
"expected": "underline"
|
||||
},
|
||||
"message": "Setze text-decoration auf <kbd>underline</kbd>, um HTTPS-Links zu unterstreichen"
|
||||
"message": "Welcher <kbd>text-decoration</kbd>-Wert macht Links visuell hervorgehoben?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -159,7 +159,7 @@
|
||||
"property": "background-color",
|
||||
"expected": "cornflowerblue"
|
||||
},
|
||||
"message": "Setze background-color auf <kbd>cornflowerblue</kbd> für das Hauptmenü-Styling"
|
||||
"message": "Überprüfe die <kbd>background-color</kbd>-Eigenschaft für die Hauptmenüpunkte"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -172,7 +172,7 @@
|
||||
"property": "color",
|
||||
"expected": "white"
|
||||
},
|
||||
"message": "Setze die Textfarbe auf <kbd>white</kbd> für Kontrast gegen den blauen Hintergrund"
|
||||
"message": "Welche Textfarbe sorgt für guten Kontrast auf einem blauen Hintergrund?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -217,7 +217,7 @@
|
||||
"property": "text-decoration",
|
||||
"expected": "none"
|
||||
},
|
||||
"message": "Setze text-decoration auf <kbd>none</kbd>"
|
||||
"message": "Welcher <kbd>text-decoration</kbd>-Wert entfernt die Unterstreichung?"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -230,7 +230,7 @@
|
||||
"property": "color",
|
||||
"expected": "blue"
|
||||
},
|
||||
"message": "Setze color auf <kbd>blue</kbd>"
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft für die Links"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -275,7 +275,7 @@
|
||||
"property": "margin-top",
|
||||
"expected": "0"
|
||||
},
|
||||
"message": "Setze margin-top auf <kbd>0</kbd>"
|
||||
"message": "Welcher Wert bei <kbd>margin-top</kbd> entfernt den oberen Abstand?"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -288,7 +288,7 @@
|
||||
"property": "font-style",
|
||||
"expected": "italic"
|
||||
},
|
||||
"message": "Setze font-style auf <kbd>italic</kbd>"
|
||||
"message": "Welcher <kbd>font-style</kbd>-Wert macht den Text kursiv?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -333,7 +333,7 @@
|
||||
"property": "color",
|
||||
"expected": "gray"
|
||||
},
|
||||
"message": "Setze color auf <kbd>gray</kbd>"
|
||||
"message": "Überprüfe die <kbd>color</kbd>-Eigenschaft -- welche Farbe sollen die Absätze haben?"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -346,7 +346,7 @@
|
||||
"property": "padding-left",
|
||||
"expected": "20px"
|
||||
},
|
||||
"message": "Setze padding-left auf <kbd>20px</kbd>"
|
||||
"message": "Das Element benötigt eine Einrückung -- überprüfe die <kbd>padding-left</kbd>-Eigenschaft"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -391,7 +391,7 @@
|
||||
"property": "background-color",
|
||||
"expected": "darkblue"
|
||||
},
|
||||
"message": "Setze background-color auf <kbd>darkblue</kbd>"
|
||||
"message": "Welche Hintergrundfarbe soll der Button beim Hover haben?"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -404,7 +404,7 @@
|
||||
"property": "color",
|
||||
"expected": "white"
|
||||
},
|
||||
"message": "Setze color auf <kbd>white</kbd>"
|
||||
"message": "Welche Textfarbe sorgt für Kontrast auf dunklem Hintergrund?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -449,7 +449,7 @@
|
||||
"property": "font-weight",
|
||||
"expected": "bold"
|
||||
},
|
||||
"message": "Setze font-weight auf <kbd>bold</kbd>"
|
||||
"message": "Welcher <kbd>font-weight</kbd>-Wert macht Text fett?"
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
@@ -462,7 +462,7 @@
|
||||
"property": "margin-top",
|
||||
"expected": "0"
|
||||
},
|
||||
"message": "Setze margin-top auf <kbd>0</kbd>"
|
||||
"message": "Welcher Wert entfernt den oberen Abstand vollständig?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Setze <kbd>padding: 1rem</kbd>"
|
||||
"message": "Welche Eigenschaft steuert den Innenabstand zwischen Inhalt und Rahmen?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Setze <kbd>border-left: 4px solid steelblue</kbd>",
|
||||
"message": "Überprüfe die <kbd>border-left</kbd>-Eigenschaft -- welche drei Werte braucht sie?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Setze <kbd>margin-bottom: 1rem</kbd>"
|
||||
"message": "Welche Eigenschaft steuert den Außenabstand nach unten?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Setze <kbd>box-sizing: border-box</kbd>"
|
||||
"message": "Welcher <kbd>box-sizing</kbd>-Wert bezieht Padding und Rahmen in die Breite ein?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Setze <kbd>padding: 8px 1rem</kbd>",
|
||||
"message": "Überprüfe die <kbd>padding</kbd>-Kurzschreibweise -- zwei Werte setzen vertikal und horizontal",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Setze <kbd>margin: 0 auto</kbd>",
|
||||
"message": "Welche <kbd>margin</kbd>-Kurzschreibweise zentriert ein Block-Element horizontal?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Setze <kbd>border-radius: 50%</kbd>"
|
||||
"message": "Welcher <kbd>border-radius</kbd>-Wert macht ein quadratisches Element rund?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Setze <kbd>padding: 1rem</kbd>"
|
||||
"message": "Das Element benötigt Innenabstand -- überprüfe die <kbd>padding</kbd>-Eigenschaft"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Setze <kbd>border-left: 4px solid coral</kbd>",
|
||||
"message": "Überprüfe die <kbd>border-left</kbd>-Eigenschaft -- sie braucht Breite, Stil und Farbe",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Setze <kbd>border-radius: 4px</kbd>"
|
||||
"message": "Das Element benötigt abgerundete Ecken -- überprüfe die <kbd>border-radius</kbd>-Eigenschaft"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Setze <kbd>max-width: 40rem</kbd>"
|
||||
"message": "Welche Eigenschaft begrenzt die maximale Breite eines Elements?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "steelblue",
|
||||
"message": "Setze den Wert auf <kbd>steelblue</kbd>",
|
||||
"message": "Welche Farbe soll die Variable haben?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Setze <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"message": "Überprüfe die <kbd>width</kbd>-Eigenschaft -- wie berechnest du den verbleibenden Platz nach der Sidebar?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Setze <kbd>min-height: 100vh</kbd>"
|
||||
"message": "Welche Eigenschaft setzt die Mindesthöhe? Welche Viewport-Einheit entspricht 100% der Fensterhöhe?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Setze <kbd>transition: background-color 0.3s</kbd>",
|
||||
"message": "Überprüfe die <kbd>transition</kbd>-Eigenschaft -- welche CSS-Eigenschaft soll sanft übergehen und wie lange?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Setze timing auf <kbd>ease-in-out</kbd>"
|
||||
"message": "Welche Timing-Funktion startet und endet langsam?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Setze <kbd>animation-name: pulse</kbd>"
|
||||
"message": "Welche Animation soll angewendet werden? Überprüfe den <kbd>@keyframes</kbd>-Namen."
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Setze <kbd>animation-duration: 2s</kbd>"
|
||||
"message": "Welche Eigenschaft steuert die Dauer der Animation?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Setze <kbd>animation-delay: 1s</kbd>"
|
||||
"message": "Welche Eigenschaft verzögert den Start der Animation?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Setze <kbd>animation-iteration-count: 2</kbd>"
|
||||
"message": "Welche Eigenschaft steuert, wie oft die Animation wiederholt wird?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Setze <kbd>animation-fill-mode: forwards</kbd>"
|
||||
"message": "Welcher <kbd>animation-fill-mode</kbd>-Wert behält den Endzustand bei?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Setze <kbd>background: lightcoral</kbd>",
|
||||
"message": "Überprüfe die <kbd>background</kbd>-Eigenschaft innerhalb der Media Query",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -53,7 +53,11 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Setze <kbd>font-size: 5vw</kbd>" }
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Welche Eigenschaft steuert die Schriftgröße? Welche Viewport-Einheit skaliert mit der Breite?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -73,7 +77,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Setze <kbd>display: grid</kbd>"
|
||||
"message": "Welcher Display-Wert aktiviert das CSS-Grid-Layout?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -84,7 +88,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Setze <kbd>gap: 1rem</kbd>"
|
||||
"message": "Welche Eigenschaft steuert den Abstand zwischen Grid-Zellen?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -117,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Setze <kbd>width: 250px</kbd>",
|
||||
"message": "Überprüfe die <kbd>width</kbd>-Eigenschaft für die Sidebar",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "bg-blue-500",
|
||||
"message": "Füge die <kbd>bg-blue-500</kbd>-Klasse für einen blauen Hintergrund hinzu."
|
||||
"message": "Welche Tailwind-Klasse setzt eine blaue Hintergrundfarbe? Denke an das <kbd>bg-{farbe}-{abstufung}</kbd>-Muster."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -38,22 +38,22 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "bg-white",
|
||||
"message": "Füge <kbd>bg-white</kbd> hinzu, um die Hintergrundfarbe auf weiß zu setzen."
|
||||
"message": "Das Element benötigt einen weißen Hintergrund -- welches <kbd>bg-</kbd>-Utility passt?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "p-4",
|
||||
"message": "Füge <kbd>p-4</kbd> hinzu, um 1rem Padding auf allen Seiten anzuwenden."
|
||||
"message": "Welches <kbd>p-</kbd>-Utility erzeugt 1rem Padding auf allen Seiten?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "rounded",
|
||||
"message": "Füge <kbd>rounded</kbd> hinzu, um einen border-radius von 0.25rem anzuwenden."
|
||||
"message": "Welche Klasse fügt abgerundete Ecken hinzu?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "shadow-sm",
|
||||
"message": "Füge <kbd>shadow-sm</kbd> hinzu, um einen kleinen Schlagschatten anzuwenden."
|
||||
"message": "Das Element benötigt einen kleinen Schatten -- welches <kbd>shadow-</kbd>-Utility passt?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,17 +71,17 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-blue-600",
|
||||
"message": "Füge <kbd>text-blue-600</kbd> hinzu, um den Text blau zu machen"
|
||||
"message": "Welches <kbd>text-</kbd>-Utility setzt eine blaue Textfarbe? Denke an das <kbd>text-{farbe}-{abstufung}</kbd>-Muster."
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-2xl",
|
||||
"message": "Füge <kbd>text-2xl</kbd> hinzu, um die Schriftgröße auf 1.5rem zu erhöhen"
|
||||
"message": "Welches <kbd>text-</kbd>-Utility setzt die Schriftgröße auf 1.5rem?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "font-bold",
|
||||
"message": "Füge <kbd>font-bold</kbd> hinzu, um den Text fett zu machen (font-weight: 700)"
|
||||
"message": "Welches <kbd>font-</kbd>-Utility macht den Text fett?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -99,17 +99,17 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "px-6",
|
||||
"message": "Füge <kbd>px-6</kbd> für horizontales Padding hinzu (1.5rem links und rechts)"
|
||||
"message": "Welches <kbd>px-</kbd>-Utility erzeugt 1.5rem horizontales Padding?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "py-3",
|
||||
"message": "Füge <kbd>py-3</kbd> für vertikales Padding hinzu (0.75rem oben und unten)"
|
||||
"message": "Welches <kbd>py-</kbd>-Utility erzeugt 0.75rem vertikales Padding?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "mx-auto",
|
||||
"message": "Füge <kbd>mx-auto</kbd> hinzu, um den Button horizontal zu zentrieren"
|
||||
"message": "Welches <kbd>mx-</kbd>-Utility zentriert ein Element horizontal?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,32 +127,32 @@
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "w-full",
|
||||
"message": "Füge <kbd>w-full</kbd> für 100% Breite auf Mobil hinzu"
|
||||
"message": "Welches Breiten-Utility macht das Element auf Mobil 100% breit?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "md:w-1/2",
|
||||
"message": "Füge <kbd>md:w-1/2</kbd> für 50% Breite auf Tablet und größer hinzu"
|
||||
"message": "Welches responsive Breiten-Utility setzt 50% ab dem <kbd>md:</kbd>-Breakpoint?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "lg:w-1/3",
|
||||
"message": "Füge <kbd>lg:w-1/3</kbd> für 33.33% Breite auf Desktop und größer hinzu"
|
||||
"message": "Welches responsive Breiten-Utility setzt 33.33% ab dem <kbd>lg:</kbd>-Breakpoint?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "text-lg",
|
||||
"message": "Füge <kbd>text-lg</kbd> für die Basis-Textgröße hinzu"
|
||||
"message": "Welches <kbd>text-</kbd>-Utility setzt die Basis-Textgröße auf 1.125rem?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "md:text-xl",
|
||||
"message": "Füge <kbd>md:text-xl</kbd> für größeren Text auf Tablets hinzu"
|
||||
"message": "Welches responsive Text-Utility setzt eine größere Schrift ab dem <kbd>md:</kbd>-Breakpoint?"
|
||||
},
|
||||
{
|
||||
"type": "contains_class",
|
||||
"value": "lg:text-2xl",
|
||||
"message": "Füge <kbd>lg:text-2xl</kbd> für noch größeren Text auf Desktop hinzu"
|
||||
"message": "Welches responsive Text-Utility setzt die größte Schrift ab dem <kbd>lg:</kbd>-Breakpoint?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Setze <kbd>display: flex</kbd>"
|
||||
"message": "Welcher Display-Wert macht ein Element zu einem flexiblen Box-Container?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Setze <kbd>gap: 1rem</kbd>"
|
||||
"message": "Welche Eigenschaft erzeugt Abstände zwischen Flex-Items ohne Margins?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Setze <kbd>justify-content: space-between</kbd>"
|
||||
"message": "Welcher <kbd>justify-content</kbd>-Wert schiebt das erste und letzte Element an gegenüberliegende Ränder?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Setze <kbd>align-items: center</kbd>"
|
||||
"message": "Welche Eigenschaft richtet Flex-Items entlang der Querachse aus?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Setze <kbd>flex-wrap: wrap</kbd>"
|
||||
"message": "Welche Eigenschaft erlaubt Flex-Items, auf mehrere Zeilen umzubrechen?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Setze <kbd>flex: 1</kbd>"
|
||||
"message": "Welche Eigenschaft lässt ein Flex-Item wachsen, um den verbleibenden Platz zu füllen?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Añade <kbd>color: coral;</kbd>"
|
||||
"message": "¿Qué propiedad controla el color del texto?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Añade <kbd>background: lavender;</kbd>"
|
||||
"message": "Revisa la propiedad <kbd>background</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Añade <kbd>padding: 1rem;</kbd>"
|
||||
"message": "La tarjeta necesita espacio dentro de sus bordes"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Establece <kbd>color: steelblue</kbd>"
|
||||
"message": "¿Qué propiedad cambia el color del texto?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Establece <kbd>color: coral</kbd>"
|
||||
"message": "¿Qué valor da un color cálido, rojo-anaranjado?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "Establece <kbd>background: tomato</kbd>"
|
||||
"message": "El badge necesita un fondo rojo brillante"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Establece <kbd>background: steelblue</kbd>"
|
||||
"message": "¿Qué propiedad establece el color de relleno del botón?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Establece <kbd>text-decoration: none</kbd>"
|
||||
"message": "¿Qué propiedad controla el subrayado de los enlaces?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Establece <kbd>color: steelblue</kbd>"
|
||||
"message": "Revisa la propiedad <kbd>color</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Establece <kbd>color: white</kbd>"
|
||||
"message": "Los enlaces necesitan destacar sobre el fondo azul"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Establece <kbd>font-size: 0.9rem</kbd>"
|
||||
"message": "Revisa la propiedad <kbd>font-size</kbd> — el texto debería ser ligeramente más pequeño"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Establece <kbd>padding: 1rem</kbd>"
|
||||
"message": "¿Qué propiedad añade espacio entre el contenido de un elemento y su borde?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Establece <kbd>border-left: 4px solid steelblue</kbd>",
|
||||
"message": "Usa el atajo <kbd>border-left</kbd> con valores de ancho, estilo y color",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Establece <kbd>margin-bottom: 1rem</kbd>"
|
||||
"message": "¿Qué propiedad crea espacio debajo de un elemento, separándolo de sus vecinos?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Establece <kbd>box-sizing: border-box</kbd>"
|
||||
"message": "¿Qué valor de <kbd>box-sizing</kbd> incluye padding y borde en el ancho total del elemento?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Establece <kbd>padding: 8px 1rem</kbd>",
|
||||
"message": "Usa el atajo <kbd>padding</kbd> con dos valores: vertical y luego horizontal",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Establece <kbd>margin: 0 auto</kbd>",
|
||||
"message": "Usa <kbd>margin</kbd> con una palabra clave que calcula automáticamente espaciado igual a izquierda y derecha",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Establece <kbd>border-radius: 50%</kbd>"
|
||||
"message": "¿Qué porcentaje de <kbd>border-radius</kbd> crea un círculo perfecto a partir de un elemento cuadrado?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Establece <kbd>padding: 1rem</kbd>"
|
||||
"message": "El elemento necesita espacio interior"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Establece <kbd>border-left: 4px solid coral</kbd>",
|
||||
"message": "Añade un acento de borde izquierdo usando el atajo <kbd>border-left</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Establece <kbd>border-radius: 4px</kbd>"
|
||||
"message": "Redondea las esquinas ligeramente con <kbd>border-radius</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Establece <kbd>max-width: 40rem</kbd>"
|
||||
"message": "¿Qué propiedad limita el ancho de un elemento? Prueba un valor en <kbd>rem</kbd> para una longitud de línea legible."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "steelblue",
|
||||
"message": "Establece el valor a <kbd>steelblue</kbd>",
|
||||
"message": "Asigna el valor <kbd>steelblue</kbd> a la variable",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Establece <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"message": "Usa <kbd>calc()</kbd> para restar el ancho fijo de la barra lateral del ancho total del contenedor.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Establece <kbd>min-height: 100vh</kbd>"
|
||||
"message": "¿Qué propiedad asegura una altura mínima? Usa una unidad de viewport para cobertura de pantalla completa."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Establece <kbd>transition: background-color 0.3s</kbd>",
|
||||
"message": "Especifica qué propiedad transicionar y cuánto debe durar.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Establece timing a <kbd>ease-in-out</kbd>"
|
||||
"message": "¿Qué palabra clave de easing empieza lento, acelera, y luego desacelera de nuevo?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Aplica <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"message": "Usa el atajo <kbd>animation</kbd>: nombre, duración y número de repeticiones.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Establece <kbd>animation-name: pulse</kbd>"
|
||||
"message": "¿Qué propiedad vincula un elemento a una regla <kbd>@keyframes</kbd> nombrada?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Establece <kbd>animation-duration: 2s</kbd>"
|
||||
"message": "¿Cuánto debe durar un ciclo completo de la animación?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Establece <kbd>animation-delay: 1s</kbd>"
|
||||
"message": "¿Qué propiedad hace que la animación espere antes de comenzar?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Establece <kbd>animation-iteration-count: 2</kbd>"
|
||||
"message": "¿Qué propiedad controla cuántas veces se repite la animación?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Establece <kbd>animation-fill-mode: forwards</kbd>"
|
||||
"message": "¿Qué propiedad mantiene el elemento con los estilos de su último keyframe después de que termina la animación?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||
"message": "Usa <kbd>@media (max-width: 600px)</kbd>",
|
||||
"message": "Empieza con una regla <kbd>@media</kbd> — ¿qué condición apunta a pantallas de 600px de ancho o menos?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Establece <kbd>background: lightcoral</kbd>",
|
||||
"message": "¿Qué propiedad cambia el color de fondo del elemento?",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Establece <kbd>font-size: 5vw</kbd>"
|
||||
"message": "¿Qué unidad CSS escala en relación al ancho del viewport?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -77,18 +77,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Establece <kbd>display: grid</kbd>"
|
||||
"message": "¿Qué modo de display permite definir filas y columnas?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||
"message": "Usa <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||
"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?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Establece <kbd>gap: 1rem</kbd>"
|
||||
"message": "¿Qué propiedad añade espacio entre los elementos del grid?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -109,7 +109,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||
"message": "Usa <kbd>@media (min-width: 768px)</kbd>",
|
||||
"message": "¿Qué condición <kbd>@media</kbd> aplica estilos cuando el viewport tiene al menos 768px de ancho?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -121,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Establece <kbd>width: 250px</kbd>",
|
||||
"message": "¿Qué propiedad controla el ancho de la barra lateral en pantallas más grandes?",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Establece <kbd>display: flex</kbd>"
|
||||
"message": "¿Qué valor de display convierte un elemento en un contenedor de caja flexible?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Establece <kbd>gap: 1rem</kbd>"
|
||||
"message": "¿Qué propiedad crea espaciado entre elementos flex sin usar márgenes?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Establece <kbd>justify-content: space-between</kbd>"
|
||||
"message": "¿Qué valor de <kbd>justify-content</kbd> empuja el primer y último elemento a los extremos opuestos?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Establece <kbd>align-items: center</kbd>"
|
||||
"message": "¿Qué propiedad alinea los elementos flex a lo largo del eje transversal?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Establece <kbd>flex-wrap: wrap</kbd>"
|
||||
"message": "¿Qué propiedad permite que los elementos flex fluyan a múltiples líneas?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Establece <kbd>flex: 1</kbd>"
|
||||
"message": "¿Qué propiedad hace que un elemento flex crezca para llenar el espacio restante?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Set <kbd>display: flex</kbd>"
|
||||
"message": "Which display value turns an element into a flexible box container?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||
"message": "Which property creates spacing between flex items without using margins?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Set <kbd>justify-content: space-between</kbd>"
|
||||
"message": "Which <kbd>justify-content</kbd> value pushes the first and last items to opposite edges?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Set <kbd>align-items: center</kbd>"
|
||||
"message": "Which property aligns flex items along the cross axis?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Set <kbd>flex-wrap: wrap</kbd>"
|
||||
"message": "Which property allows flex items to flow onto multiple lines?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Set <kbd>flex: 1</kbd>"
|
||||
"message": "Which property makes a flex item grow to fill the remaining space?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Set <kbd>display: grid</kbd>"
|
||||
"message": "Which <kbd>display</kbd> value activates the CSS Grid layout system?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "grid-template-columns:\\s*repeat\\(\\s*3\\s*,\\s*1fr\\s*\\)",
|
||||
"message": "Set <kbd>grid-template-columns: repeat(3, 1fr)</kbd>",
|
||||
"message": "Which CSS property defines column sizes in a grid? Use <kbd>repeat()</kbd> with the <kbd>fr</kbd> unit for equal columns.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||
"message": "Which CSS property adds spacing between grid cells without affecting the outer edges?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "grid-column:\\s*span\\s+2",
|
||||
"message": "Set <kbd>grid-column: span 2</kbd>",
|
||||
"message": "Which CSS property makes a grid item stretch across multiple columns? Use the <kbd>span</kbd> keyword.",
|
||||
"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": "Set <kbd>grid-template-columns: repeat(auto-fit, minmax(150px, 1fr))</kbd>",
|
||||
"message": "Which CSS property creates responsive columns? Combine <kbd>auto-fit</kbd> with <kbd>minmax()</kbd> for flexible sizing.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Dodaj <kbd>color: coral;</kbd>"
|
||||
"message": "Która właściwość kontroluje kolor tekstu?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Dodaj <kbd>background: lavender;</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>background</kbd> — jaki kolor potrzebuje karta?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Dodaj <kbd>padding: 1rem;</kbd>"
|
||||
"message": "Element potrzebuje wewnętrznej przestrzeni — sprawdź właściwość <kbd>padding</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Ustaw <kbd>color: steelblue</kbd>"
|
||||
"message": "Która właściwość kontroluje kolor tekstu?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Ustaw <kbd>color: coral</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>color</kbd> — jaki kolor potrzebują linki?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "Ustaw <kbd>background: tomato</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>background</kbd> — jaki kolor potrzebuje badge?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Ustaw <kbd>background: steelblue</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>background</kbd> — jaki kolor potrzebuje przycisk?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Ustaw <kbd>text-decoration: none</kbd>"
|
||||
"message": "Która właściwość kontroluje podkreślenie linków?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Ustaw <kbd>color: steelblue</kbd>"
|
||||
"message": "Która właściwość kontroluje kolor tekstu nagłówków?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Ustaw <kbd>color: white</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>color</kbd> — jaki kolor potrzebują linki nawigacji?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Ustaw <kbd>font-size: 0.9rem</kbd>"
|
||||
"message": "Która właściwość kontroluje rozmiar tekstu?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Ustaw <kbd>padding: 1rem</kbd>"
|
||||
"message": "Element potrzebuje wewnętrznej przestrzeni — sprawdź właściwość <kbd>padding</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Ustaw <kbd>border-left: 4px solid steelblue</kbd>",
|
||||
"message": "Sprawdź właściwość <kbd>border-left</kbd> — jakiej szerokości, stylu i koloru potrzebujesz?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Ustaw <kbd>margin-bottom: 1rem</kbd>"
|
||||
"message": "Która właściwość kontroluje przestrzeń pod elementem?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Ustaw <kbd>box-sizing: border-box</kbd>"
|
||||
"message": "Która wartość <kbd>box-sizing</kbd> włącza padding i ramkę do szerokości?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Ustaw <kbd>padding: 8px 1rem</kbd>",
|
||||
"message": "Sprawdź skrót <kbd>padding</kbd> — dwie wartości oznaczają pion i poziom",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Ustaw <kbd>margin: 0 auto</kbd>",
|
||||
"message": "Sprawdź skrót <kbd>margin</kbd> — jak automatycznie wycentrować element poziomo?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Ustaw <kbd>border-radius: 50%</kbd>"
|
||||
"message": "Która wartość <kbd>border-radius</kbd> tworzy pełne koło?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Ustaw <kbd>padding: 1rem</kbd>"
|
||||
"message": "Element potrzebuje wewnętrznej przestrzeni — sprawdź właściwość <kbd>padding</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Ustaw <kbd>border-left: 4px solid coral</kbd>",
|
||||
"message": "Sprawdź właściwość <kbd>border-left</kbd> — jaki styl akcentu potrzebuje powiadomienie?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Ustaw <kbd>border-radius: 4px</kbd>"
|
||||
"message": "Element potrzebuje zaokrąglonych rogów — sprawdź właściwość <kbd>border-radius</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Ustaw <kbd>max-width: 40rem</kbd>"
|
||||
"message": "Która właściwość ogranicza maksymalną szerokość elementu?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "steelblue",
|
||||
"message": "Ustaw wartość na <kbd>steelblue</kbd>",
|
||||
"message": "Jaki kolor powinna mieć zmienna brand?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Ustaw <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"message": "Sprawdź funkcję <kbd>calc()</kbd> — jak obliczyć szerokość minus sidebar?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Ustaw <kbd>min-height: 100vh</kbd>"
|
||||
"message": "Która właściwość zapewnia minimalną wysokość na cały viewport?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Ustaw <kbd>transition: background-color 0.3s</kbd>",
|
||||
"message": "Sprawdź właściwość <kbd>transition</kbd> — jaką właściwość i czas trwania podać?",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Ustaw timing na <kbd>ease-in-out</kbd>"
|
||||
"message": "Która wartość tworzy płynne przyspieszenie i spowolnienie?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -83,7 +83,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "50%.*transform: translateY\\(-20px\\)",
|
||||
"message": "Przy <kbd>50%</kbd>, użyj <kbd>transform: translateY(-20px)</kbd>",
|
||||
"message": "W połowie animacji piłka powinna podskoczyć w górę — sprawdź <kbd>transform</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Zastosuj <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"message": "Sprawdź skrót <kbd>animation</kbd> — podaj nazwę, czas trwania i powtarzanie",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Ustaw <kbd>animation-name: pulse</kbd>"
|
||||
"message": "Która właściwość wskazuje nazwę animacji do zastosowania?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Ustaw <kbd>animation-duration: 2s</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>animation-duration</kbd> — jak długo trwa jeden cykl?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Ustaw <kbd>animation-delay: 1s</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>animation-delay</kbd> — ile czeka przed startem?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Ustaw <kbd>animation-iteration-count: 2</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>animation-iteration-count</kbd> — ile razy ma się powtórzyć?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Ustaw <kbd>animation-fill-mode: forwards</kbd>"
|
||||
"message": "Która wartość <kbd>animation-fill-mode</kbd> zachowuje końcowy stan animacji?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Ustaw <kbd>background: lightcoral</kbd>",
|
||||
"message": "Sprawdź właściwość <kbd>background</kbd> — jaki kolor potrzebuje panel na małych ekranach?",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -53,7 +53,11 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Ustaw <kbd>font-size: 5vw</kbd>" }
|
||||
{
|
||||
"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?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -73,7 +77,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Ustaw <kbd>display: grid</kbd>"
|
||||
"message": "Która wartość <kbd>display</kbd> włącza układ siatkowy?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
@@ -84,7 +88,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Ustaw <kbd>gap: 1rem</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>gap</kbd> — jaki odstęp potrzebują elementy siatki?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -117,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Ustaw <kbd>width: 250px</kbd>",
|
||||
"message": "Sprawdź właściwość <kbd>width</kbd> — jaką stałą szerokość potrzebuje sidebar?",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Ustaw <kbd>display: flex</kbd>"
|
||||
"message": "Która właściwość <kbd>display</kbd> tworzy kontener flex?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Ustaw <kbd>gap: 1rem</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>gap</kbd> — jaki odstęp potrzebują elementy?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Ustaw <kbd>justify-content: space-between</kbd>"
|
||||
"message": "Która wartość <kbd>justify-content</kbd> rozdziela elementy na końce?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Ustaw <kbd>align-items: center</kbd>"
|
||||
"message": "Która właściwość kontroluje wyrównanie na osi poprzecznej?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Ustaw <kbd>flex-wrap: wrap</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>flex-wrap</kbd> — jak pozwolić elementom przenosić się na nowe linie?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Ustaw <kbd>flex: 1</kbd>"
|
||||
"message": "Sprawdź właściwość <kbd>flex</kbd> — jak sprawić, by element wypełnił dostępną przestrzeń?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Додайте <kbd>color: coral;</kbd>"
|
||||
"message": "Яка властивість керує кольором тексту?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,12 +43,12 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lavender" },
|
||||
"message": "Додайте <kbd>background: lavender;</kbd>"
|
||||
"message": "Перевірте властивість <kbd>background</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Додайте <kbd>padding: 1rem;</kbd>"
|
||||
"message": "Картка потребує простору всередині її меж"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Встановіть <kbd>color: steelblue</kbd>"
|
||||
"message": "Яка властивість змінює колір тексту?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "coral" },
|
||||
"message": "Встановіть <kbd>color: coral</kbd>"
|
||||
"message": "Яке значення дає теплий червонувато-оранжевий колір?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "tomato" },
|
||||
"message": "Встановіть <kbd>background: tomato</kbd>"
|
||||
"message": "Значку потрібен яскравий червоний фон"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -152,7 +152,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "steelblue" },
|
||||
"message": "Встановіть <kbd>background: steelblue</kbd>"
|
||||
"message": "Яка властивість встановлює колір заливки кнопки?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "text-decoration", "expected": "none" },
|
||||
"message": "Встановіть <kbd>text-decoration: none</kbd>"
|
||||
"message": "Яка властивість керує підкресленням посилань?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "steelblue" },
|
||||
"message": "Встановіть <kbd>color: steelblue</kbd>"
|
||||
"message": "Перевірте властивість <kbd>color</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "color", "expected": "white" },
|
||||
"message": "Встановіть <kbd>color: white</kbd>"
|
||||
"message": "Посилання мають виділятися на синьому фоні"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "0.9rem" },
|
||||
"message": "Встановіть <kbd>font-size: 0.9rem</kbd>"
|
||||
"message": "Перевірте властивість <kbd>font-size</kbd> — текст має бути трохи меншим"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Встановіть <kbd>padding: 1rem</kbd>"
|
||||
"message": "Яка властивість додає простір між вмістом елемента та його межею?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Встановіть <kbd>border-left: 4px solid steelblue</kbd>",
|
||||
"message": "Використайте скорочення <kbd>border-left</kbd> зі значеннями ширини, стилю та кольору",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -65,7 +65,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Встановіть <kbd>margin-bottom: 1rem</kbd>"
|
||||
"message": "Яка властивість створює простір знизу елемента, відштовхуючи сусідів?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "box-sizing", "expected": "border-box" },
|
||||
"message": "Встановіть <kbd>box-sizing: border-box</kbd>"
|
||||
"message": "Яке значення <kbd>box-sizing</kbd> включає padding та межу в загальну ширину елемента?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Встановіть <kbd>padding: 8px 1rem</kbd>",
|
||||
"message": "Використайте скорочення <kbd>padding</kbd> з двома значеннями: вертикальне та горизонтальне",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -129,7 +129,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Встановіть <kbd>margin: 0 auto</kbd>",
|
||||
"message": "Використайте <kbd>margin</kbd> з ключовим словом, яке автоматично обчислює рівні ліві та праві відступи",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -151,7 +151,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Встановіть <kbd>border-radius: 50%</kbd>"
|
||||
"message": "Який відсоток <kbd>border-radius</kbd> створює ідеальне коло з квадратного елемента?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -172,18 +172,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Встановіть <kbd>padding: 1rem</kbd>"
|
||||
"message": "Додайте внутрішній відступ до картки сповіщення"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Встановіть <kbd>border-left: 4px solid coral</kbd>",
|
||||
"message": "Додайте лівий акцент за допомогою скорочення <kbd>border-left</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Встановіть <kbd>border-radius: 4px</kbd>"
|
||||
"message": "Злегка заокругліть кути за допомогою <kbd>border-radius</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Встановіть <kbd>max-width: 40rem</kbd>"
|
||||
"message": "Яка властивість обмежує ширину елемента? Спробуйте значення в <kbd>rem</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>width: calc(100% - 200px)</kbd>",
|
||||
"message": "Використайте <kbd>calc()</kbd>, щоб відняти фіксовану ширину сайдбару від повної ширини контейнера.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Встановіть <kbd>min-height: 100vh</kbd>"
|
||||
"message": "Яка властивість забезпечує мінімальну висоту? Використайте одиницю viewport для повноекранного покриття."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "transition:\\s*background-color\\s*0\\.3s",
|
||||
"message": "Встановіть <kbd>transition: background-color 0.3s</kbd>",
|
||||
"message": "Вкажіть, яку властивість анімувати та скільки це має тривати.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
|
||||
"message": "Встановіть timing на <kbd>ease-in-out</kbd>"
|
||||
"message": "Яке ключове слово пом'якшення починається повільно, прискорюється, а потім знову сповільнюється?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Застосуйте <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"message": "Використайте скорочення <kbd>animation</kbd>: назва, тривалість та кількість повторень.",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Встановіть <kbd>animation-name: pulse</kbd>"
|
||||
"message": "Яка властивість пов'язує елемент з іменованим правилом <kbd>@keyframes</kbd>?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Встановіть <kbd>animation-duration: 2s</kbd>"
|
||||
"message": "Скільки має тривати один повний цикл анімації?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Встановіть <kbd>animation-delay: 1s</kbd>"
|
||||
"message": "Яка властивість змушує анімацію зачекати перед початком?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-iteration-count", "expected": "2" },
|
||||
"message": "Встановіть <kbd>animation-iteration-count: 2</kbd>"
|
||||
"message": "Яка властивість контролює кількість повторень анімації?"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Встановіть <kbd>animation-fill-mode: forwards</kbd>"
|
||||
"message": "Яка властивість зберігає стиль елемента в його фінальному стані keyframe після завершення анімації?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(max-width:\\s*600px\\)",
|
||||
"message": "Використайте <kbd>@media (max-width: 600px)</kbd>",
|
||||
"message": "Почніть з правила <kbd>@media</kbd> — яка умова націлюється на екрани шириною 600px або менше?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Встановіть <kbd>background: lightcoral</kbd>",
|
||||
"message": "Яка властивість змінює колір фону елемента?",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Встановіть <kbd>font-size: 5vw</kbd>"
|
||||
"message": "Яка одиниця CSS масштабується відносно ширини viewport?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -77,18 +77,18 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Встановіть <kbd>display: grid</kbd>"
|
||||
"message": "Який режим display дозволяє визначати рядки та колонки?"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
|
||||
"message": "Використайте <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
|
||||
"message": "Спробуйте <kbd>repeat()</kbd> з <kbd>auto-fit</kbd> та <kbd>minmax()</kbd> — які мінімальний та максимальний розміри створять гнучкі колонки?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Встановіть <kbd>gap: 1rem</kbd>"
|
||||
"message": "Яка властивість додає простір між елементами grid?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -109,7 +109,7 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "@media\\s*\\(min-width:\\s*768px\\)",
|
||||
"message": "Використайте <kbd>@media (min-width: 768px)</kbd>",
|
||||
"message": "Яка умова <kbd>@media</kbd> застосовує стилі, коли viewport має ширину щонайменше 768px?",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
@@ -121,7 +121,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Встановіть <kbd>width: 250px</kbd>",
|
||||
"message": "Яка властивість контролює ширину сайдбару на великих екранах?",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "flex" },
|
||||
"message": "Встановіть <kbd>display: flex</kbd>"
|
||||
"message": "Яке значення display перетворює елемент на гнучкий контейнер?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Встановіть <kbd>gap: 1rem</kbd>"
|
||||
"message": "Яка властивість створює відстань між flex-елементами без використання margin?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "justify-content", "expected": "space-between" },
|
||||
"message": "Встановіть <kbd>justify-content: space-between</kbd>"
|
||||
"message": "Яке значення <kbd>justify-content</kbd> розміщує перший та останній елементи на протилежних краях?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "align-items", "expected": "center" },
|
||||
"message": "Встановіть <kbd>align-items: center</kbd>"
|
||||
"message": "Яка властивість вирівнює flex-елементи вздовж поперечної осі?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -106,7 +106,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex-wrap", "expected": "wrap" },
|
||||
"message": "Встановіть <kbd>flex-wrap: wrap</kbd>"
|
||||
"message": "Яка властивість дозволяє flex-елементам переходити на кілька рядків?"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "flex", "expected": "1" },
|
||||
"message": "Встановіть <kbd>flex: 1</kbd>"
|
||||
"message": "Яка властивість змушує flex-елемент зростати, щоб заповнити залишковий простір?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
87
specs/004-pedagogical-messages/plan.md
Normal file
87
specs/004-pedagogical-messages/plan.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
50
specs/004-pedagogical-messages/spec.md
Normal file
50
specs/004-pedagogical-messages/spec.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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
|
||||
39
specs/004-pedagogical-messages/tasks.md
Normal file
39
specs/004-pedagogical-messages/tasks.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 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
|
||||
@@ -13,7 +13,7 @@ import { abbreviationTracker, expandAbbreviation } from "@emmetio/codemirror6-pl
|
||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
// Custom theme with purple accent colors (matching app completed state)
|
||||
// Custom theme with pink accent colors (matching app completed state)
|
||||
const crispyTheme = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
@@ -21,10 +21,10 @@ const crispyTheme = EditorView.theme(
|
||||
color: "#c8c8d0"
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: "#9b6dd4"
|
||||
caretColor: "#d46d9b"
|
||||
},
|
||||
".cm-cursor, .cm-dropCursor": {
|
||||
borderLeftColor: "#9b6dd4"
|
||||
borderLeftColor: "#d46d9b"
|
||||
},
|
||||
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: "#3e3e4a"
|
||||
@@ -35,10 +35,10 @@ const crispyTheme = EditorView.theme(
|
||||
},
|
||||
".cm-searchMatch": {
|
||||
backgroundColor: "#3e3e4a",
|
||||
outline: "1px solid #9b6dd4"
|
||||
outline: "1px solid #d46d9b"
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: "rgba(155, 109, 212, 0.3)"
|
||||
backgroundColor: "rgba(212, 109, 155, 0.3)"
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: "#2e2e3a"
|
||||
@@ -63,13 +63,13 @@ const crispyTheme = EditorView.theme(
|
||||
|
||||
// Default syntax highlighting (blue accent)
|
||||
const defaultHighlight = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: "#c9a6eb" },
|
||||
{ tag: tags.keyword, color: "#eba6c9" },
|
||||
{ tag: tags.operator, color: "#cdd6f4" },
|
||||
{ tag: tags.variableName, color: "#89b4fa" },
|
||||
{ tag: tags.propertyName, color: "#89b4fa" },
|
||||
{ tag: tags.attributeName, color: "#89b4fa" },
|
||||
{ tag: tags.className, color: "#89b4fa" },
|
||||
{ tag: tags.tagName, color: "#c9a6eb" },
|
||||
{ tag: tags.tagName, color: "#eba6c9" },
|
||||
{ tag: tags.string, color: "#a6e3a1" },
|
||||
{ tag: tags.number, color: "#fab387" },
|
||||
{ tag: tags.bool, color: "#fab387" },
|
||||
@@ -79,20 +79,20 @@ const defaultHighlight = HighlightStyle.define([
|
||||
{ tag: tags.punctuation, color: "#cdd6f4" },
|
||||
{ tag: tags.definition(tags.variableName), color: "#89b4fa" },
|
||||
{ tag: tags.function(tags.variableName), color: "#89b4fa" },
|
||||
{ tag: tags.atom, color: "#c9a6eb" },
|
||||
{ tag: tags.atom, color: "#eba6c9" },
|
||||
{ tag: tags.unit, color: "#a6e3a1" },
|
||||
{ tag: tags.color, color: "#f9e2af" }
|
||||
]);
|
||||
|
||||
// CSS section highlighting (purple selectors)
|
||||
// CSS section highlighting (pink selectors)
|
||||
const cssHighlight = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: "#c9a6eb" },
|
||||
{ tag: tags.keyword, color: "#eba6c9" },
|
||||
{ tag: tags.operator, color: "#cdd6f4" },
|
||||
{ tag: tags.variableName, color: "#c9a6eb" },
|
||||
{ tag: tags.variableName, color: "#eba6c9" },
|
||||
{ tag: tags.propertyName, color: "#89b4fa" },
|
||||
{ tag: tags.attributeName, color: "#89b4fa" },
|
||||
{ tag: tags.className, color: "#c9a6eb" },
|
||||
{ tag: tags.tagName, color: "#c9a6eb" },
|
||||
{ tag: tags.className, color: "#eba6c9" },
|
||||
{ tag: tags.tagName, color: "#eba6c9" },
|
||||
{ tag: tags.string, color: "#a6e3a1" },
|
||||
{ tag: tags.number, color: "#fab387" },
|
||||
{ tag: tags.bool, color: "#fab387" },
|
||||
@@ -100,9 +100,9 @@ const cssHighlight = HighlightStyle.define([
|
||||
{ tag: tags.comment, color: "#6c7086", fontStyle: "italic" },
|
||||
{ tag: tags.bracket, color: "#cdd6f4" },
|
||||
{ tag: tags.punctuation, color: "#cdd6f4" },
|
||||
{ tag: tags.definition(tags.variableName), color: "#c9a6eb" },
|
||||
{ tag: tags.definition(tags.variableName), color: "#eba6c9" },
|
||||
{ tag: tags.function(tags.variableName), color: "#89b4fa" },
|
||||
{ tag: tags.atom, color: "#c9a6eb" },
|
||||
{ tag: tags.atom, color: "#eba6c9" },
|
||||
{ tag: tags.unit, color: "#a6e3a1" },
|
||||
{ tag: tags.color, color: "#f9e2af" }
|
||||
]);
|
||||
|
||||
@@ -216,18 +216,18 @@ export class LessonEngine {
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.border = "none";
|
||||
iframe.title = "Preview";
|
||||
iframe.setAttribute("sandbox", "allow-scripts");
|
||||
|
||||
const container = document.getElementById(previewContainer || "preview-area");
|
||||
container.innerHTML = "";
|
||||
container.appendChild(iframe);
|
||||
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
iframeDoc.open();
|
||||
let html;
|
||||
|
||||
if (mode === "html" || mode === "playground") {
|
||||
// For HTML/playground mode, user code IS the HTML content (may include <style> blocks)
|
||||
const userHtml = this.userCode || "";
|
||||
iframeDoc.write(`
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -239,11 +239,11 @@ export class LessonEngine {
|
||||
${userHtml}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
`;
|
||||
} else if (mode === "tailwind") {
|
||||
// For Tailwind mode, user code goes directly in HTML classes
|
||||
const htmlWithClasses = this.injectTailwindClasses(previewHTML, this.userCode);
|
||||
iframeDoc.write(`
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -256,11 +256,11 @@ export class LessonEngine {
|
||||
${htmlWithClasses}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
`;
|
||||
} else if (mode === "markdown") {
|
||||
// For Markdown mode, parse user code to HTML
|
||||
const renderedHtml = marked.parse(this.userCode || "");
|
||||
iframeDoc.write(`
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -288,11 +288,11 @@ export class LessonEngine {
|
||||
${renderedHtml}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
`;
|
||||
} else {
|
||||
// Original CSS mode
|
||||
const userCssWithWrapper = this.getCompleteCss();
|
||||
iframeDoc.write(`
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -305,10 +305,10 @@ export class LessonEngine {
|
||||
${previewHTML}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
`;
|
||||
}
|
||||
|
||||
iframeDoc.close();
|
||||
iframe.srcdoc = html;
|
||||
}
|
||||
|
||||
injectTailwindClasses(html, userClasses) {
|
||||
@@ -341,6 +341,7 @@ export class LessonEngine {
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.border = "none";
|
||||
iframe.title = "Expected Result";
|
||||
iframe.setAttribute("sandbox", "allow-scripts");
|
||||
|
||||
const container = document.getElementById("preview-expected");
|
||||
if (!container) return;
|
||||
@@ -348,12 +349,11 @@ export class LessonEngine {
|
||||
container.innerHTML = "";
|
||||
container.appendChild(iframe);
|
||||
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
iframeDoc.open();
|
||||
let html;
|
||||
|
||||
if (mode === "html" || mode === "playground") {
|
||||
// For HTML/playground mode, solution code IS the HTML content
|
||||
iframeDoc.write(`
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -365,11 +365,11 @@ export class LessonEngine {
|
||||
${solutionCode}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
`;
|
||||
} else if (mode === "tailwind") {
|
||||
// For Tailwind mode, inject solution classes into HTML
|
||||
const htmlWithClasses = this.injectTailwindClasses(previewHTML, solutionCode);
|
||||
iframeDoc.write(`
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -382,11 +382,11 @@ export class LessonEngine {
|
||||
${htmlWithClasses}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
`;
|
||||
} else if (mode === "markdown") {
|
||||
// For Markdown mode, parse solution to HTML
|
||||
const renderedHtml = marked.parse(solutionCode || "");
|
||||
iframeDoc.write(`
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -414,12 +414,12 @@ export class LessonEngine {
|
||||
${renderedHtml}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
`;
|
||||
} else {
|
||||
// CSS mode - wrap solution with prefix/suffix
|
||||
const { codePrefix, codeSuffix } = this.currentLesson;
|
||||
const solutionCss = `${codePrefix || ""}${solutionCode}${codeSuffix || ""}`;
|
||||
iframeDoc.write(`
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -432,10 +432,10 @@ export class LessonEngine {
|
||||
${previewHTML}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
`;
|
||||
}
|
||||
|
||||
iframeDoc.close();
|
||||
iframe.srcdoc = html;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://librete.ch https://liberapay.com; style-src 'self' 'unsafe-inline'; connect-src 'self' https://*.supabase.co wss://*.supabase.co; img-src 'self' https://liberapay.com data:; font-src 'self'; frame-src 'self' blob:" />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>CODE CRISPIES - Learn HTML & CSS Interactively | Free Coding Practice</title>
|
||||
|
||||
98
src/main.css
98
src/main.css
@@ -1,15 +1,15 @@
|
||||
/* ================= BASE THEME ================= */
|
||||
:root {
|
||||
/* Primary colors */
|
||||
--primary-color: #5e4b8b;
|
||||
--primary-light: #8a77b5;
|
||||
--primary-dark: #724a95;
|
||||
--primary-color: #c9507a;
|
||||
--primary-light: #e077a0;
|
||||
--primary-dark: #a83d65;
|
||||
|
||||
/* Section colors (default to CSS purple) */
|
||||
--section-color: #9163b8;
|
||||
--section-color-light: #a87dc8;
|
||||
--section-color-dark: #724a95;
|
||||
--section-color-rgb: 145, 99, 184;
|
||||
/* Section colors (default to CSS pink) */
|
||||
--section-color: #d95a8a;
|
||||
--section-color-light: #e87da6;
|
||||
--section-color-dark: #b84472;
|
||||
--section-color-rgb: 217, 90, 138;
|
||||
|
||||
/* Secondary colors */
|
||||
--secondary-color: #444444;
|
||||
@@ -23,9 +23,9 @@
|
||||
--white-text: #ffffff;
|
||||
|
||||
/* Background colors */
|
||||
--bg-color: #f8f7fc;
|
||||
--bg-color: #fcf7f9;
|
||||
--panel-bg: #ffffff;
|
||||
--code-bg: #f7f5fa;
|
||||
--code-bg: #faf5f7;
|
||||
--editor-bg: #1e1e1e;
|
||||
--editor-highlight: #303030;
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
|
||||
/* Status colors */
|
||||
--info-color: #7a93fe;
|
||||
--success-color: #9b6dd4;
|
||||
--success-color-dark: #7c4dff;
|
||||
--success-color-light: #c9b8e8;
|
||||
--success-color: #d46d9b;
|
||||
--success-color-dark: #b84472;
|
||||
--success-color-light: #e8b8d0;
|
||||
--error-color: #cb6e75;
|
||||
--danger-color: #dc3545;
|
||||
|
||||
@@ -252,11 +252,11 @@ kbd {
|
||||
}
|
||||
|
||||
.logo h1 .code-text {
|
||||
color: #9163b8;
|
||||
color: #d95a8a;
|
||||
}
|
||||
|
||||
.logo h1 .crispies-text {
|
||||
background: #9163b8;
|
||||
background: #d95a8a;
|
||||
color: white;
|
||||
padding: 0.15rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
@@ -468,7 +468,7 @@ kbd {
|
||||
.completion-badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: linear-gradient(135deg, #9163b8, #d45aa0, #1aafb8, #7c4dff);
|
||||
background: linear-gradient(135deg, #d95a8a, #d45aa0, #1aafb8, #ff4d88);
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
@@ -714,7 +714,7 @@ kbd {
|
||||
position: absolute;
|
||||
inset: var(--spacing-md);
|
||||
border-radius: var(--border-radius-md);
|
||||
background: conic-gradient(from var(--border-angle), #9163b8, #d45aa0, #1aafb8, #7c4dff, #9163b8);
|
||||
background: conic-gradient(from var(--border-angle), #d95a8a, #d45aa0, #1aafb8, #ff4d88, #d95a8a);
|
||||
filter: blur(30px);
|
||||
opacity: 0;
|
||||
animation: spin-glow 3s ease-out forwards;
|
||||
@@ -727,7 +727,7 @@ kbd {
|
||||
position: absolute;
|
||||
inset: var(--spacing-md);
|
||||
border-radius: var(--border-radius-md);
|
||||
background: conic-gradient(from 0deg, #9163b8, #d45aa0, #1aafb8, #7c4dff, #9163b8);
|
||||
background: conic-gradient(from 0deg, #d95a8a, #d45aa0, #1aafb8, #ff4d88, #d95a8a);
|
||||
filter: blur(30px);
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
@@ -816,7 +816,7 @@ kbd {
|
||||
border: 6px solid transparent;
|
||||
background:
|
||||
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
|
||||
conic-gradient(from 0deg, #9163b8, #d45aa0, #1aafb8, #7c4dff, #9163b8) border-box;
|
||||
conic-gradient(from 0deg, #d95a8a, #d45aa0, #1aafb8, #ff4d88, #d95a8a) border-box;
|
||||
}
|
||||
|
||||
.preview-wrapper.matched {
|
||||
@@ -824,7 +824,7 @@ kbd {
|
||||
border: 6px solid transparent;
|
||||
background:
|
||||
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
|
||||
conic-gradient(from var(--border-angle), #9163b8, #d45aa0, #1aafb8, #7c4dff, #9163b8) border-box;
|
||||
conic-gradient(from var(--border-angle), #d95a8a, #d45aa0, #1aafb8, #ff4d88, #d95a8a) border-box;
|
||||
animation: spin-border 3s ease-out forwards;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -844,7 +844,7 @@ kbd {
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #9163b8 0%, #d45aa0 50%, #7c4dff 100%);
|
||||
background: linear-gradient(135deg, #d95a8a 0%, #d45aa0 50%, #ff4d88 100%);
|
||||
padding: 1.25rem 2rem 1.75rem;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
@@ -1142,7 +1142,7 @@ nav.sidebar-section:not(.sidebar-nav-mobile) {
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #9163b8, #d45aa0, #1aafb8, #7c4dff);
|
||||
background: linear-gradient(90deg, #d95a8a, #d45aa0, #1aafb8, #ff4d88);
|
||||
background-size: calc(100% * 100 / var(--progress-percent, 100)) 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
@@ -1206,7 +1206,7 @@ nav.sidebar-section:not(.sidebar-nav-mobile) {
|
||||
}
|
||||
|
||||
/* Each milestone gets a color evenly distributed across the gradient
|
||||
Gradient: #9163b8 (0%) → #d45aa0 (33%) → #1aafb8 (67%) → #7c4dff (100%) */
|
||||
Gradient: #d95a8a (0%) → #d45aa0 (33%) → #1aafb8 (67%) → #ff4d88 (100%) */
|
||||
.milestone.reached:nth-child(1) { background: #a55eac; } /* ~14% */
|
||||
.milestone.reached:nth-child(2) { background: #c459a2; } /* ~28% */
|
||||
.milestone.reached:nth-child(3) { background: #d45aa0; } /* ~33% pink */
|
||||
@@ -1214,12 +1214,12 @@ nav.sidebar-section:not(.sidebar-nav-mobile) {
|
||||
.milestone.reached:nth-child(5) { background: #7785ac; } /* ~50% */
|
||||
.milestone.reached:nth-child(6) { background: #33a3b6; } /* ~62% */
|
||||
.milestone.reached:nth-child(7) { background: #4889d8; } /* ~80% */
|
||||
.milestone.reached:nth-child(8) { background: #7c4dff; } /* 100% */
|
||||
.milestone.reached:nth-child(8) { background: #ff4d88; } /* 100% */
|
||||
|
||||
.milestone.current {
|
||||
color: white;
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 2px 8px rgba(145, 99, 184, 0.4);
|
||||
box-shadow: 0 2px 8px rgba(217, 90, 138, 0.4);
|
||||
}
|
||||
|
||||
.milestone.next {
|
||||
@@ -2590,7 +2590,7 @@ input:checked + .toggle-slider::before {
|
||||
margin-top: var(--spacing-lg);
|
||||
text-align: center;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(145, 99, 184, 0.1), rgba(212, 90, 160, 0.1), rgba(26, 175, 184, 0.1));
|
||||
background: linear-gradient(135deg, rgba(217, 90, 138, 0.1), rgba(212, 90, 160, 0.1), rgba(26, 175, 184, 0.1));
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--light-text);
|
||||
font-size: 0.9rem;
|
||||
@@ -2840,7 +2840,7 @@ input:checked + .toggle-slider::before {
|
||||
}
|
||||
|
||||
.section-overview code {
|
||||
background: rgba(var(--section-color-rgb, 145, 99, 184), 0.1);
|
||||
background: rgba(var(--section-color-rgb, 217, 90, 138), 0.1);
|
||||
color: var(--section-color-dark, var(--primary-dark));
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
@@ -2950,7 +2950,7 @@ input:checked + .toggle-slider::before {
|
||||
|
||||
/* Inline code in topic text */
|
||||
.topic-text code {
|
||||
background: rgba(var(--section-color-rgb, 145, 99, 184), 0.1);
|
||||
background: rgba(var(--section-color-rgb, 217, 90, 138), 0.1);
|
||||
color: var(--section-color-dark, var(--primary-dark));
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
@@ -3592,7 +3592,7 @@ input:checked + .toggle-slider::before {
|
||||
}
|
||||
|
||||
/* ================= SECTION COLOR CODING ================= */
|
||||
/* CSS Section uses default purple from :root */
|
||||
/* CSS Section uses default pink from :root */
|
||||
|
||||
/* HTML Section - Pink (balanced) */
|
||||
[data-section="html"] {
|
||||
@@ -3620,7 +3620,7 @@ input:checked + .toggle-slider::before {
|
||||
|
||||
/* Apply section colors to nav links */
|
||||
.nav-link[data-section="css"] {
|
||||
color: #9163b8;
|
||||
color: #d95a8a;
|
||||
}
|
||||
|
||||
.nav-link[data-section="html"] {
|
||||
@@ -3637,8 +3637,8 @@ input:checked + .toggle-slider::before {
|
||||
|
||||
.nav-link[data-section="css"]:hover,
|
||||
.nav-link[data-section="css"].active {
|
||||
background: rgba(145, 99, 184, 0.1);
|
||||
color: #724a95;
|
||||
background: rgba(217, 90, 138, 0.1);
|
||||
color: #a83d65;
|
||||
}
|
||||
|
||||
.nav-link[data-section="html"]:hover,
|
||||
@@ -3661,12 +3661,12 @@ input:checked + .toggle-slider::before {
|
||||
|
||||
/* Hint section colors */
|
||||
body[data-section="css"] .hint {
|
||||
background: rgba(145, 99, 184, 0.3);
|
||||
background: rgba(217, 90, 138, 0.3);
|
||||
border-left-color: #a98cd6;
|
||||
}
|
||||
|
||||
body[data-section="css"] .hint-progress {
|
||||
background: #9163b8;
|
||||
background: #d95a8a;
|
||||
}
|
||||
|
||||
body[data-section="html"] .hint {
|
||||
@@ -3718,7 +3718,7 @@ body[data-section="markdown"] .hint-progress {
|
||||
.ref-nav-link[data-ref="selectors"],
|
||||
.ref-nav-link[data-ref="flexbox"],
|
||||
.ref-nav-link[data-ref="grid"] {
|
||||
color: #9163b8;
|
||||
color: #d95a8a;
|
||||
}
|
||||
|
||||
.ref-nav-link[data-ref="css"]:hover,
|
||||
@@ -3729,8 +3729,8 @@ body[data-section="markdown"] .hint-progress {
|
||||
.ref-nav-link[data-ref="flexbox"].active,
|
||||
.ref-nav-link[data-ref="grid"]:hover,
|
||||
.ref-nav-link[data-ref="grid"].active {
|
||||
background: rgba(145, 99, 184, 0.15);
|
||||
color: #724a95;
|
||||
background: rgba(217, 90, 138, 0.15);
|
||||
color: #a83d65;
|
||||
}
|
||||
|
||||
.ref-nav-link[data-ref="html"] {
|
||||
@@ -3745,21 +3745,21 @@ body[data-section="markdown"] .hint-progress {
|
||||
|
||||
/* CodeMirror section color overrides */
|
||||
body[data-section="css"] .cm-editor .cm-content {
|
||||
caret-color: #9163b8 !important;
|
||||
caret-color: #d95a8a !important;
|
||||
}
|
||||
|
||||
body[data-section="css"] .cm-editor .cm-cursor,
|
||||
body[data-section="css"] .cm-editor .cm-dropCursor {
|
||||
border-left-color: #9163b8 !important;
|
||||
border-left-color: #d95a8a !important;
|
||||
}
|
||||
|
||||
body[data-section="css"] .cm-editor .cm-selectionBackground,
|
||||
body[data-section="css"] .cm-editor .cm-content ::selection {
|
||||
background-color: rgba(145, 99, 184, 0.25) !important;
|
||||
background-color: rgba(217, 90, 138, 0.25) !important;
|
||||
}
|
||||
|
||||
body[data-section="css"] .cm-editor .cm-activeLine {
|
||||
background-color: rgba(145, 99, 184, 0.08) !important;
|
||||
background-color: rgba(217, 90, 138, 0.08) !important;
|
||||
}
|
||||
|
||||
body[data-section="html"] .cm-editor .cm-content {
|
||||
@@ -3818,12 +3818,12 @@ body[data-section="markdown"] .cm-editor .cm-activeLine {
|
||||
|
||||
/* Module pill section colors */
|
||||
body[data-section="css"] .module-pill {
|
||||
background: rgba(145, 99, 184, 0.1);
|
||||
color: #9163b8;
|
||||
background: rgba(217, 90, 138, 0.1);
|
||||
color: #d95a8a;
|
||||
}
|
||||
|
||||
body[data-section="css"] .module-pill .level-indicator {
|
||||
color: #724a95;
|
||||
color: #a83d65;
|
||||
}
|
||||
|
||||
body[data-section="html"] .module-pill {
|
||||
@@ -3855,7 +3855,7 @@ body[data-section="markdown"] .module-pill .level-indicator {
|
||||
|
||||
/* Code block border section colors */
|
||||
body[data-section="css"] .code-block {
|
||||
border-color: rgba(145, 99, 184, 0.4);
|
||||
border-color: rgba(217, 90, 138, 0.4);
|
||||
}
|
||||
|
||||
body[data-section="html"] .code-block {
|
||||
@@ -3889,7 +3889,7 @@ body[data-section="markdown"] .code-block .cm-editor .cm-line {
|
||||
|
||||
/* Task instruction bubble section colors */
|
||||
[data-section="css"] .task-instruction {
|
||||
background: rgba(145, 99, 184, 0.92);
|
||||
background: rgba(217, 90, 138, 0.92);
|
||||
}
|
||||
|
||||
[data-section="html"] .task-instruction {
|
||||
@@ -3906,7 +3906,7 @@ body[data-section="markdown"] .code-block .cm-editor .cm-line {
|
||||
|
||||
/* Section page progress bar colors */
|
||||
body[data-section="css"] .section-progress-bar .progress-fill {
|
||||
background: #9163b8;
|
||||
background: #d95a8a;
|
||||
}
|
||||
|
||||
body[data-section="html"] .section-progress-bar .progress-fill {
|
||||
@@ -3923,7 +3923,7 @@ body[data-section="markdown"] .section-progress-bar .progress-fill {
|
||||
|
||||
/* Section page header colors */
|
||||
[data-section="css"] .section-hero h1 {
|
||||
color: #9163b8;
|
||||
color: #d95a8a;
|
||||
}
|
||||
|
||||
[data-section="html"] .section-hero h1 {
|
||||
@@ -3940,7 +3940,7 @@ body[data-section="markdown"] .section-progress-bar .progress-fill {
|
||||
|
||||
/* Lesson title h2 section colors */
|
||||
body[data-section="css"] #lesson-title {
|
||||
color: #9163b8;
|
||||
color: #d95a8a;
|
||||
}
|
||||
|
||||
body[data-section="html"] #lesson-title {
|
||||
|
||||
538
tests/unit/renderer-extended.test.js
Normal file
538
tests/unit/renderer-extended.test.js
Normal file
@@ -0,0 +1,538 @@
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
renderModuleList,
|
||||
renderLesson,
|
||||
renderLevelIndicator,
|
||||
renderDifficultyBadge,
|
||||
showFeedback,
|
||||
clearFeedback,
|
||||
updateActiveLessonInSidebar,
|
||||
computeLessonDifficulty
|
||||
} from "../../src/helpers/renderer.js";
|
||||
|
||||
// Mock i18n
|
||||
vi.mock("../../src/i18n.js", () => ({
|
||||
t: (key, params = {}) => {
|
||||
const translations = {
|
||||
lessonLabel: "Lesson",
|
||||
untitledLesson: "Untitled Lesson",
|
||||
lessonFallback: `Lesson ${params.index || ""}`,
|
||||
difficulty_easy_label: "Easy difficulty",
|
||||
difficulty_medium_label: "Medium difficulty",
|
||||
difficulty_hard_label: "Hard difficulty",
|
||||
difficulty_easy: "Easy",
|
||||
difficulty_medium: "Medium",
|
||||
difficulty_hard: "Hard"
|
||||
};
|
||||
return translations[key] || key;
|
||||
}
|
||||
}));
|
||||
|
||||
describe("Renderer Extended Coverage", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div id="module-list"></div>
|
||||
<div class="lesson-title-row">
|
||||
<h2 id="title"></h2>
|
||||
</div>
|
||||
<div id="description"></div>
|
||||
<div id="task"></div>
|
||||
<div id="preview"></div>
|
||||
<div id="prefix"></div>
|
||||
<textarea id="input"></textarea>
|
||||
<div id="suffix"></div>
|
||||
<div id="level-indicator"></div>
|
||||
<div class="editor-content"></div>
|
||||
<input type="checkbox" id="disable-feedback-toggle" checked>
|
||||
`;
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("renderModuleList - progress tracking", () => {
|
||||
test("renderModuleList_CorruptedProgress_HandlesGracefully", () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
localStorage.setItem("codeCrispies.progress", "not-valid-json{{{");
|
||||
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "mod1", title: "Module 1", lessons: [{ title: "L1" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Error parsing progress"), expect.anything());
|
||||
// Should still render modules despite parse error
|
||||
expect(container.querySelectorAll(".module-header").length).toBe(1);
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("renderModuleList_CompletedModule_AddedCompletedClass", () => {
|
||||
localStorage.setItem(
|
||||
"codeCrispies.progress",
|
||||
JSON.stringify({
|
||||
mod1: { completed: [0, 1], current: 1 }
|
||||
})
|
||||
);
|
||||
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "mod1", title: "Module 1", lessons: [{ title: "L1" }, { title: "L2" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const header = container.querySelector(".module-header");
|
||||
expect(header.classList.contains("completed")).toBe(true);
|
||||
});
|
||||
|
||||
test("renderModuleList_PartiallyCompleted_NoCompletedClass", () => {
|
||||
localStorage.setItem(
|
||||
"codeCrispies.progress",
|
||||
JSON.stringify({
|
||||
mod1: { completed: [0], current: 1 }
|
||||
})
|
||||
);
|
||||
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "mod1", title: "Module 1", lessons: [{ title: "L1" }, { title: "L2" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const header = container.querySelector(".module-header");
|
||||
expect(header.classList.contains("completed")).toBe(false);
|
||||
});
|
||||
|
||||
test("renderModuleList_CompletedLesson_HasCompletedClass", () => {
|
||||
localStorage.setItem(
|
||||
"codeCrispies.progress",
|
||||
JSON.stringify({
|
||||
mod1: { completed: [0], current: 1 }
|
||||
})
|
||||
);
|
||||
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "mod1", title: "Module 1", lessons: [{ title: "L1" }, { title: "L2" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const lessonItems = container.querySelectorAll(".lesson-list-item");
|
||||
expect(lessonItems[0].classList.contains("completed")).toBe(true);
|
||||
expect(lessonItems[1].classList.contains("completed")).toBe(false);
|
||||
});
|
||||
|
||||
test("renderModuleList_CurrentLesson_HasCurrentClass", () => {
|
||||
localStorage.setItem(
|
||||
"codeCrispies.progress",
|
||||
JSON.stringify({
|
||||
mod1: { completed: [0], current: 1 }
|
||||
})
|
||||
);
|
||||
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "mod1", title: "Module 1", lessons: [{ title: "L1" }, { title: "L2" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const lessonItems = container.querySelectorAll(".lesson-list-item");
|
||||
expect(lessonItems[1].classList.contains("current")).toBe(true);
|
||||
expect(lessonItems[0].classList.contains("current")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderModuleList - welcome/playground always expanded", () => {
|
||||
test("renderModuleList_WelcomeModule_AlwaysExpanded", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "welcome", title: "Welcome", lessons: [{ title: "Intro" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const details = container.querySelector("details.module-container");
|
||||
expect(details.open).toBe(true);
|
||||
});
|
||||
|
||||
test("renderModuleList_PlaygroundModule_AlwaysExpanded", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "playground", title: "Playground", lessons: [{ title: "Play" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const details = container.querySelector("details.module-container");
|
||||
expect(details.open).toBe(true);
|
||||
});
|
||||
|
||||
test("renderModuleList_RegularModule_CollapsedByDefault", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "flexbox", title: "Flexbox", lessons: [{ title: "L1" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const details = container.querySelector("details.module-container");
|
||||
expect(details.open).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderModuleList - lesson fallback title", () => {
|
||||
test("renderModuleList_NoLessonTitle_UsesFallback", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "mod1", title: "Module 1", lessons: [{}] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const lessonItem = container.querySelector(".lesson-list-item");
|
||||
expect(lessonItem.textContent).toContain("Lesson");
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderModuleList - click behavior", () => {
|
||||
test("renderModuleList_LessonClick_RemovesActiveFromOthers", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [
|
||||
{
|
||||
id: "mod1",
|
||||
title: "Module 1",
|
||||
lessons: [{ title: "L1" }, { title: "L2" }]
|
||||
}
|
||||
];
|
||||
const onSelectLesson = vi.fn();
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), onSelectLesson);
|
||||
|
||||
const lessonItems = container.querySelectorAll(".lesson-list-item");
|
||||
|
||||
// Click first lesson
|
||||
lessonItems[0].click();
|
||||
expect(lessonItems[0].classList.contains("active")).toBe(true);
|
||||
expect(onSelectLesson).toHaveBeenCalledWith("mod1", 0);
|
||||
|
||||
// Click second lesson
|
||||
lessonItems[1].click();
|
||||
expect(lessonItems[0].classList.contains("active")).toBe(false);
|
||||
expect(lessonItems[1].classList.contains("active")).toBe(true);
|
||||
expect(onSelectLesson).toHaveBeenCalledWith("mod1", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderModuleList - module dataset", () => {
|
||||
test("renderModuleList_DataAttributes_SetCorrectly", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "flex-mod", title: "Flex Module", lessons: [{ title: "L1" }] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
const details = container.querySelector("details.module-container");
|
||||
expect(details.dataset.moduleId).toBe("flex-mod");
|
||||
|
||||
const header = container.querySelector(".module-header");
|
||||
expect(header.dataset.moduleId).toBe("flex-mod");
|
||||
|
||||
const lesson = container.querySelector(".lesson-list-item");
|
||||
expect(lesson.dataset.moduleId).toBe("flex-mod");
|
||||
expect(lesson.dataset.lessonIndex).toBe("0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderModuleList - empty lessons", () => {
|
||||
test("renderModuleList_EmptyLessonsArray_RendersModuleOnly", () => {
|
||||
const container = document.getElementById("module-list");
|
||||
const modules = [{ id: "mod1", title: "Module 1", lessons: [] }];
|
||||
|
||||
renderModuleList(container, modules, vi.fn(), vi.fn());
|
||||
|
||||
expect(container.querySelectorAll(".module-header").length).toBe(1);
|
||||
expect(container.querySelectorAll(".lesson-list-item").length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderDifficultyBadge", () => {
|
||||
test("renderDifficultyBadge_EasyLesson_CreatesEasyBadge", () => {
|
||||
const container = document.querySelector(".lesson-title-row");
|
||||
const lesson = { codePrefix: ".box {\n ", solution: "color: red;" };
|
||||
|
||||
renderDifficultyBadge(container, lesson);
|
||||
|
||||
const badge = container.querySelector(".difficulty-badge");
|
||||
expect(badge).not.toBeNull();
|
||||
expect(badge.classList.contains("difficulty-easy")).toBe(true);
|
||||
expect(badge.querySelectorAll(".bar").length).toBe(3);
|
||||
});
|
||||
|
||||
test("renderDifficultyBadge_MediumLesson_CreatesMediumBadge", () => {
|
||||
const container = document.querySelector(".lesson-title-row");
|
||||
const lesson = { codePrefix: "", solution: "p {\n color: red;\n}" };
|
||||
|
||||
renderDifficultyBadge(container, lesson);
|
||||
|
||||
const badge = container.querySelector(".difficulty-badge");
|
||||
expect(badge.classList.contains("difficulty-medium")).toBe(true);
|
||||
});
|
||||
|
||||
test("renderDifficultyBadge_HardLesson_CreatesHardBadge", () => {
|
||||
const container = document.querySelector(".lesson-title-row");
|
||||
const lesson = { codePrefix: "", solution: ".nav a {\n color: white;\n}" };
|
||||
|
||||
renderDifficultyBadge(container, lesson);
|
||||
|
||||
const badge = container.querySelector(".difficulty-badge");
|
||||
expect(badge.classList.contains("difficulty-hard")).toBe(true);
|
||||
});
|
||||
|
||||
test("renderDifficultyBadge_CalledTwice_RemovesPreviousBadge", () => {
|
||||
const container = document.querySelector(".lesson-title-row");
|
||||
const lesson1 = { codePrefix: ".box {\n ", solution: "color: red;" };
|
||||
const lesson2 = { codePrefix: "", solution: ".nav a {\n color: white;\n}" };
|
||||
|
||||
renderDifficultyBadge(container, lesson1);
|
||||
expect(container.querySelectorAll(".difficulty-wrapper").length).toBe(1);
|
||||
|
||||
renderDifficultyBadge(container, lesson2);
|
||||
expect(container.querySelectorAll(".difficulty-wrapper").length).toBe(1);
|
||||
|
||||
const badge = container.querySelector(".difficulty-badge");
|
||||
expect(badge.classList.contains("difficulty-hard")).toBe(true);
|
||||
});
|
||||
|
||||
test("renderDifficultyBadge_HasAriaLabel", () => {
|
||||
const container = document.querySelector(".lesson-title-row");
|
||||
const lesson = { codePrefix: ".box {", solution: "color: red;" };
|
||||
|
||||
renderDifficultyBadge(container, lesson);
|
||||
|
||||
const badge = container.querySelector(".difficulty-badge");
|
||||
expect(badge.getAttribute("aria-label")).toBeTruthy();
|
||||
expect(badge.getAttribute("title")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("showFeedback", () => {
|
||||
test("showFeedback_Success_CreatesSuccessElement", () => {
|
||||
showFeedback(true, "Great job!");
|
||||
|
||||
const feedback = document.querySelector(".feedback-success");
|
||||
expect(feedback).not.toBeNull();
|
||||
expect(feedback.innerHTML).toBe("Great job!");
|
||||
});
|
||||
|
||||
test("showFeedback_Success_InsertedAfterEditorContent", () => {
|
||||
showFeedback(true, "Good!");
|
||||
|
||||
const editorContent = document.querySelector(".editor-content");
|
||||
const feedback = editorContent.nextSibling;
|
||||
expect(feedback).not.toBeNull();
|
||||
expect(feedback.classList.contains("feedback-success")).toBe(true);
|
||||
});
|
||||
|
||||
test("showFeedback_Error_ToggleChecked_ShowsError", () => {
|
||||
const toggle = document.getElementById("disable-feedback-toggle");
|
||||
toggle.checked = true;
|
||||
|
||||
showFeedback(false, "Try again");
|
||||
|
||||
const feedback = document.querySelector(".feedback-error");
|
||||
expect(feedback).not.toBeNull();
|
||||
expect(feedback.innerHTML).toBe("Try again");
|
||||
});
|
||||
|
||||
test("showFeedback_Error_ToggleUnchecked_HidesError", () => {
|
||||
const toggle = document.getElementById("disable-feedback-toggle");
|
||||
toggle.checked = false;
|
||||
|
||||
showFeedback(false, "Try again");
|
||||
|
||||
const feedback = document.querySelector(".feedback-error");
|
||||
expect(feedback).toBeNull();
|
||||
});
|
||||
|
||||
test("showFeedback_Error_AutoClearsAfterTimeout", () => {
|
||||
vi.useFakeTimers();
|
||||
const toggle = document.getElementById("disable-feedback-toggle");
|
||||
toggle.checked = true;
|
||||
|
||||
showFeedback(false, "Error!");
|
||||
|
||||
expect(document.querySelector(".feedback-error")).not.toBeNull();
|
||||
|
||||
vi.advanceTimersByTime(3000);
|
||||
|
||||
expect(document.querySelector(".feedback-error")).toBeNull();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("showFeedback_Success_DoesNotAutoCleanup", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
showFeedback(true, "Good!");
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
expect(document.querySelector(".feedback-success")).not.toBeNull();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("showFeedback_CalledTwice_ClearsPrevious", () => {
|
||||
showFeedback(true, "First");
|
||||
showFeedback(true, "Second");
|
||||
|
||||
const feedbacks = document.querySelectorAll(".feedback-success");
|
||||
expect(feedbacks.length).toBe(1);
|
||||
expect(feedbacks[0].innerHTML).toBe("Second");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearFeedback", () => {
|
||||
test("clearFeedback_NoExistingFeedback_DoesNotThrow", () => {
|
||||
expect(() => clearFeedback()).not.toThrow();
|
||||
});
|
||||
|
||||
test("clearFeedback_ExistingFeedback_RemovesIt", () => {
|
||||
showFeedback(true, "Test");
|
||||
expect(document.querySelector(".feedback-success")).not.toBeNull();
|
||||
|
||||
clearFeedback();
|
||||
expect(document.querySelector(".feedback-success")).toBeNull();
|
||||
});
|
||||
|
||||
test("clearFeedback_CalledMultipleTimes_Safe", () => {
|
||||
showFeedback(true, "Test");
|
||||
clearFeedback();
|
||||
clearFeedback();
|
||||
clearFeedback();
|
||||
expect(document.querySelector(".feedback-success")).toBeNull();
|
||||
});
|
||||
|
||||
test("clearFeedback_ClearsTimeout", () => {
|
||||
vi.useFakeTimers();
|
||||
const toggle = document.getElementById("disable-feedback-toggle");
|
||||
toggle.checked = true;
|
||||
|
||||
showFeedback(false, "Error");
|
||||
clearFeedback();
|
||||
|
||||
// Advance past the timeout - should not throw
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateActiveLessonInSidebar", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<details class="module-container" data-module-id="mod1">
|
||||
<summary class="module-header">Module 1</summary>
|
||||
<div class="lessons-container">
|
||||
<button class="lesson-list-item active" data-module-id="mod1" data-lesson-index="0">L1</button>
|
||||
<button class="lesson-list-item" data-module-id="mod1" data-lesson-index="1">L2</button>
|
||||
</div>
|
||||
</details>
|
||||
<details class="module-container" data-module-id="mod2">
|
||||
<summary class="module-header">Module 2</summary>
|
||||
<div class="lessons-container">
|
||||
<button class="lesson-list-item" data-module-id="mod2" data-lesson-index="0">L1</button>
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
// Mock scrollIntoView on all lesson items (not available in jsdom)
|
||||
document.querySelectorAll(".lesson-list-item").forEach((el) => {
|
||||
el.scrollIntoView = vi.fn();
|
||||
});
|
||||
});
|
||||
|
||||
test("updateActiveLessonInSidebar_ValidLesson_ActivatesCorrectItem", () => {
|
||||
updateActiveLessonInSidebar("mod1", 1);
|
||||
|
||||
const items = document.querySelectorAll(".lesson-list-item");
|
||||
expect(items[0].classList.contains("active")).toBe(false);
|
||||
expect(items[1].classList.contains("active")).toBe(true);
|
||||
});
|
||||
|
||||
test("updateActiveLessonInSidebar_DifferentModule_ExpandsParent", () => {
|
||||
const details = document.querySelector('details[data-module-id="mod2"]');
|
||||
expect(details.open).toBe(false);
|
||||
|
||||
updateActiveLessonInSidebar("mod2", 0);
|
||||
|
||||
expect(details.open).toBe(true);
|
||||
const mod2Lesson = document.querySelector('.lesson-list-item[data-module-id="mod2"]');
|
||||
expect(mod2Lesson.classList.contains("active")).toBe(true);
|
||||
});
|
||||
|
||||
test("updateActiveLessonInSidebar_RemovesPreviousActive", () => {
|
||||
const firstItem = document.querySelector('.lesson-list-item[data-module-id="mod1"][data-lesson-index="0"]');
|
||||
expect(firstItem.classList.contains("active")).toBe(true);
|
||||
|
||||
updateActiveLessonInSidebar("mod2", 0);
|
||||
|
||||
expect(firstItem.classList.contains("active")).toBe(false);
|
||||
});
|
||||
|
||||
test("updateActiveLessonInSidebar_NonExistentItem_DoesNotThrow", () => {
|
||||
expect(() => {
|
||||
updateActiveLessonInSidebar("nonexistent", 99);
|
||||
}).not.toThrow();
|
||||
|
||||
// All active classes should still be removed
|
||||
const activeItems = document.querySelectorAll(".lesson-list-item.active");
|
||||
expect(activeItems.length).toBe(0);
|
||||
});
|
||||
|
||||
test("updateActiveLessonInSidebar_ScrollsToLesson", () => {
|
||||
const targetItem = document.querySelector('.lesson-list-item[data-module-id="mod1"][data-lesson-index="1"]');
|
||||
|
||||
updateActiveLessonInSidebar("mod1", 1);
|
||||
|
||||
expect(targetItem.scrollIntoView).toHaveBeenCalledWith({ behavior: "smooth", block: "nearest" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeLessonDifficulty - additional edge cases", () => {
|
||||
test("computeLessonDifficulty_NoSolution_ReturnsMedium", () => {
|
||||
expect(computeLessonDifficulty({ codePrefix: "" })).toBe("medium");
|
||||
});
|
||||
|
||||
test("computeLessonDifficulty_SolutionNoBrace_ReturnsMedium", () => {
|
||||
expect(
|
||||
computeLessonDifficulty({
|
||||
codePrefix: "",
|
||||
solution: "color: red;"
|
||||
})
|
||||
).toBe("medium");
|
||||
});
|
||||
|
||||
test("computeLessonDifficulty_CodePrefixWithBrace_IgnoresSolution", () => {
|
||||
expect(
|
||||
computeLessonDifficulty({
|
||||
codePrefix: ".nav a {",
|
||||
solution: ".nav a {\n color: white;\n}"
|
||||
})
|
||||
).toBe("easy");
|
||||
});
|
||||
|
||||
test("computeLessonDifficulty_NullCodePrefix_ReturnsMedium", () => {
|
||||
expect(computeLessonDifficulty({ codePrefix: null, solution: null })).toBe("medium");
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderLesson - edge cases", () => {
|
||||
test("renderLesson_NullInputEl_DoesNotThrow", () => {
|
||||
const titleEl = document.getElementById("title");
|
||||
const descriptionEl = document.getElementById("description");
|
||||
const taskEl = document.getElementById("task");
|
||||
const previewEl = document.getElementById("preview");
|
||||
const prefixEl = document.getElementById("prefix");
|
||||
const suffixEl = document.getElementById("suffix");
|
||||
const lesson = { title: "Test", description: "Desc", task: "Task" };
|
||||
|
||||
expect(() => {
|
||||
renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl, null, suffixEl, lesson);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderLevelIndicator - formatting", () => {
|
||||
test("renderLevelIndicator_ContainsLabelSpan", () => {
|
||||
const element = document.getElementById("level-indicator");
|
||||
renderLevelIndicator(element, 5, 12);
|
||||
|
||||
const label = element.querySelector(".level-label");
|
||||
expect(label).not.toBeNull();
|
||||
expect(label.textContent).toBe("Lesson");
|
||||
expect(element.textContent).toContain("5 / 12");
|
||||
});
|
||||
});
|
||||
});
|
||||
232
tests/unit/router.test.js
Normal file
232
tests/unit/router.test.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { parseHash, updateHash, navigateTo, replaceHash, replaceTo, getShareableUrl, getSectionIds, RouteType } from "../../src/helpers/router.js";
|
||||
|
||||
describe("Router", () => {
|
||||
let pushStateSpy;
|
||||
let replaceStateSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset hash
|
||||
window.location.hash = "";
|
||||
pushStateSpy = vi.spyOn(history, "pushState").mockImplementation(() => {});
|
||||
replaceStateSpy = vi.spyOn(history, "replaceState").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
pushStateSpy.mockRestore();
|
||||
replaceStateSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe("RouteType", () => {
|
||||
test("RouteType_Constants_CorrectValues", () => {
|
||||
expect(RouteType.HOME).toBe("home");
|
||||
expect(RouteType.SECTION).toBe("section");
|
||||
expect(RouteType.REFERENCE).toBe("reference");
|
||||
expect(RouteType.LESSON).toBe("lesson");
|
||||
expect(RouteType.LANGUAGE).toBe("language");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseHash", () => {
|
||||
test("parseHash_EmptyHash_ReturnsHome", () => {
|
||||
window.location.hash = "";
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.HOME });
|
||||
});
|
||||
|
||||
test("parseHash_HashOnly_ReturnsHome", () => {
|
||||
window.location.hash = "#";
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.HOME });
|
||||
});
|
||||
|
||||
test.each([
|
||||
["de", "de"],
|
||||
["pl", "pl"],
|
||||
["ar", "ar"],
|
||||
["es", "es"],
|
||||
["en", "en"],
|
||||
["uk", "uk"]
|
||||
])("parseHash_LanguageCode_%s_ReturnsLanguageRoute", (code, expectedLang) => {
|
||||
window.location.hash = `#${code}`;
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.LANGUAGE, lang: expectedLang });
|
||||
});
|
||||
|
||||
test.each([
|
||||
["css", "css"],
|
||||
["html", "html"],
|
||||
["markdown", "markdown"]
|
||||
])("parseHash_SectionId_%s_ReturnsSectionRoute", (sectionId, expectedId) => {
|
||||
window.location.hash = `#${sectionId}`;
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.SECTION, sectionId: expectedId });
|
||||
});
|
||||
|
||||
test("parseHash_ReferenceWithoutSubpage_ReturnsReferenceRouteNullRefId", () => {
|
||||
window.location.hash = "#reference";
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.REFERENCE, refId: null });
|
||||
});
|
||||
|
||||
test("parseHash_ReferenceWithSubpage_ReturnsReferenceRouteWithRefId", () => {
|
||||
window.location.hash = "#reference/css";
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.REFERENCE, refId: "css" });
|
||||
});
|
||||
|
||||
test("parseHash_ReferenceWithFlexboxSubpage_ReturnsCorrectRefId", () => {
|
||||
window.location.hash = "#reference/flexbox";
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.REFERENCE, refId: "flexbox" });
|
||||
});
|
||||
|
||||
test("parseHash_SingleUnknownSegment_ReturnsLessonWithIndex0", () => {
|
||||
window.location.hash = "#flexbox";
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.LESSON, moduleId: "flexbox", lessonIndex: 0 });
|
||||
});
|
||||
|
||||
test("parseHash_ModuleWithLessonIndex_ReturnsLessonRoute", () => {
|
||||
window.location.hash = "#flexbox/2";
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.LESSON, moduleId: "flexbox", lessonIndex: 2 });
|
||||
});
|
||||
|
||||
test("parseHash_ModuleWithIndex0_ReturnsLessonRoute", () => {
|
||||
window.location.hash = "#box-model/0";
|
||||
const result = parseHash();
|
||||
expect(result).toEqual({ type: RouteType.LESSON, moduleId: "box-model", lessonIndex: 0 });
|
||||
});
|
||||
|
||||
test("parseHash_NegativeLessonIndex_ReturnsNull", () => {
|
||||
window.location.hash = "#module/-1";
|
||||
const result = parseHash();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("parseHash_NonNumericLessonIndex_ReturnsNull", () => {
|
||||
window.location.hash = "#module/abc";
|
||||
const result = parseHash();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("parseHash_ThreeOrMoreSegments_ReturnsNull", () => {
|
||||
window.location.hash = "#a/b/c";
|
||||
const result = parseHash();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("parseHash_EmptyModuleIdWithIndex_ReturnsNull", () => {
|
||||
// #/0 → parts = ["", "0"], moduleId is empty string (falsy)
|
||||
window.location.hash = "#/0";
|
||||
const result = parseHash();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateHash", () => {
|
||||
test("updateHash_NewHash_CallsPushState", () => {
|
||||
window.location.hash = "";
|
||||
updateHash("flexbox", 2);
|
||||
expect(pushStateSpy).toHaveBeenCalledWith(null, "", "#flexbox/2");
|
||||
});
|
||||
|
||||
test("updateHash_SameHash_DoesNotCallPushState", () => {
|
||||
window.location.hash = "#flexbox/2";
|
||||
updateHash("flexbox", 2);
|
||||
expect(pushStateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("updateHash_DifferentModule_CallsPushState", () => {
|
||||
window.location.hash = "#flexbox/0";
|
||||
updateHash("box-model", 0);
|
||||
expect(pushStateSpy).toHaveBeenCalledWith(null, "", "#box-model/0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("navigateTo", () => {
|
||||
test("navigateTo_SectionRoute_CallsPushState", () => {
|
||||
window.location.hash = "";
|
||||
navigateTo("css");
|
||||
expect(pushStateSpy).toHaveBeenCalledWith(null, "", "#css");
|
||||
});
|
||||
|
||||
test("navigateTo_EmptyRoute_NavigatesToHash", () => {
|
||||
window.location.hash = "#something";
|
||||
navigateTo("");
|
||||
expect(pushStateSpy).toHaveBeenCalledWith(null, "", "#");
|
||||
});
|
||||
|
||||
test("navigateTo_SameHash_DoesNotCallPushState", () => {
|
||||
window.location.hash = "#css";
|
||||
navigateTo("css");
|
||||
expect(pushStateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("replaceHash", () => {
|
||||
test("replaceHash_ValidArgs_CallsReplaceState", () => {
|
||||
replaceHash("flexbox", 3);
|
||||
expect(replaceStateSpy).toHaveBeenCalledWith(null, "", "#flexbox/3");
|
||||
});
|
||||
|
||||
test("replaceHash_Index0_FormatsCorrectly", () => {
|
||||
replaceHash("box-model", 0);
|
||||
expect(replaceStateSpy).toHaveBeenCalledWith(null, "", "#box-model/0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("replaceTo", () => {
|
||||
test("replaceTo_Route_CallsReplaceState", () => {
|
||||
replaceTo("css");
|
||||
expect(replaceStateSpy).toHaveBeenCalledWith(null, "", "#css");
|
||||
});
|
||||
|
||||
test("replaceTo_EmptyRoute_ReplacesToHash", () => {
|
||||
replaceTo("");
|
||||
expect(replaceStateSpy).toHaveBeenCalledWith(null, "", "#");
|
||||
});
|
||||
|
||||
test("replaceTo_ReferenceRoute_FormatsCorrectly", () => {
|
||||
replaceTo("reference/flexbox");
|
||||
expect(replaceStateSpy).toHaveBeenCalledWith(null, "", "#reference/flexbox");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getShareableUrl", () => {
|
||||
test("getShareableUrl_ValidArgs_ReturnsFullUrl", () => {
|
||||
const url = getShareableUrl("flexbox", 2);
|
||||
expect(url).toContain("#flexbox/2");
|
||||
expect(url).toMatch(/^https?:\/\/.+#flexbox\/2$/);
|
||||
});
|
||||
|
||||
test("getShareableUrl_Index0_IncludesIndex", () => {
|
||||
const url = getShareableUrl("box-model", 0);
|
||||
expect(url).toContain("#box-model/0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSectionIds", () => {
|
||||
test("getSectionIds_ReturnsCopy_NotOriginalArray", () => {
|
||||
const ids1 = getSectionIds();
|
||||
const ids2 = getSectionIds();
|
||||
expect(ids1).toEqual(ids2);
|
||||
expect(ids1).not.toBe(ids2); // Different references
|
||||
});
|
||||
|
||||
test("getSectionIds_ContainsExpectedSections", () => {
|
||||
const ids = getSectionIds();
|
||||
expect(ids).toContain("css");
|
||||
expect(ids).toContain("html");
|
||||
expect(ids).toContain("markdown");
|
||||
});
|
||||
|
||||
test("getSectionIds_MutatingCopy_DoesNotAffectOriginal", () => {
|
||||
const ids = getSectionIds();
|
||||
ids.push("custom");
|
||||
const freshIds = getSectionIds();
|
||||
expect(freshIds).not.toContain("custom");
|
||||
});
|
||||
});
|
||||
});
|
||||
172
tests/unit/sections.test.js
Normal file
172
tests/unit/sections.test.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { sections, getSection, getSectionList, getModuleSection, getModulesBySection } from "../../src/config/sections.js";
|
||||
|
||||
describe("Sections Config", () => {
|
||||
describe("sections constant", () => {
|
||||
test("sections_AllDefined_HasFourSections", () => {
|
||||
expect(Object.keys(sections)).toHaveLength(4);
|
||||
expect(sections).toHaveProperty("css");
|
||||
expect(sections).toHaveProperty("html");
|
||||
expect(sections).toHaveProperty("tailwind");
|
||||
expect(sections).toHaveProperty("markdown");
|
||||
});
|
||||
|
||||
test("sections_EachSection_HasRequiredFields", () => {
|
||||
for (const [key, section] of Object.entries(sections)) {
|
||||
expect(section.id).toBe(key);
|
||||
expect(section.title).toBeTruthy();
|
||||
expect(section.description).toBeTruthy();
|
||||
expect(section.color).toMatch(/^#[0-9a-f]{6}$/);
|
||||
expect(typeof section.order).toBe("number");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSection", () => {
|
||||
test.each([
|
||||
["css", "CSS"],
|
||||
["html", "HTML"],
|
||||
["tailwind", "Tailwind CSS"],
|
||||
["markdown", "Markdown"]
|
||||
])("getSection_%s_ReturnsCorrectSection", (id, expectedTitle) => {
|
||||
const section = getSection(id);
|
||||
expect(section).not.toBeNull();
|
||||
expect(section.id).toBe(id);
|
||||
expect(section.title).toBe(expectedTitle);
|
||||
});
|
||||
|
||||
test("getSection_NonExistentId_ReturnsNull", () => {
|
||||
expect(getSection("nonexistent")).toBeNull();
|
||||
});
|
||||
|
||||
test("getSection_Undefined_ReturnsNull", () => {
|
||||
expect(getSection(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
test("getSection_EmptyString_ReturnsNull", () => {
|
||||
expect(getSection("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSectionList", () => {
|
||||
test("getSectionList_Default_ReturnsSortedByOrder", () => {
|
||||
const list = getSectionList();
|
||||
expect(list).toHaveLength(4);
|
||||
|
||||
// Verify sorted by order
|
||||
for (let i = 1; i < list.length; i++) {
|
||||
expect(list[i].order).toBeGreaterThan(list[i - 1].order);
|
||||
}
|
||||
});
|
||||
|
||||
test("getSectionList_Default_CSSIsFirst", () => {
|
||||
const list = getSectionList();
|
||||
expect(list[0].id).toBe("css");
|
||||
});
|
||||
|
||||
test("getSectionList_Default_MarkdownIsLast", () => {
|
||||
const list = getSectionList();
|
||||
expect(list[list.length - 1].id).toBe("markdown");
|
||||
});
|
||||
|
||||
test("getSectionList_Default_ContainsAllSections", () => {
|
||||
const list = getSectionList();
|
||||
const ids = list.map((s) => s.id);
|
||||
expect(ids).toContain("css");
|
||||
expect(ids).toContain("html");
|
||||
expect(ids).toContain("tailwind");
|
||||
expect(ids).toContain("markdown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getModuleSection", () => {
|
||||
test("getModuleSection_ExplicitSection_UsesExplicitValue", () => {
|
||||
const module = { mode: "css", section: "html" };
|
||||
expect(getModuleSection(module)).toBe("html");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["css", "css"],
|
||||
["html", "html"],
|
||||
["tailwind", "tailwind"],
|
||||
["markdown", "markdown"]
|
||||
])("getModuleSection_Mode%s_InfersCorrectSection", (mode, expectedSection) => {
|
||||
const module = { mode };
|
||||
expect(getModuleSection(module)).toBe(expectedSection);
|
||||
});
|
||||
|
||||
test("getModuleSection_NoMode_DefaultsToCss", () => {
|
||||
expect(getModuleSection({})).toBe("css");
|
||||
});
|
||||
|
||||
test("getModuleSection_UndefinedMode_DefaultsToCss", () => {
|
||||
expect(getModuleSection({ mode: undefined })).toBe("css");
|
||||
});
|
||||
|
||||
test("getModuleSection_UnknownMode_DefaultsToCss", () => {
|
||||
expect(getModuleSection({ mode: "javascript" })).toBe("css");
|
||||
});
|
||||
|
||||
test("getModuleSection_ExplicitSectionOverridesMode_UsesSection", () => {
|
||||
const module = { mode: "html", section: "tailwind" };
|
||||
expect(getModuleSection(module)).toBe("tailwind");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getModulesBySection", () => {
|
||||
const testModules = [
|
||||
{ id: "css-basics", mode: "css" },
|
||||
{ id: "flexbox", mode: "css" },
|
||||
{ id: "html-elements", mode: "html" },
|
||||
{ id: "tailwind-intro", mode: "tailwind" },
|
||||
{ id: "markdown-basics", mode: "markdown" },
|
||||
{ id: "welcome", mode: "css", excludeFromProgress: true },
|
||||
{ id: "playground", mode: "css", excludeFromProgress: true }
|
||||
];
|
||||
|
||||
test("getModulesBySection_Css_ReturnsCssModules", () => {
|
||||
const result = getModulesBySection(testModules, "css");
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((m) => m.id)).toEqual(["css-basics", "flexbox"]);
|
||||
});
|
||||
|
||||
test("getModulesBySection_Html_ReturnsHtmlModules", () => {
|
||||
const result = getModulesBySection(testModules, "html");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe("html-elements");
|
||||
});
|
||||
|
||||
test("getModulesBySection_Tailwind_ReturnsTailwindModules", () => {
|
||||
const result = getModulesBySection(testModules, "tailwind");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe("tailwind-intro");
|
||||
});
|
||||
|
||||
test("getModulesBySection_ExcludesFromProgress_FiltersOut", () => {
|
||||
const result = getModulesBySection(testModules, "css");
|
||||
const ids = result.map((m) => m.id);
|
||||
expect(ids).not.toContain("welcome");
|
||||
expect(ids).not.toContain("playground");
|
||||
});
|
||||
|
||||
test("getModulesBySection_EmptyModules_ReturnsEmptyArray", () => {
|
||||
const result = getModulesBySection([], "css");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test("getModulesBySection_NonExistentSection_ReturnsEmptyArray", () => {
|
||||
const result = getModulesBySection(testModules, "nonexistent");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test("getModulesBySection_ExplicitSectionOverride_IncludesModule", () => {
|
||||
const modules = [
|
||||
{ id: "special", mode: "css", section: "html" },
|
||||
{ id: "normal-html", mode: "html" }
|
||||
];
|
||||
const result = getModulesBySection(modules, "html");
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((m) => m.id)).toContain("special");
|
||||
});
|
||||
});
|
||||
});
|
||||
735
tests/unit/validator-extended.test.js
Normal file
735
tests/unit/validator-extended.test.js
Normal file
@@ -0,0 +1,735 @@
|
||||
import { describe, test, expect, vi, beforeEach } from "vitest";
|
||||
import { validateUserCode, validateCssCode } from "../../src/helpers/validator.js";
|
||||
|
||||
describe("Validator Extended Coverage", () => {
|
||||
describe("validateUserCode mode dispatch", () => {
|
||||
test("validateUserCode_NoMode_DefaultsToCss", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [{ type: "contains", value: "color: red" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("validateUserCode_CssMode_UsesCssValidator", () => {
|
||||
const result = validateUserCode("display: flex;", {
|
||||
mode: "css",
|
||||
validations: [{ type: "contains", value: "display: flex" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("validateUserCode_TailwindMode_UsesTailwindValidator", () => {
|
||||
const result = validateUserCode("flex items-center", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_class", value: "flex" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("validateUserCode_HtmlMode_UsesHtmlValidator", () => {
|
||||
const result = validateUserCode("<div>Hello</div>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "element_exists", value: "div" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("validateUserCode_UnknownMode_DefaultsToCss", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
mode: "javascript",
|
||||
validations: [{ type: "contains", value: "color: red" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("validateUserCode_NullLesson_Throws", () => {
|
||||
expect(() => validateUserCode("anything", null)).toThrow();
|
||||
});
|
||||
|
||||
test("validateUserCode_UndefinedLesson_Throws", () => {
|
||||
expect(() => validateUserCode("anything", undefined)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tailwind validation", () => {
|
||||
test("tailwind_ContainsClass_Pass", () => {
|
||||
const result = validateUserCode("flex items-center justify-between", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_class", value: "flex" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.validCases).toBe(1);
|
||||
});
|
||||
|
||||
test("tailwind_ContainsClass_Fail_ReturnsMessage", () => {
|
||||
const result = validateUserCode("items-center", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_class", value: "flex", message: "Add flex class" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Add flex class");
|
||||
});
|
||||
|
||||
test("tailwind_ContainsClass_Fail_DefaultMessage", () => {
|
||||
const result = validateUserCode("items-center", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_class", value: "flex" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toContain("flex");
|
||||
});
|
||||
|
||||
test("tailwind_ContainsClass_PartialMatch_Fails", () => {
|
||||
// "flex-1" contains "flex" as substring but split should not match
|
||||
const result = validateUserCode("flex-1 items-center", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_class", value: "flex" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("tailwind_ContainsPattern_Pass", () => {
|
||||
const result = validateUserCode("text-lg font-bold", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_pattern", value: "text-\\w+" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("tailwind_ContainsPattern_Fail_ReturnsMessage", () => {
|
||||
const result = validateUserCode("font-bold", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_pattern", value: "text-\\w+", message: "Add text size" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Add text size");
|
||||
});
|
||||
|
||||
test("tailwind_ContainsPattern_Fail_DefaultMessage", () => {
|
||||
const result = validateUserCode("font-bold", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_pattern", value: "text-\\w+" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toContain("pattern");
|
||||
});
|
||||
|
||||
test("tailwind_DefaultType_FallsBackToContains", () => {
|
||||
const result = validateUserCode("flex items-center", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains", value: "items-center" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("tailwind_NoValidations_ReturnsValid", () => {
|
||||
const result = validateUserCode("flex", { mode: "tailwind" });
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.message).toContain("No validations specified");
|
||||
});
|
||||
|
||||
test("tailwind_NullLesson_ReturnsValid", () => {
|
||||
const result = validateUserCode("flex", { mode: "tailwind", validations: null });
|
||||
// validateTailwindClasses checks !lesson.validations
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("tailwind_MultipleValidations_AllPass", () => {
|
||||
const result = validateUserCode("flex items-center gap-4", {
|
||||
mode: "tailwind",
|
||||
validations: [
|
||||
{ type: "contains_class", value: "flex" },
|
||||
{ type: "contains_class", value: "items-center" },
|
||||
{ type: "contains_class", value: "gap-4" }
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.validCases).toBe(3);
|
||||
});
|
||||
|
||||
test("tailwind_MultipleValidations_EarlyReturn", () => {
|
||||
const result = validateUserCode("flex", {
|
||||
mode: "tailwind",
|
||||
validations: [
|
||||
{ type: "contains_class", value: "flex" },
|
||||
{ type: "contains_class", value: "items-center", message: "Missing items-center" },
|
||||
{ type: "contains_class", value: "gap-4" }
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Missing items-center");
|
||||
expect(result.validCases).toBe(1);
|
||||
});
|
||||
|
||||
test("tailwind_WhitespaceHandling_LeadingTrailing", () => {
|
||||
const result = validateUserCode(" flex items-center ", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_class", value: "flex" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("tailwind_EmptyUserClasses_Fails", () => {
|
||||
const result = validateUserCode("", {
|
||||
mode: "tailwind",
|
||||
validations: [{ type: "contains_class", value: "flex" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - sibling type", () => {
|
||||
test("sibling_ValidOrder_Passes", () => {
|
||||
const result = validateUserCode("<h1>Title</h1><p>Content</p>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "sibling", value: { first: "h1", then: "p" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("sibling_NonAdjacentButAfter_Passes", () => {
|
||||
const result = validateUserCode("<h1>Title</h1><span>Middle</span><p>Content</p>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "sibling", value: { first: "h1", then: "p" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("sibling_WrongOrder_Fails", () => {
|
||||
const result = validateUserCode("<p>Content</p><h1>Title</h1>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "sibling", value: { first: "h1", then: "p" } }]
|
||||
});
|
||||
// h1 is after p, so p is not a sibling after h1 - but wait, h1 exists and p is before h1...
|
||||
// Actually h1 exists. nextElementSibling of h1 is nothing. So it fails.
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("sibling_FirstNotFound_Fails", () => {
|
||||
const result = validateUserCode("<p>Content</p>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "sibling", value: { first: "h1", then: "p" }, message: "h1 not found" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("h1 not found");
|
||||
});
|
||||
|
||||
test("sibling_ThenNotFound_Fails", () => {
|
||||
const result = validateUserCode("<h1>Title</h1><span>Only span</span>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "sibling", value: { first: "h1", then: "p" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("sibling_DefaultMessage_ContainsBothSelectors", () => {
|
||||
const result = validateUserCode("<div>Only div</div>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "sibling", value: { first: "h1", then: "p" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toContain("p");
|
||||
expect(result.message).toContain("h1");
|
||||
});
|
||||
|
||||
test("sibling_NoFollowingSiblings_Fails", () => {
|
||||
const result = validateUserCode("<div><h1>Title</h1></div>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "sibling", value: { first: "h1", then: "p" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - not_contains type", () => {
|
||||
test("htmlNotContains_AbsentText_Passes", () => {
|
||||
const result = validateUserCode("<p>Hello</p>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "not_contains", value: "class=" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("htmlNotContains_PresentText_Fails", () => {
|
||||
const result = validateUserCode('<p class="red">Hello</p>', {
|
||||
mode: "html",
|
||||
validations: [{ type: "not_contains", value: "class=", message: "Remove classes" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Remove classes");
|
||||
});
|
||||
|
||||
test("htmlNotContains_DefaultMessage", () => {
|
||||
const result = validateUserCode('<p class="red">Hello</p>', {
|
||||
mode: "html",
|
||||
validations: [{ type: "not_contains", value: "class=" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toContain("should not include");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - regex type", () => {
|
||||
test("htmlRegex_MatchingPattern_Passes", () => {
|
||||
const result = validateUserCode('<img src="photo.jpg" alt="A photo">', {
|
||||
mode: "html",
|
||||
validations: [{ type: "regex", value: 'alt="[^"]+"' }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("htmlRegex_NonMatchingPattern_Fails", () => {
|
||||
const result = validateUserCode('<img src="photo.jpg">', {
|
||||
mode: "html",
|
||||
validations: [{ type: "regex", value: 'alt="[^"]+"', message: "Add alt text" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Add alt text");
|
||||
});
|
||||
|
||||
test("htmlRegex_DefaultMessage", () => {
|
||||
const result = validateUserCode("<p>Hello</p>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "regex", value: "<h1>" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toContain("pattern");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - unknown type", () => {
|
||||
test("htmlUnknownType_SkipsAndPasses", () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const result = validateUserCode("<p>Hello</p>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "unknown_type", value: "anything" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown HTML validation type"));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - element_count fallback (>0)", () => {
|
||||
test("elementCount_NoCountNoMin_ChecksGreaterThanZero_Pass", () => {
|
||||
const result = validateUserCode("<ul><li>Item</li></ul>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "element_count", value: { selector: "li" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("elementCount_NoCountNoMin_NoElements_Fails", () => {
|
||||
const result = validateUserCode("<ul></ul>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "element_count", value: { selector: "li" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - attribute_value edge cases", () => {
|
||||
test("attributeValue_ElementNotFound_Fails", () => {
|
||||
const result = validateUserCode("<p>Hello</p>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "attribute_value", value: { selector: "input", attr: "type", value: "email" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("attributeValue_NullValue_ChecksExists", () => {
|
||||
const result = validateUserCode('<input data-test="anything">', {
|
||||
mode: "html",
|
||||
validations: [{ type: "attribute_value", value: { selector: "input", attr: "data-test", value: null } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("attributeValue_NullValue_AttributeMissing_Fails", () => {
|
||||
const result = validateUserCode("<input>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "attribute_value", value: { selector: "input", attr: "data-test", value: null } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - element_text edge cases", () => {
|
||||
test("elementText_ElementNotFound_Fails", () => {
|
||||
const result = validateUserCode("<p>Hello</p>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "element_text", value: { selector: "button", text: "Submit" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("elementText_EmptyTextContent_FailsForNonEmptyExpected", () => {
|
||||
const result = validateUserCode("<button></button>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "element_text", value: { selector: "button", text: "Submit" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("elementText_EmptyExpectedText_MatchesEmptyElement", () => {
|
||||
const result = validateUserCode("<button></button>", {
|
||||
mode: "html",
|
||||
validations: [{ type: "element_text", value: { selector: "button", text: "" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - containsValidation wholeWord option", () => {
|
||||
test("contains_WholeWord_ExactMatch_Passes", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [{ type: "contains", value: "red", options: { wholeWord: true } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("contains_WholeWord_PartialMatch_Fails", () => {
|
||||
const result = validateUserCode("color: darkred;", {
|
||||
validations: [{ type: "contains", value: "red", options: { wholeWord: true } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("contains_WholeWord_CaseInsensitive_Passes", () => {
|
||||
const result = validateUserCode("COLOR: RED;", {
|
||||
validations: [{ type: "contains", value: "red", options: { wholeWord: true, caseSensitive: false } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("contains_WholeWord_SpecialChars_Escaped", () => {
|
||||
// \b doesn't match at non-word chars like ".", so use a word value with special chars around it
|
||||
const result = validateUserCode("value: calc(100% - 20px);", {
|
||||
validations: [{ type: "contains", value: "calc", options: { wholeWord: true } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
|
||||
// "calc" should not match "recalculate"
|
||||
const failResult = validateUserCode("/* recalculate */", {
|
||||
validations: [{ type: "contains", value: "calc", options: { wholeWord: true } }]
|
||||
});
|
||||
expect(failResult.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - regexValidation options", () => {
|
||||
test("regex_CaseInsensitive_Passes", () => {
|
||||
const result = validateUserCode("COLOR: RED;", {
|
||||
validations: [{ type: "regex", value: "color:\\s*red", options: { caseSensitive: false } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("regex_CaseSensitive_Default_FailsOnCaseMismatch", () => {
|
||||
const result = validateUserCode("COLOR: RED;", {
|
||||
validations: [{ type: "regex", value: "color:\\s*red" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("regex_MultilineFalse_DoesNotMatchAcrossLines", () => {
|
||||
const code = "body {\n color: red;\n}";
|
||||
// With multiline=false, ^ should not match beginning of each line
|
||||
const result = validateUserCode(code, {
|
||||
validations: [{ type: "regex", value: "^\\s*color", options: { multiline: false } }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("regex_MultilineTrue_Default_MatchesEachLine", () => {
|
||||
const code = "body {\n color: red;\n}";
|
||||
const result = validateUserCode(code, {
|
||||
validations: [{ type: "regex", value: "^\\s*color", options: { multiline: true } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("regex_InvalidPattern_ReturnsFalse", () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [{ type: "regex", value: "[invalid(regex" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("regex_EmptyPattern_MatchesEverything", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [{ type: "regex", value: "" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - propertyValueValidation edge cases", () => {
|
||||
test("propertyValue_PropertyNotFound_Fails", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [
|
||||
{
|
||||
type: "property_value",
|
||||
value: { property: "display", expected: "flex" }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("propertyValue_ExactMatch_Passes", () => {
|
||||
const result = validateUserCode("display: flex;", {
|
||||
validations: [
|
||||
{
|
||||
type: "property_value",
|
||||
value: { property: "display", expected: "flex" },
|
||||
options: { exact: true }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("propertyValue_ExactMatch_CaseMismatch_Fails", () => {
|
||||
const result = validateUserCode("display: FLEX;", {
|
||||
validations: [
|
||||
{
|
||||
type: "property_value",
|
||||
value: { property: "display", expected: "flex" },
|
||||
options: { exact: true }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("propertyValue_FlexibleMatch_CaseInsensitive", () => {
|
||||
const result = validateUserCode("display: FLEX;", {
|
||||
validations: [
|
||||
{
|
||||
type: "property_value",
|
||||
value: { property: "display", expected: "flex" }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("propertyValue_ShorthandProperty_Passes", () => {
|
||||
const result = validateUserCode("margin: 10px 20px;", {
|
||||
validations: [
|
||||
{
|
||||
type: "property_value",
|
||||
value: { property: "margin", expected: "10px 20px" }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("propertyValue_DefaultMessage_IncludesPropertyAndExpected", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [
|
||||
{
|
||||
type: "property_value",
|
||||
value: { property: "display", expected: "flex" }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toContain("display");
|
||||
expect(result.message).toContain("flex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - syntaxValidation", () => {
|
||||
test("syntax_ValidCss_Passes", () => {
|
||||
const result = validateUserCode("div { color: red; }", {
|
||||
validations: [{ type: "syntax" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - custom edge cases", () => {
|
||||
test("custom_NoValidatorFunction_ReturnsEarlyWithOriginalResult", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [{ type: "custom" }]
|
||||
});
|
||||
// When validator is falsy, validationPassed stays false, but result.isValid was never set to false
|
||||
// The function returns early with the unmodified result (isValid: true)
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("custom_NonFunctionValidator_ReturnsEarlyWithOriginalResult", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [{ type: "custom", validator: "not-a-function" }]
|
||||
});
|
||||
// Same behavior: validator check fails, validationPassed stays false, returns unmodified result
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("custom_ValidatorReturnsNoMessage_UsesMessage", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [
|
||||
{
|
||||
type: "custom",
|
||||
validator: () => ({ isValid: false }),
|
||||
message: "Fallback message"
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toBe("Fallback message");
|
||||
});
|
||||
|
||||
test("custom_ValidatorReturnsNoMessage_NoLessonMessage_DefaultMessage", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [
|
||||
{
|
||||
type: "custom",
|
||||
validator: () => ({ isValid: false })
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.message).toContain("does not meet the requirements");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - unknown type", () => {
|
||||
test("unknownType_WarnsAndContinues", () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [
|
||||
{ type: "invented_type", value: "anything" },
|
||||
{ type: "contains", value: "color: red" }
|
||||
]
|
||||
});
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown validation type"));
|
||||
// The unknown type is skipped (continue), then the next validation passes
|
||||
expect(result.isValid).toBe(true);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - empty and whitespace input", () => {
|
||||
test("emptyString_ContainsValidation_Fails", () => {
|
||||
const result = validateUserCode("", {
|
||||
validations: [{ type: "contains", value: "color" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("whitespaceOnly_ContainsValidation_Fails", () => {
|
||||
const result = validateUserCode(" \n\t ", {
|
||||
validations: [{ type: "contains", value: "color" }]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
test("emptyString_NotContains_Passes", () => {
|
||||
const result = validateUserCode("", {
|
||||
validations: [{ type: "not_contains", value: "color" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - validCases and totalCases tracking", () => {
|
||||
test("allPassingValidations_ValidCasesEqualsTotalCases", () => {
|
||||
const result = validateUserCode("display: flex; color: red;", {
|
||||
validations: [
|
||||
{ type: "contains", value: "display: flex" },
|
||||
{ type: "contains", value: "color: red" }
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.validCases).toBe(2);
|
||||
expect(result.totalCases).toBe(2);
|
||||
});
|
||||
|
||||
test("firstValidationFails_ValidCasesIs0", () => {
|
||||
const result = validateUserCode("color: red;", {
|
||||
validations: [
|
||||
{ type: "contains", value: "display: flex" },
|
||||
{ type: "contains", value: "color: red" }
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.validCases).toBe(0);
|
||||
expect(result.totalCases).toBe(2);
|
||||
});
|
||||
|
||||
test("secondValidationFails_ValidCasesIs1", () => {
|
||||
const result = validateUserCode("display: flex;", {
|
||||
validations: [
|
||||
{ type: "contains", value: "display: flex" },
|
||||
{ type: "contains", value: "color: red" }
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.validCases).toBe(1);
|
||||
expect(result.totalCases).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS validation - special regex metacharacters in contains", () => {
|
||||
test("contains_DotInValue_TreatedAsLiteral", () => {
|
||||
// ".class" should match literally, not any char + "class"
|
||||
const result = validateUserCode(".card { color: red; }", {
|
||||
validations: [{ type: "contains", value: ".card" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test("contains_BracketsInValue_TreatedAsLiteral", () => {
|
||||
const result = validateUserCode("content: '[test]';", {
|
||||
validations: [{ type: "contains", value: "[test]" }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - deeply nested parent_child", () => {
|
||||
test("parentChild_DeeplyNested_Passes", () => {
|
||||
const html = "<div><section><article><p>Deep</p></article></section></div>";
|
||||
const result = validateUserCode(html, {
|
||||
mode: "html",
|
||||
validations: [{ type: "parent_child", value: { parent: "div", child: "p" } }]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML validation - validCases tracking", () => {
|
||||
test("htmlAllPass_ValidCasesEqualsTotal", () => {
|
||||
const result = validateUserCode("<h1>Title</h1><p>Content</p>", {
|
||||
mode: "html",
|
||||
validations: [
|
||||
{ type: "element_exists", value: "h1" },
|
||||
{ type: "element_exists", value: "p" }
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.validCases).toBe(2);
|
||||
expect(result.totalCases).toBe(2);
|
||||
});
|
||||
|
||||
test("htmlPartialPass_EarlyReturn", () => {
|
||||
const result = validateUserCode("<h1>Title</h1>", {
|
||||
mode: "html",
|
||||
validations: [
|
||||
{ type: "element_exists", value: "h1" },
|
||||
{ type: "element_exists", value: "p", message: "Need paragraph" }
|
||||
]
|
||||
});
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.validCases).toBe(1);
|
||||
expect(result.message).toBe("Need paragraph");
|
||||
});
|
||||
});
|
||||
});
|
||||
376
wave.yaml
Normal file
376
wave.yaml
Normal file
@@ -0,0 +1,376 @@
|
||||
adapters:
|
||||
claude:
|
||||
binary: claude
|
||||
default_permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
deny: []
|
||||
mode: headless
|
||||
output_format: json
|
||||
project_files:
|
||||
- CLAUDE.md
|
||||
- .claude/settings.json
|
||||
apiVersion: v1
|
||||
kind: WaveManifest
|
||||
metadata:
|
||||
description: An interactive platform for learning CSS through practical challenges
|
||||
name: code-crispies
|
||||
ontology:
|
||||
telos: Interactive self-learning platform for web technologies (CSS, HTML, JavaScript, Markdown)
|
||||
personas:
|
||||
auditor:
|
||||
adapter: claude
|
||||
description: Security review and quality assurance
|
||||
model: claude-haiku
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Grep
|
||||
- Glob
|
||||
- Bash
|
||||
deny:
|
||||
- Edit(*)
|
||||
- Bash(rm -rf /*)
|
||||
- Bash(git push*)
|
||||
- Bash(git commit*)
|
||||
system_prompt_file: .wave/personas/auditor.md
|
||||
temperature: 0.1
|
||||
craftsman:
|
||||
adapter: claude
|
||||
description: Code implementation and testing
|
||||
model: claude-opus
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
deny:
|
||||
- Bash(rm -rf /*)
|
||||
system_prompt_file: .wave/personas/craftsman.md
|
||||
temperature: 0.7
|
||||
debugger:
|
||||
adapter: claude
|
||||
description: Systematic debugging and root cause analysis
|
||||
model: claude-opus
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
deny:
|
||||
- Edit(*)
|
||||
- Bash(rm -rf /*)
|
||||
- Bash(git push*)
|
||||
- Bash(git commit*)
|
||||
system_prompt_file: .wave/personas/debugger.md
|
||||
temperature: 0.1
|
||||
gitea-analyst:
|
||||
adapter: claude
|
||||
description: Gitea issue analysis and scanning
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash(tea issues view*)
|
||||
- Bash(tea issues list*)
|
||||
- Bash(tea releases list*)
|
||||
- Bash(tea pulls view*)
|
||||
- Bash(tea pulls list*)
|
||||
- Bash(tea --version)
|
||||
- Bash(git log*)
|
||||
- Bash(git status*)
|
||||
- Bash(ls *)
|
||||
deny:
|
||||
- Bash(tea issues edit*)
|
||||
- Bash(tea issues create*)
|
||||
- Bash(tea issues close*)
|
||||
- Bash(gh *)
|
||||
- Bash(glab *)
|
||||
- Edit(*)
|
||||
system_prompt_file: .wave/personas/gitea-analyst.md
|
||||
temperature: 0.1
|
||||
gitea-commenter:
|
||||
adapter: claude
|
||||
description: Posts comments on Gitea issues and pull requests
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash(tea issues comment*)
|
||||
- Bash(tea pulls create*)
|
||||
- Bash(tea --version)
|
||||
- Bash(git push*)
|
||||
- Bash(git status*)
|
||||
- Bash(git log*)
|
||||
- Bash(git remote*)
|
||||
- Bash(git diff*)
|
||||
deny:
|
||||
- Bash(tea issues edit*)
|
||||
- Bash(tea issues close*)
|
||||
- Bash(tea pulls merge*)
|
||||
- Bash(tea pulls close*)
|
||||
- Bash(gh *)
|
||||
- Bash(glab *)
|
||||
- Edit(*)
|
||||
system_prompt_file: .wave/personas/gitea-commenter.md
|
||||
temperature: 0.2
|
||||
gitea-enhancer:
|
||||
adapter: claude
|
||||
description: Gitea issue enhancement and improvement
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash(tea issues edit*)
|
||||
- Bash(tea issues view*)
|
||||
- Bash(tea --version)
|
||||
deny:
|
||||
- Bash(tea issues create*)
|
||||
- Bash(tea issues close*)
|
||||
- Bash(gh *)
|
||||
- Bash(glab *)
|
||||
- Edit(*)
|
||||
system_prompt_file: .wave/personas/gitea-enhancer.md
|
||||
temperature: 0.2
|
||||
gitea-scoper:
|
||||
adapter: claude
|
||||
description: Gitea epic analysis, decomposition, and sub-issue creation
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash(tea issues create*)
|
||||
- Bash(tea issues view*)
|
||||
- Bash(tea issues list*)
|
||||
- Bash(tea --version)
|
||||
deny:
|
||||
- Bash(tea issues edit*)
|
||||
- Bash(tea issues close*)
|
||||
- Bash(gh *)
|
||||
- Bash(glab *)
|
||||
- Edit(*)
|
||||
system_prompt_file: .wave/personas/gitea-scoper.md
|
||||
temperature: 0.1
|
||||
implementer:
|
||||
adapter: claude
|
||||
description: Execution specialist for code changes and structured output
|
||||
model: claude-opus
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
deny:
|
||||
- Bash(rm -rf /*)
|
||||
- Bash(sudo *)
|
||||
system_prompt_file: .wave/personas/implementer.md
|
||||
temperature: 0.3
|
||||
navigator:
|
||||
adapter: claude
|
||||
description: Read-only codebase exploration and analysis
|
||||
model: claude-haiku
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash(git log*)
|
||||
- Bash(git status*)
|
||||
deny:
|
||||
- Edit(*)
|
||||
- Bash(git commit*)
|
||||
- Bash(git push*)
|
||||
system_prompt_file: .wave/personas/navigator.md
|
||||
temperature: 0.1
|
||||
philosopher:
|
||||
adapter: claude
|
||||
description: Architecture design and specification
|
||||
model: claude-opus
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
deny: []
|
||||
system_prompt_file: .wave/personas/philosopher.md
|
||||
temperature: 0.3
|
||||
planner:
|
||||
adapter: claude
|
||||
description: Task breakdown and planning
|
||||
model: claude-haiku
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
deny: []
|
||||
system_prompt_file: .wave/personas/planner.md
|
||||
temperature: 0.2
|
||||
provocateur:
|
||||
adapter: claude
|
||||
description: Creative challenger for divergent thinking and complexity hunting
|
||||
model: claude-opus
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash(wc *)
|
||||
- Bash(git log*)
|
||||
- Bash(git diff*)
|
||||
- Bash(find*)
|
||||
- Bash(ls*)
|
||||
deny:
|
||||
- Edit(*)
|
||||
- Bash(git commit*)
|
||||
- Bash(git push*)
|
||||
- Bash(rm*)
|
||||
system_prompt_file: .wave/personas/provocateur.md
|
||||
temperature: 0.8
|
||||
researcher:
|
||||
adapter: claude
|
||||
description: Deep codebase research and analysis
|
||||
model: claude-opus
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
deny: []
|
||||
system_prompt_file: .wave/personas/researcher.md
|
||||
temperature: 0.1
|
||||
reviewer:
|
||||
adapter: claude
|
||||
description: Code review and quality checks
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
deny:
|
||||
- Write(*.go)
|
||||
- Write(*.ts)
|
||||
- Write(*.py)
|
||||
- Write(*.rs)
|
||||
- Edit(*)
|
||||
- Bash(rm *)
|
||||
- Bash(git push*)
|
||||
- Bash(git commit*)
|
||||
system_prompt_file: .wave/personas/reviewer.md
|
||||
temperature: 0.1
|
||||
summarizer:
|
||||
adapter: claude
|
||||
description: Context compaction for relay handoffs
|
||||
model: claude-haiku
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
deny: []
|
||||
system_prompt_file: .wave/personas/summarizer.md
|
||||
temperature: 0
|
||||
supervisor:
|
||||
adapter: claude
|
||||
description: Work supervision and quality evaluation
|
||||
model: claude-opus
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
deny:
|
||||
- Edit(*)
|
||||
- Bash(git push*)
|
||||
- Bash(git commit*)
|
||||
- Bash(rm*)
|
||||
system_prompt_file: .wave/personas/supervisor.md
|
||||
temperature: 0.2
|
||||
synthesizer:
|
||||
adapter: claude
|
||||
description: Structured synthesis of analysis findings into actionable JSON proposals
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
deny: []
|
||||
system_prompt_file: .wave/personas/synthesizer.md
|
||||
temperature: 0.2
|
||||
validator:
|
||||
adapter: claude
|
||||
description: Skeptical analysis and verification of findings against source code
|
||||
permissions:
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash(wc *)
|
||||
- Bash(git log*)
|
||||
- Bash(git diff*)
|
||||
deny:
|
||||
- Edit(*)
|
||||
- Bash(git commit*)
|
||||
- Bash(git push*)
|
||||
- Bash(rm*)
|
||||
system_prompt_file: .wave/personas/validator.md
|
||||
temperature: 0.1
|
||||
project:
|
||||
build_command: npm run build
|
||||
flavour: node
|
||||
format_command: npm run format
|
||||
language: javascript
|
||||
lint_command: ""
|
||||
skill: javascript
|
||||
source_glob: '*.{js,jsx,ts,tsx}'
|
||||
test_command: npm test
|
||||
runtime:
|
||||
audit:
|
||||
log_all_file_operations: false
|
||||
log_all_tool_calls: true
|
||||
log_dir: .wave/traces/
|
||||
default_timeout_minutes: 30
|
||||
max_concurrent_workers: 5
|
||||
meta_pipeline:
|
||||
max_depth: 2
|
||||
max_total_steps: 20
|
||||
max_total_tokens: 500000
|
||||
timeout_minutes: 60
|
||||
relay:
|
||||
strategy: summarize_to_checkpoint
|
||||
token_threshold_percent: 80
|
||||
workspace_root: .wave/workspaces
|
||||
Reference in New Issue
Block a user